diff options
Diffstat (limited to 'plugins')
157 files changed, 32847 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 diff --git a/plugins/dpkg/gs-plugin-dpkg.c b/plugins/dpkg/gs-plugin-dpkg.c new file mode 100644 index 0000000..48fb72a --- /dev/null +++ b/plugins/dpkg/gs-plugin-dpkg.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-2013 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <stdlib.h> +#include <gnome-software.h> + +#define DPKG_DEB_BINARY "/usr/bin/dpkg-deb" + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + if (!g_file_test (DPKG_DEB_BINARY, G_FILE_TEST_EXISTS)) { + g_debug ("disabling '%s' as no %s available", + gs_plugin_get_name (plugin), DPKG_DEB_BINARY); + gs_plugin_set_enabled (plugin, FALSE); + } +} + +gboolean +gs_plugin_file_to_app (GsPlugin *plugin, + GsAppList *list, + GFile *file, + GCancellable *cancellable, + GError **error) +{ + guint i; + g_autofree gchar *content_type = NULL; + g_autofree gchar *output = NULL; + g_auto(GStrv) argv = NULL; + g_auto(GStrv) tokens = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GString) str = NULL; + const gchar *mimetypes[] = { + "application/vnd.debian.binary-package", + NULL }; + + /* does this match any of the mimetypes we support */ + content_type = gs_utils_get_content_type (file, cancellable, error); + if (content_type == NULL) + return FALSE; + if (!g_strv_contains (mimetypes, content_type)) + return TRUE; + + /* exec sync */ + argv = g_new0 (gchar *, 5); + argv[0] = g_strdup (DPKG_DEB_BINARY); + argv[1] = g_strdup ("--showformat=${Package}\\n" + "${Version}\\n" + "${Installed-Size}\\n" + "${Homepage}\\n" + "${Description}"); + argv[2] = g_strdup ("-W"); + argv[3] = g_file_get_path (file); + if (!g_spawn_sync (NULL, argv, NULL, + G_SPAWN_SEARCH_PATH | G_SPAWN_STDERR_TO_DEV_NULL, + NULL, NULL, &output, NULL, NULL, error)) { + gs_utils_error_convert_gio (error); + return FALSE; + } + + /* parse output */ + tokens = g_strsplit (output, "\n", 0); + if (g_strv_length (tokens) < 5) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "dpkg-deb output format incorrect:\n\"%s\"\n", output); + return FALSE; + } + + /* create app */ + app = gs_app_new (NULL); + gs_app_set_state (app, AS_APP_STATE_AVAILABLE_LOCAL); + gs_app_add_source (app, tokens[0]); + gs_app_set_name (app, GS_APP_QUALITY_LOWEST, tokens[0]); + gs_app_set_version (app, tokens[1]); + gs_app_set_size_installed (app, 1024 * g_ascii_strtoull (tokens[2], NULL, 10)); + gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, tokens[3]); + gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, tokens[4]); + gs_app_set_kind (app, AS_APP_KIND_GENERIC); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_metadata (app, "GnomeSoftware::Creator", + gs_plugin_get_name (plugin)); + + /* multiline text */ + str = g_string_new (""); + for (i = 5; tokens[i] != NULL; i++) { + if (g_strcmp0 (tokens[i], " .") == 0) { + if (str->len > 0) + g_string_truncate (str, str->len - 1); + g_string_append (str, "\n"); + continue; + } + g_strstrip (tokens[i]); + g_string_append_printf (str, "%s ", tokens[i]); + } + if (str->len > 0) + g_string_truncate (str, str->len - 1); + gs_app_set_description (app, GS_APP_QUALITY_LOWEST, str->str); + + /* success */ + gs_app_list_add (list, app); + return TRUE; +} diff --git a/plugins/dpkg/gs-self-test.c b/plugins/dpkg/gs-self-test.c new file mode 100644 index 0000000..e3a76b2 --- /dev/null +++ b/plugins/dpkg/gs-self-test.c @@ -0,0 +1,97 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2017 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include "gnome-software-private.h" + +#include "gs-test.h" + +static void +gs_plugins_dpkg_func (GsPluginLoader *plugin_loader) +{ + g_autoptr(GsApp) app = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GError) error = NULL; + g_autofree gchar *fn = NULL; + g_autoptr(GFile) file = NULL; + + /* no dpkg, abort */ + if (!gs_plugin_loader_get_enabled (plugin_loader, "dpkg")) { + g_test_skip ("not enabled"); + return; + } + + /* load local file */ + fn = gs_test_get_filename (TESTDATADIR, "chiron-1.1-1.deb"); + g_assert (fn != NULL); + file = g_file_new_for_path (fn); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP, + "file", file, + NULL); + app = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (app != NULL); + g_assert_cmpstr (gs_app_get_source_default (app), ==, "chiron"); + g_assert_cmpstr (gs_app_get_url (app, AS_URL_KIND_HOMEPAGE), ==, "http://127.0.0.1/"); + g_assert_cmpstr (gs_app_get_name (app), ==, "chiron"); + g_assert_cmpstr (gs_app_get_version (app), ==, "1.1-1"); + g_assert_cmpstr (gs_app_get_summary (app), ==, "Single line synopsis"); + g_assert_cmpstr (gs_app_get_description (app), ==, + "This is the first paragraph in the example " + "package control file.\nThis is the second paragraph."); + g_assert (gs_app_get_local_file (app) != NULL); +} + +int +main (int argc, char **argv) +{ + gboolean ret; + g_autoptr(GError) error = NULL; + g_autoptr(GsPluginLoader) plugin_loader = NULL; + const gchar *allowlist[] = { + "dpkg", + NULL + }; + + /* While we use %G_TEST_OPTION_ISOLATE_DIRS to create temporary directories + * for each of the tests, we want to use the system MIME registry, assuming + * that it exists and correctly has shared-mime-info installed. */ +#if GLIB_CHECK_VERSION(2, 60, 0) + g_content_type_set_mime_dirs (NULL); +#endif + + 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); + + /* 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/dpkg", + plugin_loader, + (GTestDataFunc) gs_plugins_dpkg_func); + + return g_test_run (); +} diff --git a/plugins/dpkg/meson.build b/plugins/dpkg/meson.build new file mode 100644 index 0000000..ffae636 --- /dev/null +++ b/plugins/dpkg/meson.build @@ -0,0 +1,41 @@ +cargs = ['-DG_LOG_DOMAIN="GsPluginDpkg"'] + +shared_module( + 'gs_plugin_dpkg', + sources : 'gs-plugin-dpkg.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : plugin_libs, + 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-dpkg', + compiled_schemas, + sources : [ + 'gs-self-test.c' + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + dependencies : [ + plugin_libs, + ], + link_with : [ + libgnomesoftware + ], + c_args : cargs, + ) + test('gs-self-test-dpkg', e, suite: ['plugins', 'dpkg'], env: test_env) +endif diff --git a/plugins/dpkg/tests/build-deb.sh b/plugins/dpkg/tests/build-deb.sh new file mode 100755 index 0000000..cbf50f6 --- /dev/null +++ b/plugins/dpkg/tests/build-deb.sh @@ -0,0 +1 @@ +dpkg-deb --build debian chiron-1.1-1.deb diff --git a/plugins/dpkg/tests/chiron-1.1-1.deb b/plugins/dpkg/tests/chiron-1.1-1.deb Binary files differnew file mode 100644 index 0000000..f4f921a --- /dev/null +++ b/plugins/dpkg/tests/chiron-1.1-1.deb diff --git a/plugins/dpkg/tests/debian/DEBIAN/control b/plugins/dpkg/tests/debian/DEBIAN/control new file mode 100644 index 0000000..ad5d9c6 --- /dev/null +++ b/plugins/dpkg/tests/debian/DEBIAN/control @@ -0,0 +1,13 @@ +Package: chiron +Version: 1.1-1 +Section: base +Priority: optional +Architecture: all +Homepage: http://127.0.0.1/ +Maintainer: Richard Hughes <richard@hughsie.com> +Description: Single line synopsis + This is the first + paragraph in the example package + control file. + . + This is the second paragraph. diff --git a/plugins/dpkg/tests/debian/usr/bin/chiron b/plugins/dpkg/tests/debian/usr/bin/chiron new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/dpkg/tests/debian/usr/bin/chiron diff --git a/plugins/dummy/gs-plugin-dummy.c b/plugins/dummy/gs-plugin-dummy.c new file mode 100644 index 0000000..824baae --- /dev/null +++ b/plugins/dummy/gs-plugin-dummy.c @@ -0,0 +1,938 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2011-2017 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015-2016 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <gnome-software.h> + +/* + * SECTION: + * Provides some dummy data that is useful in self test programs. + */ + +struct GsPluginData { + guint quirk_id; + guint allow_updates_id; + gboolean allow_updates_inhibit; + GsApp *cached_origin; + GHashTable *installed_apps; /* id:1 */ + GHashTable *available_apps; /* id:1 */ +}; + +/* just flip-flop this every few seconds */ +static gboolean +gs_plugin_dummy_allow_updates_cb (gpointer user_data) +{ + GsPlugin *plugin = GS_PLUGIN (user_data); + GsPluginData *priv = gs_plugin_get_data (plugin); + gs_plugin_set_allow_updates (plugin, priv->allow_updates_inhibit); + priv->allow_updates_inhibit = !priv->allow_updates_inhibit; + return G_SOURCE_CONTINUE; +} + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData)); + if (g_getenv ("GS_SELF_TEST_DUMMY_ENABLE") == NULL) { + g_debug ("disabling '%s' as not in self test", + gs_plugin_get_name (plugin)); + gs_plugin_set_enabled (plugin, FALSE); + return; + } + + /* toggle this */ + if (g_getenv ("GS_SELF_TEST_TOGGLE_ALLOW_UPDATES") != NULL) { + priv->allow_updates_id = g_timeout_add_seconds (10, + gs_plugin_dummy_allow_updates_cb, plugin); + } + + /* add source */ + priv->cached_origin = gs_app_new (gs_plugin_get_name (plugin)); + gs_app_set_kind (priv->cached_origin, AS_APP_KIND_SOURCE); + gs_app_set_origin_hostname (priv->cached_origin, "http://www.bbc.co.uk/"); + + /* add the source to the plugin cache which allows us to match the + * unique ID to a GsApp when creating an event */ + gs_plugin_cache_add (plugin, NULL, priv->cached_origin); + + /* keep track of what apps are installed */ + priv->installed_apps = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + priv->available_apps = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + g_hash_table_insert (priv->available_apps, + g_strdup ("chiron.desktop"), + GUINT_TO_POINTER (1)); + g_hash_table_insert (priv->available_apps, + g_strdup ("zeus.desktop"), + GUINT_TO_POINTER (1)); + g_hash_table_insert (priv->available_apps, + g_strdup ("zeus-spell.addon"), + GUINT_TO_POINTER (1)); + g_hash_table_insert (priv->available_apps, + g_strdup ("com.hughski.ColorHug2.driver"), + GUINT_TO_POINTER (1)); + + /* need help from appstream */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "appstream"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "os-release"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_CONFLICTS, "odrs"); +} + +void +gs_plugin_destroy (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + if (priv->installed_apps != NULL) + g_hash_table_unref (priv->installed_apps); + if (priv->available_apps != NULL) + g_hash_table_unref (priv->available_apps); + if (priv->quirk_id > 0) + g_source_remove (priv->quirk_id); + if (priv->cached_origin != NULL) + g_object_unref (priv->cached_origin); +} + +void +gs_plugin_adopt_app (GsPlugin *plugin, GsApp *app) +{ + if (gs_app_get_id (app) != NULL && + g_str_has_prefix (gs_app_get_id (app), "dummy:")) { + gs_app_set_management_plugin (app, gs_plugin_get_name (plugin)); + return; + } + if (g_strcmp0 (gs_app_get_id (app), "mate-spell.desktop") == 0 || + g_strcmp0 (gs_app_get_id (app), "chiron.desktop") == 0 || + g_strcmp0 (gs_app_get_id (app), "zeus.desktop") == 0 || + g_strcmp0 (gs_app_get_id (app), "com.hughski.ColorHug2.driver") == 0 || + g_strcmp0 (gs_app_get_id (app), "zeus-spell.addon") == 0 || + g_strcmp0 (gs_app_get_source_default (app), "chiron") == 0) + gs_app_set_management_plugin (app, gs_plugin_get_name (plugin)); +} + +static gboolean +gs_plugin_dummy_delay (GsPlugin *plugin, + GsApp *app, + guint timeout_ms, + GCancellable *cancellable, + GError **error) +{ + gboolean ret = TRUE; + guint i; + guint timeout_us = timeout_ms * 10; + + /* do blocking delay in 1% increments */ + for (i = 0; i < 100; i++) { + g_usleep (timeout_us); + if (g_cancellable_set_error_if_cancelled (cancellable, error)) { + gs_utils_error_convert_gio (error); + ret = FALSE; + break; + } + if (app != NULL) + gs_app_set_progress (app, i); + gs_plugin_status_update (plugin, app, + GS_PLUGIN_STATUS_DOWNLOADING); + } + return ret; +} + +static gboolean +gs_plugin_dummy_poll_cb (gpointer user_data) +{ + g_autoptr(GsApp) app = NULL; + GsPlugin *plugin = GS_PLUGIN (user_data); + + /* find the app in the per-plugin cache -- this assumes that we can + * calculate the same key as used when calling gs_plugin_cache_add() */ + app = gs_plugin_cache_lookup (plugin, "chiron"); + if (app == NULL) { + g_warning ("app not found in cache!"); + return FALSE; + } + + /* toggle this to animate the hide/show the 3rd party banner */ + if (!gs_app_has_quirk (app, GS_APP_QUIRK_PROVENANCE)) { + g_debug ("about to make app distro-provided"); + gs_app_add_quirk (app, GS_APP_QUIRK_PROVENANCE); + } else { + g_debug ("about to make app 3rd party"); + gs_app_remove_quirk (app, GS_APP_QUIRK_PROVENANCE); + } + + /* continue polling */ + return TRUE; +} + +gboolean +gs_plugin_url_to_app (GsPlugin *plugin, + GsAppList *list, + const gchar *url, + GCancellable *cancellable, + GError **error) +{ + g_autofree gchar *path = NULL; + g_autofree gchar *scheme = NULL; + g_autoptr(GsApp) app = NULL; + + /* not us */ + scheme = gs_utils_get_url_scheme (url); + if (g_strcmp0 (scheme, "dummy") != 0) + return TRUE; + + /* create app */ + path = gs_utils_get_url_path (url); + app = gs_app_new (path); + gs_app_set_management_plugin (app, gs_plugin_get_name (plugin)); + gs_app_set_metadata (app, "GnomeSoftware::Creator", + gs_plugin_get_name (plugin)); + gs_app_list_add (list, app); + return TRUE; +} + +typedef struct { + GMainLoop *loop; + GCancellable *cancellable; + guint timer_id; + gulong cancellable_id; +} GsPluginDummyTimeoutHelper; + +static gboolean +gs_plugin_dummy_timeout_hang_cb (gpointer user_data) +{ + GsPluginDummyTimeoutHelper *helper = (GsPluginDummyTimeoutHelper *) user_data; + helper->timer_id = 0; + g_debug ("timeout hang"); + g_main_loop_quit (helper->loop); + return FALSE; +} + +static void +gs_plugin_dummy_timeout_cancelled_cb (GCancellable *cancellable, gpointer user_data) +{ + GsPluginDummyTimeoutHelper *helper = (GsPluginDummyTimeoutHelper *) user_data; + g_debug ("calling cancel"); + g_main_loop_quit (helper->loop); +} + +static void +gs_plugin_dummy_timeout_helper_free (GsPluginDummyTimeoutHelper *helper) +{ + if (helper->cancellable_id != 0) + g_signal_handler_disconnect (helper->cancellable, helper->cancellable_id); + if (helper->timer_id != 0) + g_source_remove (helper->timer_id); + if (helper->cancellable != NULL) + g_object_unref (helper->cancellable); + g_main_loop_unref (helper->loop); + g_free (helper); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(GsPluginDummyTimeoutHelper, gs_plugin_dummy_timeout_helper_free) + +static void +gs_plugin_dummy_timeout_add (guint timeout_ms, GCancellable *cancellable) +{ + g_autoptr(GsPluginDummyTimeoutHelper) helper = g_new0 (GsPluginDummyTimeoutHelper, 1); + helper->loop = g_main_loop_new (NULL, TRUE); + if (cancellable != NULL) { + helper->cancellable = g_object_ref (cancellable); + helper->cancellable_id = + g_signal_connect (cancellable, "cancelled", + G_CALLBACK (gs_plugin_dummy_timeout_cancelled_cb), + helper); + } + helper->timer_id = g_timeout_add (timeout_ms, + gs_plugin_dummy_timeout_hang_cb, + helper); + g_main_loop_run (helper->loop); +} + +gboolean +gs_plugin_add_alternates (GsPlugin *plugin, + GsApp *app, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + if (g_strcmp0 (gs_app_get_id (app), "zeus.desktop") == 0) { + g_autoptr(GsApp) app2 = gs_app_new ("chiron.desktop"); + gs_app_list_add (list, app2); + } + return TRUE; +} + +gboolean +gs_plugin_add_search (GsPlugin *plugin, + gchar **values, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GsApp) app = NULL; + g_autoptr(AsIcon) ic = NULL; + + /* hang the plugin for 5 seconds */ + if (g_strcmp0 (values[0], "hang") == 0) { + gs_plugin_dummy_timeout_add (5000, cancellable); + if (g_cancellable_set_error_if_cancelled (cancellable, error)) { + gs_utils_error_convert_gio (error); + return FALSE; + } + return TRUE; + } + + /* we're very specific */ + if (g_strcmp0 (values[0], "chiron") != 0) + return TRUE; + + /* does the app already exist? */ + app = gs_plugin_cache_lookup (plugin, "chiron"); + if (app != NULL) { + g_debug ("using %s fom the cache", gs_app_get_id (app)); + gs_app_list_add (list, app); + return TRUE; + } + + /* set up a timeout to emulate getting a GFileMonitor callback */ + priv->quirk_id = + g_timeout_add_seconds (1, gs_plugin_dummy_poll_cb, plugin); + + /* use a generic stock icon */ + ic = as_icon_new (); + as_icon_set_kind (ic, AS_ICON_KIND_STOCK); + as_icon_set_name (ic, "drive-harddisk"); + + /* add a live updatable normal application */ + app = gs_app_new ("chiron.desktop"); + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, "Chiron"); + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, "A teaching application"); + gs_app_add_icon (app, ic); + gs_app_set_size_installed (app, 42 * 1024 * 1024); + gs_app_set_size_download (app, 50 * 1024 * 1024); + gs_app_set_kind (app, AS_APP_KIND_DESKTOP); + gs_app_set_state (app, AS_APP_STATE_INSTALLED); + gs_app_set_management_plugin (app, gs_plugin_get_name (plugin)); + gs_app_set_metadata (app, "GnomeSoftware::Creator", + gs_plugin_get_name (plugin)); + gs_app_list_add (list, app); + + /* add to cache so it can be found by the flashing callback */ + gs_plugin_cache_add (plugin, NULL, app); + + return TRUE; +} + +gboolean +gs_plugin_add_updates (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsApp *app; + GsApp *proxy; + g_autoptr(AsIcon) ic = NULL; + + /* update UI as this might take some time */ + gs_plugin_status_update (plugin, NULL, GS_PLUGIN_STATUS_WAITING); + + /* spin */ + if (!gs_plugin_dummy_delay (plugin, NULL, 2000, cancellable, error)) + return FALSE; + + /* use a generic stock icon */ + ic = as_icon_new (); + as_icon_set_kind (ic, AS_ICON_KIND_STOCK); + as_icon_set_name (ic, "drive-harddisk"); + + /* add a live updatable normal application */ + app = gs_app_new ("chiron.desktop"); + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, "Chiron"); + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, "A teaching application"); + gs_app_set_update_details (app, "Do not crash when using libvirt."); + gs_app_set_update_urgency (app, AS_URGENCY_KIND_HIGH); + gs_app_add_icon (app, ic); + gs_app_set_kind (app, AS_APP_KIND_DESKTOP); + gs_app_set_state (app, AS_APP_STATE_UPDATABLE_LIVE); + gs_app_set_management_plugin (app, gs_plugin_get_name (plugin)); + gs_app_list_add (list, app); + g_object_unref (app); + + /* add a offline OS update */ + app = gs_app_new (NULL); + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, "libvirt-glib-devel"); + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, "Development files for libvirt"); + gs_app_set_update_details (app, "Fix several memory leaks."); + gs_app_set_update_urgency (app, AS_URGENCY_KIND_LOW); + gs_app_set_kind (app, AS_APP_KIND_GENERIC); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_scope (app, AS_APP_SCOPE_SYSTEM); + gs_app_set_state (app, AS_APP_STATE_UPDATABLE); + gs_app_add_source (app, "libvirt-glib-devel"); + gs_app_add_source_id (app, "libvirt-glib-devel;0.0.1;noarch;fedora"); + gs_app_set_management_plugin (app, gs_plugin_get_name (plugin)); + gs_app_list_add (list, app); + g_object_unref (app); + + /* add a live OS update */ + app = gs_app_new (NULL); + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, "chiron-libs"); + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, "library for chiron"); + gs_app_set_update_details (app, "Do not crash when using libvirt."); + gs_app_set_update_urgency (app, AS_URGENCY_KIND_HIGH); + gs_app_set_kind (app, AS_APP_KIND_GENERIC); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_scope (app, AS_APP_SCOPE_SYSTEM); + gs_app_set_state (app, AS_APP_STATE_UPDATABLE_LIVE); + gs_app_add_source (app, "chiron-libs"); + gs_app_add_source_id (app, "chiron-libs;0.0.1;i386;updates-testing"); + gs_app_set_management_plugin (app, gs_plugin_get_name (plugin)); + gs_app_list_add (list, app); + g_object_unref (app); + + /* add a proxy app update */ + proxy = gs_app_new ("proxy.desktop"); + gs_app_set_name (proxy, GS_APP_QUALITY_NORMAL, "Proxy"); + gs_app_set_summary (proxy, GS_APP_QUALITY_NORMAL, "A proxy app"); + gs_app_set_update_details (proxy, "Update all related apps."); + gs_app_set_update_urgency (proxy, AS_URGENCY_KIND_HIGH); + gs_app_add_icon (proxy, ic); + gs_app_set_kind (proxy, AS_APP_KIND_DESKTOP); + gs_app_add_quirk (proxy, GS_APP_QUIRK_IS_PROXY); + gs_app_set_state (proxy, AS_APP_STATE_UPDATABLE_LIVE); + gs_app_set_management_plugin (proxy, gs_plugin_get_name (plugin)); + gs_app_list_add (list, proxy); + g_object_unref (proxy); + + /* add a proxy related app */ + app = gs_app_new ("proxy-related-app.desktop"); + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, "Related app"); + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, "A related app"); + gs_app_set_kind (app, AS_APP_KIND_DESKTOP); + gs_app_set_state (app, AS_APP_STATE_UPDATABLE_LIVE); + gs_app_set_management_plugin (app, gs_plugin_get_name (plugin)); + gs_app_add_related (proxy, app); + g_object_unref (app); + + /* add another proxy related app */ + app = gs_app_new ("proxy-another-related-app.desktop"); + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, "Another Related app"); + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, "A related app"); + gs_app_set_kind (app, AS_APP_KIND_DESKTOP); + gs_app_set_state (app, AS_APP_STATE_UPDATABLE_LIVE); + gs_app_set_management_plugin (app, gs_plugin_get_name (plugin)); + gs_app_add_related (proxy, app); + g_object_unref (app); + + return TRUE; +} + +gboolean +gs_plugin_add_installed (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + const gchar *packages[] = { "zeus", "zeus-common", NULL }; + const gchar *app_ids[] = { "Uninstall Zeus.desktop", NULL }; + guint i; + + /* add all packages */ + for (i = 0; packages[i] != NULL; i++) { + g_autoptr(GsApp) app = gs_app_new (NULL); + gs_app_add_source (app, packages[i]); + gs_app_set_state (app, AS_APP_STATE_INSTALLED); + gs_app_set_kind (app, AS_APP_KIND_GENERIC); + gs_app_set_origin (app, "london-west"); + gs_app_set_management_plugin (app, gs_plugin_get_name (plugin)); + gs_app_list_add (list, app); + } + + /* add all app-ids */ + for (i = 0; app_ids[i] != NULL; i++) { + g_autoptr(GsApp) app = gs_app_new (app_ids[i]); + gs_app_set_state (app, AS_APP_STATE_INSTALLED); + gs_app_set_kind (app, AS_APP_KIND_DESKTOP); + gs_app_set_management_plugin (app, gs_plugin_get_name (plugin)); + gs_app_list_add (list, app); + } + + return TRUE; +} + +gboolean +gs_plugin_add_popular (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GsApp) app1 = NULL; + g_autoptr(GsApp) app2 = NULL; + + /* add wildcard */ + app1 = gs_app_new ("zeus.desktop"); + gs_app_add_quirk (app1, GS_APP_QUIRK_IS_WILDCARD); + gs_app_set_metadata (app1, "GnomeSoftware::Creator", + gs_plugin_get_name (plugin)); + gs_app_list_add (list, app1); + + /* add again, this time with a prefix so it gets deduplicated */ + app2 = gs_app_new ("zeus.desktop"); + gs_app_set_scope (app2, AS_APP_SCOPE_USER); + gs_app_set_bundle_kind (app2, AS_BUNDLE_KIND_SNAP); + gs_app_set_metadata (app2, "GnomeSoftware::Creator", + gs_plugin_get_name (plugin)); + gs_app_list_add (list, app2); + return TRUE; +} + +gboolean +gs_plugin_app_remove (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + + /* only process this app if was created by this plugin */ + if (g_strcmp0 (gs_app_get_management_plugin (app), + gs_plugin_get_name (plugin)) != 0) + return TRUE; + + /* remove app */ + if (g_strcmp0 (gs_app_get_id (app), "chiron.desktop") == 0) { + gs_app_set_state (app, AS_APP_STATE_REMOVING); + if (!gs_plugin_dummy_delay (plugin, app, 500, cancellable, error)) { + gs_app_set_state_recover (app); + return FALSE; + } + gs_app_set_state (app, AS_APP_STATE_UNKNOWN); + } + + /* keep track */ + g_hash_table_remove (priv->installed_apps, gs_app_get_id (app)); + g_hash_table_insert (priv->available_apps, + g_strdup (gs_app_get_id (app)), + GUINT_TO_POINTER (1)); + return TRUE; +} + +gboolean +gs_plugin_app_install (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + + /* only process this app if was created by this plugin */ + if (g_strcmp0 (gs_app_get_management_plugin (app), + gs_plugin_get_name (plugin)) != 0) + return TRUE; + + /* install app */ + if (g_strcmp0 (gs_app_get_id (app), "chiron.desktop") == 0 || + g_strcmp0 (gs_app_get_id (app), "zeus.desktop") == 0) { + gs_app_set_state (app, AS_APP_STATE_INSTALLING); + if (!gs_plugin_dummy_delay (plugin, app, 500, cancellable, error)) { + gs_app_set_state_recover (app); + return FALSE; + } + gs_app_set_state (app, AS_APP_STATE_INSTALLED); + } + + /* keep track */ + g_hash_table_insert (priv->installed_apps, + g_strdup (gs_app_get_id (app)), + GUINT_TO_POINTER (1)); + g_hash_table_remove (priv->available_apps, gs_app_get_id (app)); + + return TRUE; +} + +gboolean +gs_plugin_update_app (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + + /* only process this app if was created by this plugin */ + if (g_strcmp0 (gs_app_get_management_plugin (app), + gs_plugin_get_name (plugin)) != 0) + return TRUE; + + if (!g_str_has_prefix (gs_app_get_id (app), "proxy")) { + /* always fail */ + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_DOWNLOAD_FAILED, + "no network connection is available"); + gs_utils_error_add_origin_id (error, priv->cached_origin); + return FALSE; + } + + /* simulate an update for 4 seconds */ + gs_app_set_state (app, AS_APP_STATE_INSTALLING); + for (guint i = 1; i <= 4; ++i) { + gs_app_set_progress (app, 25 * i); + sleep (1); + } + gs_app_set_state (app, AS_APP_STATE_INSTALLED); + + return TRUE; +} + +static gboolean +refine_app (GsPlugin *plugin, + GsApp *app, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + + /* make the local system EOL */ + if (gs_app_get_metadata_item (app, "GnomeSoftware::CpeName") != NULL) + gs_app_set_state (app, AS_APP_STATE_UNAVAILABLE); + + /* state */ + if (gs_app_get_state (app) == AS_APP_STATE_UNKNOWN) { + if (g_hash_table_lookup (priv->installed_apps, + gs_app_get_id (app)) != NULL) + gs_app_set_state (app, AS_APP_STATE_INSTALLED); + if (g_hash_table_lookup (priv->available_apps, + gs_app_get_id (app)) != NULL) + gs_app_set_state (app, AS_APP_STATE_AVAILABLE); + } + + /* kind */ + if (g_strcmp0 (gs_app_get_id (app), "chiron.desktop") == 0 || + g_strcmp0 (gs_app_get_id (app), "mate-spell.desktop") == 0 || + g_strcmp0 (gs_app_get_id (app), "com.hughski.ColorHug2.driver") == 0 || + g_strcmp0 (gs_app_get_id (app), "zeus.desktop") == 0) { + if (gs_app_get_kind (app) == AS_APP_KIND_UNKNOWN) + gs_app_set_kind (app, AS_APP_KIND_DESKTOP); + } + + /* license */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE) { + if (g_strcmp0 (gs_app_get_id (app), "chiron.desktop") == 0 || + g_strcmp0 (gs_app_get_id (app), "zeus.desktop") == 0) + gs_app_set_license (app, GS_APP_QUALITY_HIGHEST, "GPL-2.0+"); + } + + /* homepage */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL) { + if (g_strcmp0 (gs_app_get_id (app), "chiron.desktop") == 0) { + gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, + "http://www.test.org/"); + } + } + + /* origin */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN) { + if (g_strcmp0 (gs_app_get_id (app), "zeus-spell.addon") == 0) + gs_app_set_origin (app, "london-east"); + } + + /* default */ + if (g_strcmp0 (gs_app_get_id (app), "chiron.desktop") == 0) { + if (gs_app_get_name (app) == NULL) + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, "tmp"); + if (gs_app_get_summary (app) == NULL) + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, "tmp"); + if (gs_app_get_icons(app)->len == 0) { + g_autoptr(AsIcon) ic = NULL; + ic = as_icon_new (); + as_icon_set_kind (ic, AS_ICON_KIND_STOCK); + as_icon_set_name (ic, "drive-harddisk"); + gs_app_add_icon (app, ic); + } + } + + /* description */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION) { + if (g_strcmp0 (gs_app_get_id (app), "chiron.desktop") == 0) { + gs_app_set_description (app, GS_APP_QUALITY_NORMAL, + "long description!"); + } + } + + /* add fake review */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS) { + g_autoptr(AsReview) review1 = NULL; + g_autoptr(AsReview) review2 = NULL; + g_autoptr(GDateTime) dt = NULL; + + dt = g_date_time_new_now_utc (); + + /* set first review */ + review1 = as_review_new (); + as_review_set_rating (review1, 50); + as_review_set_reviewer_name (review1, "Angela Avery"); + as_review_set_summary (review1, "Steep learning curve, but worth it"); + as_review_set_description (review1, "Best overall 3D application I've ever used overall 3D application I've ever used. Best overall 3D application I've ever used overall 3D application I've ever used. Best overall 3D application I've ever used overall 3D application I've ever used. Best overall 3D application I've ever used overall 3D application I've ever used."); + as_review_set_version (review1, "3.16.4"); + as_review_set_date (review1, dt); + gs_app_add_review (app, review1); + + /* set self review */ + review2 = as_review_new (); + as_review_set_rating (review2, 100); + as_review_set_reviewer_name (review2, "Just Myself"); + as_review_set_summary (review2, "I like this application"); + as_review_set_description (review2, "I'm not very wordy myself."); + as_review_set_version (review2, "3.16.3"); + as_review_set_date (review2, dt); + as_review_set_flags (review2, AS_REVIEW_FLAG_SELF); + gs_app_add_review (app, review2); + } + + /* add fake ratings */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEW_RATINGS) { + g_autoptr(GArray) ratings = NULL; + const gint data[] = { 0, 10, 20, 30, 15, 2 }; + ratings = g_array_sized_new (FALSE, FALSE, sizeof (gint), 6); + g_array_append_vals (ratings, data, 6); + gs_app_set_review_ratings (app, ratings); + } + + /* add a rating */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING) { + gs_app_set_rating (app, 66); + } + + return TRUE; +} + +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; +} + +gboolean +gs_plugin_add_category_apps (GsPlugin *plugin, + GsCategory *category, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GsApp) app = gs_app_new ("chiron.desktop"); + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, "Chiron"); + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, "View and use virtual machines"); + gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, "http://www.box.org"); + gs_app_set_kind (app, AS_APP_KIND_DESKTOP); + gs_app_set_state (app, AS_APP_STATE_AVAILABLE); + gs_app_set_pixbuf (app, gdk_pixbuf_new_from_file ("/usr/share/icons/hicolor/48x48/apps/chiron.desktop.png", NULL)); + gs_app_set_kind (app, AS_APP_KIND_DESKTOP); + gs_app_set_management_plugin (app, gs_plugin_get_name (plugin)); + gs_app_list_add (list, app); + return TRUE; +} + +gboolean +gs_plugin_add_recent (GsPlugin *plugin, + GsAppList *list, + guint64 age, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GsApp) app = gs_app_new ("chiron.desktop"); + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, "Chiron"); + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, "View and use virtual machines"); + gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, "http://www.box.org"); + gs_app_set_kind (app, AS_APP_KIND_DESKTOP); + gs_app_set_state (app, AS_APP_STATE_AVAILABLE); + gs_app_set_pixbuf (app, gdk_pixbuf_new_from_file ("/usr/share/icons/hicolor/48x48/apps/chiron.desktop.png", NULL)); + gs_app_set_kind (app, AS_APP_KIND_DESKTOP); + gs_app_set_management_plugin (app, gs_plugin_get_name (plugin)); + gs_app_list_add (list, app); + return TRUE; +} + +gboolean +gs_plugin_add_distro_upgrades (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GsApp) app = NULL; + g_autoptr(AsIcon) ic = NULL; + + /* use stock icon */ + ic = as_icon_new (); + as_icon_set_kind (ic, AS_ICON_KIND_STOCK); + as_icon_set_name (ic, "application-x-addon"); + + /* get existing item from the cache */ + app = gs_plugin_cache_lookup (plugin, "user/*/*/os-upgrade/org.fedoraproject.release-rawhide.upgrade/*"); + if (app != NULL) { + gs_app_list_add (list, app); + return TRUE; + } + + app = gs_app_new ("org.fedoraproject.release-rawhide.upgrade"); + gs_app_set_scope (app, AS_APP_SCOPE_USER); + gs_app_set_kind (app, AS_APP_KIND_OS_UPGRADE); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_state (app, AS_APP_STATE_AVAILABLE); + gs_app_set_name (app, GS_APP_QUALITY_LOWEST, "Fedora"); + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, + "A major upgrade, with new features and added polish."); + gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, + "https://fedoraproject.org/wiki/Releases/24/Schedule"); + gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT); + gs_app_add_quirk (app, GS_APP_QUIRK_PROVENANCE); + gs_app_add_quirk (app, GS_APP_QUIRK_NOT_REVIEWABLE); + gs_app_set_version (app, "25"); + gs_app_set_size_installed (app, 256 * 1024 * 1024); + gs_app_set_size_download (app, 1024 * 1024 * 1024); + gs_app_set_license (app, GS_APP_QUALITY_LOWEST, "LicenseRef-free"); + gs_app_set_management_plugin (app, gs_plugin_get_name (plugin)); + gs_app_set_metadata (app, "GnomeSoftware::UpgradeBanner-css", + "background: url('" DATADIR "/gnome-software/upgrade-bg.png');" + "background-size: 100% 100%;"); + gs_app_add_icon (app, ic); + gs_app_list_add (list, app); + + gs_plugin_cache_add (plugin, NULL, app); + + return TRUE; +} + +gboolean +gs_plugin_download_app (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + return gs_plugin_dummy_delay (plugin, app, 5100, cancellable, error); +} + +gboolean +gs_plugin_refresh (GsPlugin *plugin, + guint cache_age, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GsApp) app = gs_app_new (NULL); + return gs_plugin_dummy_delay (plugin, app, 3100, cancellable, error); +} + +gboolean +gs_plugin_app_upgrade_download (GsPlugin *plugin, GsApp *app, + GCancellable *cancellable, GError **error) +{ + /* only process this app if was created by this plugin */ + if (g_strcmp0 (gs_app_get_management_plugin (app), + gs_plugin_get_name (plugin)) != 0) + return TRUE; + + g_debug ("starting download"); + gs_app_set_state (app, AS_APP_STATE_INSTALLING); + if (!gs_plugin_dummy_delay (plugin, app, 5000, cancellable, error)) { + gs_app_set_state_recover (app); + return FALSE; + } + gs_app_set_state (app, AS_APP_STATE_UPDATABLE); + return TRUE; +} + +gboolean +gs_plugin_app_upgrade_trigger (GsPlugin *plugin, GsApp *app, + GCancellable *cancellable, GError **error) +{ + /* only process this app if was created by this plugin */ + if (g_strcmp0 (gs_app_get_management_plugin (app), + gs_plugin_get_name (plugin)) != 0) + return TRUE; + + /* NOP */ + return TRUE; +} + +gboolean +gs_plugin_update_cancel (GsPlugin *plugin, GsApp *app, + GCancellable *cancellable, GError **error) +{ + return TRUE; +} + +gboolean +gs_plugin_review_submit (GsPlugin *plugin, + GsApp *app, + AsReview *review, + GCancellable *cancellable, + GError **error) +{ + g_debug ("Submitting dummy review"); + return TRUE; +} + +gboolean +gs_plugin_review_report (GsPlugin *plugin, + GsApp *app, + AsReview *review, + GCancellable *cancellable, + GError **error) +{ + g_debug ("Reporting dummy review"); + as_review_add_flags (review, AS_REVIEW_FLAG_VOTED); + return TRUE; +} + +gboolean +gs_plugin_review_upvote (GsPlugin *plugin, + GsApp *app, + AsReview *review, + GCancellable *cancellable, + GError **error) +{ + g_debug ("Upvoting dummy review"); + as_review_add_flags (review, AS_REVIEW_FLAG_VOTED); + return TRUE; +} + +gboolean +gs_plugin_review_downvote (GsPlugin *plugin, + GsApp *app, + AsReview *review, + GCancellable *cancellable, + GError **error) +{ + g_debug ("Downvoting dummy review"); + as_review_add_flags (review, AS_REVIEW_FLAG_VOTED); + return TRUE; +} + +gboolean +gs_plugin_review_remove (GsPlugin *plugin, + GsApp *app, + AsReview *review, + GCancellable *cancellable, + GError **error) +{ + /* all okay */ + g_debug ("Removing dummy self-review"); + return TRUE; +} diff --git a/plugins/dummy/gs-self-test.c b/plugins/dummy/gs-self-test.c new file mode 100644 index 0000000..30360cc --- /dev/null +++ b/plugins/dummy/gs-self-test.c @@ -0,0 +1,898 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2017 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gstdio.h> + +#include "gnome-software-private.h" + +#include "gs-test.h" + +static guint _status_changed_cnt = 0; + +typedef struct { + GError *error; + GMainLoop *loop; +} GsDummyTestHelper; + +static GsDummyTestHelper * +gs_dummy_test_helper_new (void) +{ + return g_new0 (GsDummyTestHelper, 1); +} + +static void +gs_dummy_test_helper_free (GsDummyTestHelper *helper) +{ + if (helper->error != NULL) + g_error_free (helper->error); + if (helper->loop != NULL) + g_main_loop_unref (helper->loop); + g_free (helper); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(GsDummyTestHelper, gs_dummy_test_helper_free) + +static void +gs_plugin_loader_status_changed_cb (GsPluginLoader *plugin_loader, + GsApp *app, + GsPluginStatus status, + gpointer user_data) +{ + _status_changed_cnt++; +} + +static void +gs_plugins_dummy_install_func (GsPluginLoader *plugin_loader) +{ + gboolean ret; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GError) error = NULL; + + /* install */ + app = gs_app_new ("chiron.desktop"); + gs_app_set_management_plugin (app, "dummy"); + gs_app_set_state (app, AS_APP_STATE_AVAILABLE); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (ret); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_INSTALLED); + + /* remove */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (ret); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_AVAILABLE); +} + +static void +gs_plugins_dummy_error_func (GsPluginLoader *plugin_loader) +{ + const GError *app_error; + gboolean ret; + g_autoptr(GError) error = NULL; + g_autoptr(GPtrArray) events = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsPluginEvent) event = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* drop all caches */ + gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL); + gs_plugin_loader_setup_again (plugin_loader); + + /* update, which should cause an error to be emitted */ + app = gs_app_new ("chiron.desktop"); + gs_app_set_management_plugin (app, "dummy"); + gs_app_set_state (app, AS_APP_STATE_AVAILABLE); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPDATE, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (ret); + + /* get last active event */ + event = gs_plugin_loader_get_event_default (plugin_loader); + g_assert (event != NULL); + g_assert (gs_plugin_event_get_app (event) == app); + + /* check all the events */ + events = gs_plugin_loader_get_events (plugin_loader); + g_assert_cmpint (events->len, ==, 1); + event = g_ptr_array_index (events, 0); + g_assert (gs_plugin_event_get_app (event) == app); + app_error = gs_plugin_event_get_error (event); + g_assert (app_error != NULL); + g_assert_error (app_error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_DOWNLOAD_FAILED); +} + +static void +gs_plugins_dummy_refine_func (GsPluginLoader *plugin_loader) +{ + gboolean ret; + g_autoptr(GsApp) app = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* get the extra bits */ + app = gs_app_new ("chiron.desktop"); + gs_app_set_management_plugin (app, "dummy"); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFINE, + "app", app, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (ret); + + g_assert_cmpstr (gs_app_get_license (app), ==, "GPL-2.0+"); + g_assert_cmpstr (gs_app_get_description (app), !=, NULL); + g_assert_cmpstr (gs_app_get_url (app, AS_URL_KIND_HOMEPAGE), ==, "http://www.test.org/"); +} + +static void +gs_plugins_dummy_metadata_quirks (GsPluginLoader *plugin_loader) +{ + gboolean ret; + g_autoptr(GsApp) app = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* get the extra bits */ + app = gs_app_new ("chiron.desktop"); + gs_app_set_management_plugin (app, "dummy"); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFINE, + "app", app, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (ret); + + g_assert_cmpstr (gs_app_get_description (app), !=, NULL); + + /* check the not-launchable quirk */ + + g_assert (!gs_app_has_quirk(app, GS_APP_QUIRK_NOT_LAUNCHABLE)); + + gs_app_set_metadata (app, "GnomeSoftware::quirks::not-launchable", "true"); + + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFINE, + "app", app, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (ret); + + g_assert (gs_app_has_quirk(app, GS_APP_QUIRK_NOT_LAUNCHABLE)); + + gs_app_set_metadata (app, "GnomeSoftware::quirks::not-launchable", NULL); + gs_app_set_metadata (app, "GnomeSoftware::quirks::not-launchable", "false"); + + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFINE, + "app", app, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (ret); + + g_assert (!gs_app_has_quirk(app, GS_APP_QUIRK_NOT_LAUNCHABLE)); +} + +static void +gs_plugins_dummy_key_colors_func (GsPluginLoader *plugin_loader) +{ + GPtrArray *array; + gboolean ret; + guint i; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GError) error = NULL; + + /* get the extra bits */ + app = gs_app_new ("zeus.desktop"); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFINE, + "app", app, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_KEY_COLORS, + 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); + array = gs_app_get_key_colors (app); + g_assert_cmpint (array->len, >=, 3); + + /* check values are in range */ + for (i = 0; i < array->len; i++) { + GdkRGBA *kc = g_ptr_array_index (array, i); + g_assert_cmpfloat (kc->red, >=, 0.f); + g_assert_cmpfloat (kc->red, <=, 1.f); + g_assert_cmpfloat (kc->green, >=, 0.f); + g_assert_cmpfloat (kc->green, <=, 1.f); + g_assert_cmpfloat (kc->blue, >=, 0.f); + g_assert_cmpfloat (kc->blue, <=, 1.f); + g_assert_cmpfloat (kc->alpha, >=, 0.f); + g_assert_cmpfloat (kc->alpha, <=, 1.f); + } +} + +static void +gs_plugins_dummy_updates_func (GsPluginLoader *plugin_loader) +{ + GsApp *app; + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* get the updates list */ + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_UPDATES, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS, + NULL); + list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (list != NULL); + + /* make sure there are three entries */ + g_assert_cmpint (gs_app_list_length (list), ==, 3); + app = gs_app_list_index (list, 0); + g_assert_cmpstr (gs_app_get_id (app), ==, "chiron.desktop"); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_APP_KIND_DESKTOP); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_UPDATABLE_LIVE); + g_assert_cmpstr (gs_app_get_update_details (app), ==, "Do not crash when using libvirt."); + g_assert_cmpint (gs_app_get_update_urgency (app), ==, AS_URGENCY_KIND_HIGH); + + /* get the virtual non-apps OS update */ + app = gs_app_list_index (list, 2); + g_assert_cmpstr (gs_app_get_id (app), ==, "org.gnome.Software.OsUpdate"); + g_assert_cmpstr (gs_app_get_name (app), ==, "OS Updates"); + g_assert_cmpstr (gs_app_get_summary (app), ==, "Includes performance, stability and security improvements."); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_APP_KIND_OS_UPDATE); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_UPDATABLE); + g_assert_cmpint (gs_app_list_length (gs_app_get_related (app)), ==, 2); + + /* get the virtual non-apps OS update */ + app = gs_app_list_index (list, 1); + g_assert_cmpstr (gs_app_get_id (app), ==, "proxy.desktop"); + g_assert (gs_app_has_quirk (app, GS_APP_QUIRK_IS_PROXY)); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_UPDATABLE_LIVE); + g_assert_cmpint (gs_app_list_length (gs_app_get_related (app)), ==, 2); +} + +static void +gs_plugins_dummy_distro_upgrades_func (GsPluginLoader *plugin_loader) +{ + GsApp *app; + gboolean ret; + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* get the updates list */ + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_DISTRO_UPDATES, NULL); + list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (list != NULL); + + /* make sure there is one entry */ + g_assert_cmpint (gs_app_list_length (list), ==, 1); + app = gs_app_list_index (list, 0); + g_assert_cmpstr (gs_app_get_id (app), ==, "org.fedoraproject.release-rawhide.upgrade"); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_APP_KIND_OS_UPGRADE); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_AVAILABLE); + + /* this should be set with a higher priority by AppStream */ + g_assert_cmpstr (gs_app_get_summary (app), ==, "Release specific tagline"); + + /* download the update */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (ret); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_UPDATABLE); + + /* trigger the update */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPGRADE_TRIGGER, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (ret); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_UPDATABLE); +} + +static void +gs_plugins_dummy_installed_func (GsPluginLoader *plugin_loader) +{ + GsApp *app; + GsApp *addon; + GsAppList *addons; + g_autofree gchar *menu_path = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* get installed packages */ + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_INSTALLED, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_MENU_PATH | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_CATEGORIES | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE, + NULL); + list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (list != NULL); + + /* make sure there is one entry */ + g_assert_cmpint (gs_app_list_length (list), ==, 1); + app = gs_app_list_index (list, 0); + g_assert_cmpstr (gs_app_get_id (app), ==, "zeus.desktop"); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_APP_KIND_DESKTOP); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_INSTALLED); + g_assert_cmpstr (gs_app_get_name (app), ==, "Zeus"); + g_assert_cmpstr (gs_app_get_source_default (app), ==, "zeus"); + g_assert (gs_app_get_pixbuf (app) != NULL); + + /* check various bitfields */ + g_assert (gs_app_has_quirk (app, GS_APP_QUIRK_PROVENANCE)); + g_assert_cmpstr (gs_app_get_license (app), ==, "GPL-2.0+"); + g_assert (gs_app_get_license_is_free (app)); + + /* check kudos */ + g_assert_true (gs_app_has_kudo (app, GS_APP_KUDO_MY_LANGUAGE)); + + /* check categories */ + g_assert (gs_app_has_category (app, "Player")); + g_assert (gs_app_has_category (app, "AudioVideo")); + g_assert (!gs_app_has_category (app, "ImageProcessing")); + g_assert (gs_app_get_menu_path (app) != NULL); + menu_path = g_strjoinv ("->", gs_app_get_menu_path (app)); + g_assert_cmpstr (menu_path, ==, "Audio & Video->Music Players"); + + /* check addon */ + addons = gs_app_get_addons (app); + g_assert_cmpint (gs_app_list_length (addons), ==, 1); + addon = gs_app_list_index (addons, 0); + g_assert_cmpstr (gs_app_get_id (addon), ==, "zeus-spell.addon"); + g_assert_cmpint (gs_app_get_kind (addon), ==, AS_APP_KIND_ADDON); + g_assert_cmpint (gs_app_get_state (addon), ==, AS_APP_STATE_AVAILABLE); + g_assert_cmpstr (gs_app_get_name (addon), ==, "Spell Check"); + g_assert_cmpstr (gs_app_get_source_default (addon), ==, "zeus-spell"); + g_assert_cmpstr (gs_app_get_license (addon), ==, + "LicenseRef-free=https://www.debian.org/"); + g_assert (gs_app_get_pixbuf (addon) == NULL); +} + +static void +gs_plugins_dummy_search_func (GsPluginLoader *plugin_loader) +{ + GsApp *app; + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* get search result based on addon keyword */ + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH, + "search", "zeus", + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + NULL); + list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (list != NULL); + + /* make sure there 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), ==, "zeus.desktop"); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_APP_KIND_DESKTOP); +} + +static void +gs_plugins_dummy_search_alternate_func (GsPluginLoader *plugin_loader) +{ + GsApp *app_tmp; + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* get search result based on addon keyword */ + app = gs_app_new ("zeus.desktop"); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_ALTERNATES, + "app", app, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + NULL); + list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (list != NULL); + + /* make sure there is the original app, and the alternate */ + g_assert_cmpint (gs_app_list_length (list), ==, 2); + app_tmp = gs_app_list_index (list, 0); + g_assert_cmpstr (gs_app_get_id (app_tmp), ==, "chiron.desktop"); + g_assert_cmpint (gs_app_get_kind (app_tmp), ==, AS_APP_KIND_DESKTOP); + app_tmp = gs_app_list_index (list, 1); + g_assert_cmpstr (gs_app_get_id (app_tmp), ==, "zeus.desktop"); + g_assert_cmpint (gs_app_get_kind (app_tmp), ==, AS_APP_KIND_DESKTOP); +} + +static void +gs_plugins_dummy_hang_func (GsPluginLoader *plugin_loader) +{ + g_autoptr(GCancellable) cancellable = g_cancellable_new (); + g_autoptr(GError) error = 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); + + /* get search result based on addon keyword */ + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH, + "search", "hang", + "timeout", 1, /* seconds */ + NULL); + list = gs_plugin_loader_job_process (plugin_loader, plugin_job, cancellable, &error); + gs_test_flush_main_context (); + g_assert_error (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_TIMED_OUT); + g_assert (list == NULL); +} + +static void +gs_plugins_dummy_search_invalid_func (GsPluginLoader *plugin_loader) +{ + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* get search result based on addon keyword */ + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH, + "search", "X", + NULL); + list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_error (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NOT_SUPPORTED); + g_assert (list == NULL); +} + +static void +gs_plugins_dummy_url_to_app_func (GsPluginLoader *plugin_loader) +{ + g_autoptr(GError) error = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_URL_TO_APP, + "search", "dummy://chiron.desktop", + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + NULL); + app = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (app != NULL); + g_assert_cmpstr (gs_app_get_id (app), ==, "chiron.desktop"); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_APP_KIND_DESKTOP); +} + +static void +gs_plugins_dummy_plugin_cache_func (GsPluginLoader *plugin_loader) +{ + GsApp *app1; + GsApp *app2; + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list1 = NULL; + g_autoptr(GsAppList) list2 = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* ensure we get the same results back from calling the methods twice */ + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_DISTRO_UPDATES, NULL); + list1 = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (list1 != NULL); + g_assert_cmpint (gs_app_list_length (list1), ==, 1); + app1 = gs_app_list_index (list1, 0); + + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_DISTRO_UPDATES, NULL); + list2 = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (list2 != NULL); + g_assert_cmpint (gs_app_list_length (list2), ==, 1); + app2 = gs_app_list_index (list2, 0); + + /* make sure there is one GObject */ + g_assert_cmpstr (gs_app_get_id (app1), ==, gs_app_get_id (app2)); + g_assert (app1 == app2); +} + +static void +gs_plugins_dummy_wildcard_func (GsPluginLoader *plugin_loader) +{ + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list1 = NULL; + g_autoptr(GsAppList) list2 = NULL; + const gchar *popular_override = "chiron.desktop,zeus.desktop"; + g_auto(GStrv) apps = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* use the plugin's add_popular function */ + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_POPULAR, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + NULL); + list1 = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (list1 != NULL); + g_assert_cmpint (gs_app_list_length (list1), ==, 1); + + /* override the popular list (do not use the add_popular function) */ + g_setenv ("GNOME_SOFTWARE_POPULAR", popular_override, TRUE); + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_POPULAR, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + NULL); + list2 = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (list2 != NULL); + + apps = g_strsplit (popular_override, ",", 0); + g_assert_cmpint (gs_app_list_length (list2), ==, g_strv_length (apps)); + + for (guint i = 0; i < gs_app_list_length (list2); ++i) { + GsApp *app = gs_app_list_index (list2, i); + g_assert (g_strv_contains ((const gchar * const *) apps, gs_app_get_id (app))); + } +} + +static void +plugin_job_action_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source); + GsDummyTestHelper *helper = (GsDummyTestHelper *) user_data; + + gs_plugin_loader_job_action_finish (plugin_loader, res, &helper->error); + if (helper->loop != NULL) + g_main_loop_quit (helper->loop); +} + +static void +gs_plugins_dummy_limit_parallel_ops_func (GsPluginLoader *plugin_loader) +{ + g_autoptr(GsAppList) list = NULL; + GsApp *app1 = NULL; + g_autoptr(GsApp) app2 = NULL; + g_autoptr(GsApp) app3 = NULL; + g_autoptr(GsPluginJob) plugin_job1 = NULL; + g_autoptr(GsPluginJob) plugin_job2 = NULL; + g_autoptr(GsPluginJob) plugin_job3 = NULL; + g_autoptr(GMainContext) context = NULL; + g_autoptr(GsDummyTestHelper) helper1 = gs_dummy_test_helper_new (); + g_autoptr(GsDummyTestHelper) helper2 = gs_dummy_test_helper_new (); + g_autoptr(GsDummyTestHelper) helper3 = gs_dummy_test_helper_new (); + + /* drop all caches */ + gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL); + gs_plugin_loader_setup_again (plugin_loader); + + /* get the updates list */ + plugin_job1 = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_DISTRO_UPDATES, NULL); + list = gs_plugin_loader_job_process (plugin_loader, plugin_job1, NULL, &helper3->error); + gs_test_flush_main_context (); + g_assert_no_error (helper3->error); + g_assert (list != NULL); + g_assert_cmpint (gs_app_list_length (list), ==, 1); + app1 = gs_app_list_index (list, 0); + g_assert_cmpstr (gs_app_get_id (app1), ==, "org.fedoraproject.release-rawhide.upgrade"); + g_assert_cmpint (gs_app_get_kind (app1), ==, AS_APP_KIND_OS_UPGRADE); + g_assert_cmpint (gs_app_get_state (app1), ==, AS_APP_STATE_AVAILABLE); + + /* allow only one operation at a time */ + gs_plugin_loader_set_max_parallel_ops (plugin_loader, 1); + + app2 = gs_app_new ("chiron.desktop"); + gs_app_set_management_plugin (app2, "dummy"); + gs_app_set_state (app2, AS_APP_STATE_AVAILABLE); + + /* use "proxy" prefix so the update function succeeds... */ + app3 = gs_app_new ("proxy-zeus.desktop"); + gs_app_set_management_plugin (app3, "dummy"); + gs_app_set_state (app3, AS_APP_STATE_UPDATABLE_LIVE); + + context = g_main_context_new (); + helper3->loop = g_main_loop_new (context, FALSE); + g_main_context_push_thread_default (context); + + /* call a few operations at the "same time" */ + + /* download an upgrade */ + g_object_unref (plugin_job1); + plugin_job1 = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD, + "app", app1, + NULL); + gs_plugin_loader_job_process_async (plugin_loader, + plugin_job1, + NULL, + plugin_job_action_cb, + helper1); + + /* install an app */ + plugin_job2 = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", app2, + NULL); + gs_plugin_loader_job_process_async (plugin_loader, + plugin_job2, + NULL, + plugin_job_action_cb, + helper2); + + /* update an app */ + plugin_job3 = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPDATE, + "app", app3, + NULL); + gs_plugin_loader_job_process_async (plugin_loader, + plugin_job3, + NULL, + plugin_job_action_cb, + helper3); + + /* since we have only 1 parallel installation op possible, + * verify the last operations are pending */ + g_assert_cmpint (gs_app_get_state (app2), ==, AS_APP_STATE_AVAILABLE); + g_assert_cmpint (gs_app_get_pending_action (app2), ==, GS_PLUGIN_ACTION_INSTALL); + g_assert_cmpint (gs_app_get_state (app3), ==, AS_APP_STATE_UPDATABLE_LIVE); + g_assert_cmpint (gs_app_get_pending_action (app3), ==, GS_PLUGIN_ACTION_UPDATE); + + /* wait for the 2nd installation to finish, it means the 1st should have been + * finished too */ + g_main_loop_run (helper3->loop); + g_main_context_pop_thread_default (context); + + gs_test_flush_main_context (); + g_assert_no_error (helper1->error); + g_assert_no_error (helper2->error); + g_assert_no_error (helper3->error); + + g_assert_cmpint (gs_app_get_state (app1), ==, AS_APP_STATE_UPDATABLE); + g_assert_cmpint (gs_app_get_state (app2), ==, AS_APP_STATE_INSTALLED); + g_assert_cmpint (gs_app_get_state (app3), ==, AS_APP_STATE_INSTALLED); + + /* set the default max parallel ops */ + gs_plugin_loader_set_max_parallel_ops (plugin_loader, 0); +} + +int +main (int argc, char **argv) +{ + g_autofree gchar *tmp_root = NULL; + gboolean ret; + int retval; + g_autofree gchar *xml = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsPluginLoader) plugin_loader = NULL; + const gchar *allowlist[] = { + "appstream", + "dummy", + "generic-updates", + "hardcoded-blocklist", + "desktop-categories", + "desktop-menu-path", + "icons", + "key-colors", + "provenance", + "provenance-license", + 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); + g_setenv ("GS_XMLB_VERBOSE", "1", TRUE); + + /* set all the things required as a dummy test harness */ + g_setenv ("GS_SELF_TEST_LOCALE", "en_GB", TRUE); + g_setenv ("GS_SELF_TEST_DUMMY_ENABLE", "1", TRUE); + g_setenv ("GS_SELF_TEST_PROVENANCE_SOURCES", "london*,boston", TRUE); + g_setenv ("GS_SELF_TEST_PROVENANCE_LICENSE_SOURCES", "london*,boston", TRUE); + g_setenv ("GS_SELF_TEST_PROVENANCE_LICENSE_URL", "https://www.debian.org/", TRUE); + g_setenv ("GNOME_SOFTWARE_POPULAR", "", TRUE); + + /* Use a common cache directory for all tests, since the appstream + * plugin uses it and cannot be reinitialised for each test. */ + tmp_root = g_dir_make_tmp ("gnome-software-dummy-test-XXXXXX", NULL); + g_assert (tmp_root != NULL); + g_setenv ("GS_SELF_TEST_CACHEDIR", tmp_root, TRUE); + + xml = g_strdup ("<?xml version=\"1.0\"?>\n" + "<components version=\"0.9\">\n" + " <component type=\"desktop\">\n" + " <id>chiron.desktop</id>\n" + " <name>Chiron</name>\n" + " <pkgname>chiron</pkgname>\n" + " </component>\n" + " <component type=\"desktop\">\n" + " <id>zeus.desktop</id>\n" + " <name>Zeus</name>\n" + " <summary>A teaching application</summary>\n" + " <pkgname>zeus</pkgname>\n" + " <icon type=\"stock\">drive-harddisk</icon>\n" + " <categories>\n" + " <category>AudioVideo</category>\n" + " <category>Player</category>\n" + " </categories>\n" + " <languages>\n" + " <lang percentage=\"100\">en_GB</lang>\n" + " </languages>\n" + " </component>\n" + " <component type=\"desktop\">\n" + " <id>mate-spell.desktop</id>\n" + " <name>Spell</name>\n" + " <summary>A spelling application for MATE</summary>\n" + " <pkgname>mate-spell</pkgname>\n" + " <icon type=\"stock\">drive-harddisk</icon>\n" + " <project_group>MATE</project_group>\n" + " </component>\n" + " <component type=\"addon\">\n" + " <id>zeus-spell.addon</id>\n" + " <extends>zeus.desktop</extends>\n" + " <name>Spell Check</name>\n" + " <summary>Check the spelling when teaching</summary>\n" + " <pkgname>zeus-spell</pkgname>\n" + " </component>\n" + " <component type=\"desktop\">\n" + " <id>Uninstall Zeus.desktop</id>\n" + " <name>Uninstall Zeus</name>\n" + " <summary>Uninstall the teaching application</summary>\n" + " <icon type=\"stock\">drive-harddisk</icon>\n" + " </component>\n" + " <component type=\"os-upgrade\">\n" + " <id>org.fedoraproject.release-rawhide.upgrade</id>\n" + " <name>Fedora Rawhide</name>\n" + " <summary>Release specific tagline</summary>\n" + " <pkgname>fedora-release</pkgname>\n" + " </component>\n" + " <info>\n" + " <scope>user</scope>\n" + " </info>\n" + "</components>\n"); + g_setenv ("GS_SELF_TEST_APPSTREAM_XML", xml, TRUE); + + /* 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 (); + g_signal_connect (plugin_loader, "status-changed", + G_CALLBACK (gs_plugin_loader_status_changed_cb), NULL); + gs_plugin_loader_add_location (plugin_loader, LOCALPLUGINDIR); + gs_plugin_loader_add_location (plugin_loader, LOCALPLUGINDIR_CORE); + ret = gs_plugin_loader_setup (plugin_loader, + (gchar**) allowlist, + NULL, + NULL, + &error); + g_assert_no_error (error); + g_assert (ret); + g_assert (!gs_plugin_loader_get_enabled (plugin_loader, "notgoingtoexist")); + g_assert (gs_plugin_loader_get_enabled (plugin_loader, "appstream")); + g_assert (gs_plugin_loader_get_enabled (plugin_loader, "dummy")); + + /* plugin tests go here */ + g_test_add_data_func ("/gnome-software/plugins/dummy/wildcard", + plugin_loader, + (GTestDataFunc) gs_plugins_dummy_wildcard_func); + g_test_add_data_func ("/gnome-software/plugins/dummy/plugin-cache", + plugin_loader, + (GTestDataFunc) gs_plugins_dummy_plugin_cache_func); + g_test_add_data_func ("/gnome-software/plugins/dummy/key-colors", + plugin_loader, + (GTestDataFunc) gs_plugins_dummy_key_colors_func); + g_test_add_data_func ("/gnome-software/plugins/dummy/search", + plugin_loader, + (GTestDataFunc) gs_plugins_dummy_search_func); + g_test_add_data_func ("/gnome-software/plugins/dummy/search-alternate", + plugin_loader, + (GTestDataFunc) gs_plugins_dummy_search_alternate_func); + g_test_add_data_func ("/gnome-software/plugins/dummy/hang", + plugin_loader, + (GTestDataFunc) gs_plugins_dummy_hang_func); + g_test_add_data_func ("/gnome-software/plugins/dummy/search{invalid}", + plugin_loader, + (GTestDataFunc) gs_plugins_dummy_search_invalid_func); + g_test_add_data_func ("/gnome-software/plugins/dummy/url-to-app", + plugin_loader, + (GTestDataFunc) gs_plugins_dummy_url_to_app_func); + g_test_add_data_func ("/gnome-software/plugins/dummy/install", + plugin_loader, + (GTestDataFunc) gs_plugins_dummy_install_func); + g_test_add_data_func ("/gnome-software/plugins/dummy/error", + plugin_loader, + (GTestDataFunc) gs_plugins_dummy_error_func); + g_test_add_data_func ("/gnome-software/plugins/dummy/installed", + plugin_loader, + (GTestDataFunc) gs_plugins_dummy_installed_func); + g_test_add_data_func ("/gnome-software/plugins/dummy/refine", + plugin_loader, + (GTestDataFunc) gs_plugins_dummy_refine_func); + g_test_add_data_func ("/gnome-software/plugins/dummy/updates", + plugin_loader, + (GTestDataFunc) gs_plugins_dummy_updates_func); + g_test_add_data_func ("/gnome-software/plugins/dummy/distro-upgrades", + plugin_loader, + (GTestDataFunc) gs_plugins_dummy_distro_upgrades_func); + g_test_add_data_func ("/gnome-software/plugins/dummy/metadata-quirks", + plugin_loader, + (GTestDataFunc) gs_plugins_dummy_metadata_quirks); + g_test_add_data_func ("/gnome-software/plugins/dummy/limit-parallel-ops", + plugin_loader, + (GTestDataFunc) gs_plugins_dummy_limit_parallel_ops_func); + retval = g_test_run (); + + /* Clean up. */ + gs_utils_rmtree (tmp_root, NULL); + + return retval; +} diff --git a/plugins/dummy/meson.build b/plugins/dummy/meson.build new file mode 100644 index 0000000..9235f4e --- /dev/null +++ b/plugins/dummy/meson.build @@ -0,0 +1,41 @@ +cargs = ['-DG_LOG_DOMAIN="GsPluginDummy"'] +cargs += ['-DLOCALPLUGINDIR="' + meson.current_build_dir() + '"'] +cargs += ['-DLOCALPLUGINDIR_CORE="' + meson.current_build_dir() + '/../core"'] + +shared_module( + 'gs_plugin_dummy', + sources : 'gs-plugin-dummy.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : [appstream_glib, gio_unix, goa, gtk, libsoup], + link_with : [ + libgnomesoftware + ] +) + +if get_option('tests') + e = executable( + 'gs-self-test-dummy', + compiled_schemas, + sources : [ + 'gs-self-test.c' + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + dependencies : [ + plugin_libs, + ], + link_with : [ + libgnomesoftware + ], + c_args : cargs, + ) + test('gs-self-test-dummy', e, suite: ['plugins', 'dummy'], env: test_env) +endif diff --git a/plugins/eos-updater/com.endlessm.Updater.xml b/plugins/eos-updater/com.endlessm.Updater.xml new file mode 100644 index 0000000..641c7f4 --- /dev/null +++ b/plugins/eos-updater/com.endlessm.Updater.xml @@ -0,0 +1,292 @@ +<!DOCTYPE node PUBLIC +'-//freedesktop//DTD D-BUS Object Introspection 1.0//EN' +'http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd'> +<node> + <!-- + com.endlessm.Updater: + @short_description: Endless OS updater control interface + + Interface to query the state of the OS updater, and to control it: checking + for, downloading and applying updates. + + The updater moves through a series of states (see the `State` property and + `StateChanged` signal), with state transitions triggered by the appropriate + method call, or by an error in an operation. It will return a + `com.endlessm.Updater.Error.WrongState` error if you attempt to perform an + invalid state transition. + --> + <interface name="com.endlessm.Updater"> + <!-- + Poll: + + Check for updates. This may be performed from the `Ready`, + `UpdateAvailable`, `UpdateReady` or `Error` states. It will immediately + transition to the `Polling` state. If an update is then successfully + found, it will transition to the `UpdateAvailable` state. If no update is + available, it will transition to the `Ready` state. If there is an error + checking for an update, it will transition to the `Error` state. + + If an update is found, its details will be reported through the updater’s + properties once the updater reaches the `UpdateAvailable` state. In + particular, through the `UpdateID`, `UpdateRefspec`, `UpdateLabel` and + `UpdateMessage` properties. + --> + <method name="Poll"></method> + + <!-- + PollVolume: + @path: Absolute path to the root directory of a volume to check. + + Like `Poll`, except this polls a specific removable volume (such as a USB + stick), rather than the internet. + + If a `.ostree/repo` directory is available beneath @path, and it contains + a valid OSTree repository, that repository will be checked for updates. + If no such directory exists, the updater will transition to state `Ready` + as no update is available. + --> + <method name="PollVolume"> + <arg name="path" type="s" direction="in"/> + </method> + + <!-- + Fetch: + + Download an update. This may be performed from the `UpdateAvailable` state + only. It will immediately transition to the `Fetching` state. If the + update is successfully downloaded, it will transition to the `UpdateReady` + state; if there was an error it will transition to `Error`. + + Download progress will be reported through the updater’s properties; in + particular, `DownloadedBytes`. + --> + <method name="Fetch"></method> + + <!-- + FetchFull: + @options: Potentially empty dictionary of options. + + Like `Fetch`, except options may be provided to affect its behaviour. + Currently, the following options are supported (unsupported options are + ignored): + + * `force` (type: `b`): If true, force the download without scheduling + it through the system’s metered data scheduler. Typically, this would + be true in response to an explicit user action, and false otherwise. + * `scheduling-timeout-seconds` (type: `u`): Number of seconds to wait + for permission to download from the system’s metered data scheduler, + before returning a `com.endlessm.Updater.Error.MeteredConnection` + error and cancelling the download. Pass zero to disable the timeout. + --> + <method name="FetchFull"> + <arg name="options" type="a{sv}" direction="in"/> + </method> + + <!-- + Apply: + + Apply a downloaded update so that it’s available to boot into on the next + boot. This may be performed from the `UpdateReady` state only. It will + immediately transition to the `Applying` state. If the update is + successfully applied, it will transition to the `UpdateApplied` state; if + there was an error it will transition to `Error`. + --> + <method name="Apply"></method> + + <!-- + Cancel: + + Cancel the ongoing poll, fetch or apply operation. This may be performed + from the `Polling`, `Fetching` or `ApplyingUpdate` states. It will cancel + the operation then transition to the `Error` state, with the error + `com.endlessm.Updater.Error.Cancelled`. + --> + <method name="Cancel"></method> + + <!-- + State: + + Current state of the updater. This will be one of the following: + + * `0` (`None`): No state. + * `1` (`Ready`): Ready to perform an action. + * `2` (`Error`): An error occurred. See the `ErrorName` and + `ErrorMessage` properties for details. + * `3` (`Polling`): Checking for updates. + * `4` (`UpdateAvailable`): An update is available. See the `UpdateID`, + `UpdateRefspec`, `UpdateLabel` and `UpdateMessage` properties for + details. + * `5` (`Fetching`): Downloading an update. See the `DownloadedBytes` + property for progress updates. + * `6` (`UpdateReady`): Update downloaded and ready to apply. + * `7` (`ApplyingUpdate`): Applying an update. + * `8` (`UpdateApplied`): Update applied and ready to reboot into. + + State changes are notified using the `StateChanged` signal. + --> + <property name="State" type="u" access="read"/> + + <!-- + UpdateID: + + Checksum of the OSTree commit available as an update, or the empty string + if no update is available. + --> + <property name="UpdateID" type="s" access="read"/> + + <!-- + UpdateRefspec: + + Refspec (remote name and branch name) of the OSTree commit available as an + update, or the empty string if no update is available. + --> + <property name="UpdateRefspec" type="s" access="read"/> + + <!-- + OriginalRefspec: + + Refspec (remote name and branch name) of the currently booted OSTree + commit. + --> + <property name="OriginalRefspec" type="s" access="read"/> + + <!-- + CurrentID: + + Checksum of the currently booted OSTree commit. + --> + <property name="CurrentID" type="s" access="read"/> + + <!-- + UpdateLabel: + + Subject of the OSTree commit available as an update, or the empty string + if it’s not set or if no update is available. This is the title of the + update. + --> + <property name="UpdateLabel" type="s" access="read"/> + + <!-- + UpdateMessage: + + Description body of the OSTree commit available as an update, or the empty + string if it’s not set or if no update is available. This is the + description of the update. + --> + <property name="UpdateMessage" type="s" access="read"/> + + <!-- + Version: + + Version number of the OSTree commit available as an update, or the empty + string if it’s not set or if no update is available. + --> + <property name="Version" type="s" access="read"/> + + <!-- + DownloadSize: + + Size (in bytes) of the update when downloaded, or `-1` if an update is + available but its download size is unknown. `0` if no update is available. + --> + <property name="DownloadSize" type="x" access="read"/> + + <!-- + DownloadedBytes: + + Number of bytes of the update which have already been downloaded. This + will be `0` before a download starts, and could be `-1` if the + `DownloadSize` is unknown. + --> + <property name="DownloadedBytes" type="x" access="read"/> + + <!-- + UnpackedSize: + + Size (in bytes) of the update when unpacked, or `-1` if an update is + available but its unpacked size is unknown. `0` if no update is available. + --> + <property name="UnpackedSize" type="x" access="read"/> + + <!-- + FullDownloadSize: + + Version of `DownloadSize` which also includes the sizes of parts of the + update which are already present locally (and hence which don’t need to + be downloaded again). + --> + <property name="FullDownloadSize" type="x" access="read"/> + + <!-- + FullUnpackedSize: + + Version of `UnpackedSize` which also includes the sizes of parts of the + update which are already unpacked locally (and hence which won’t occupy + further disk space once the update is applied). + --> + <property name="FullUnpackedSize" type="x" access="read"/> + + <!-- + ErrorCode: + + Error code of the current error, or `0` if no error has been reported. + This is in an unspecified error doman, and hence is useless. + + Deprecated: Use `ErrorName` instead. + --> + <property name="ErrorCode" type="u" access="read"> + <annotation name="org.freedesktop.DBus.Deprecated" value="true"/> + </property> + + <!-- + ErrorName: + + A fully-qualified D-Bus error name, as might be returned from a D-Bus + method. + + This is the empty string if no error has been reported. + + Known errors include: + + * `com.endlessm.Updater.Error.WrongState`: Method was called in a state + which doesn’t support that method. + * `com.endlessm.Updater.Error.LiveBoot`: The updater cannot be used + because the current system is a live boot. + * `com.endlessm.Updater.Error.WrongConfiguration`: A configuration file + contains an error. + * `com.endlessm.Updater.Error.NotOstreeSystem`: The updater cannot be + used because the current system is not booted from an OSTree commit. + * `com.endlessm.Updater.Error.Fetching`: Error when downloading an + update. + * `com.endlessm.Updater.Error.MalformedAutoinstallSpec`: An autoinstall + specification in the pending update contains an error. + * `com.endlessm.Updater.Error.UnknownEntryInAutoinstallSpec`: An + autoinstall specification in the pending update contains an unknown + entry. + * `com.endlessm.Updater.Error.FlatpakRemoteConflict`: An autoinstall + specification in the pending update contains a remote name which + doesn’t match the system’s configuration. + * `com.endlessm.Updater.Error.MeteredConnection`: A fetch operation timed + out while waiting for permission to download. + --> + <property name="ErrorName" type="s" access="read"/> + + <!-- + ErrorMessage: + + A human-readable (but unlocalised) error message, or the empty string if + no error has been reported. + --> + <property name="ErrorMessage" type="s" access="read"/> + + <!-- + StateChanged: + @state: The new state. + + Signal notifying of a change in the `State` property. + --> + <signal name="StateChanged"> + <arg type="u" name="state"/> + </signal> + </interface> +</node> diff --git a/plugins/eos-updater/gs-plugin-eos-updater.c b/plugins/eos-updater/gs-plugin-eos-updater.c new file mode 100644 index 0000000..a8aba16 --- /dev/null +++ b/plugins/eos-updater/gs-plugin-eos-updater.c @@ -0,0 +1,962 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016-2019 Endless Mobile, Inc + * + * Authors: + * Joaquim Rocha <jrocha@endlessm.com> + * Philip Withnall <withnall@endlessm.com> + * + * Licensed under the GNU General Public License Version 2 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include <config.h> +#include <gio/gio.h> +#include <glib.h> +#include <glib/gi18n.h> +#include <glib-object.h> +#include <gnome-software.h> +#include <gs-plugin.h> +#include <gs-utils.h> +#include <math.h> +#include <ostree.h> + +#include "gs-eos-updater-generated.h" + +/* + * SECTION: + * Plugin to poll for, download and apply OS updates using the `eos-updater` + * service when running on Endless OS. + * + * This plugin is only useful on Endless OS. + * + * It creates a proxy for the `eos-updater` D-Bus service, which implements a + * basic state machine which progresses through several states in order to + * download updates: `Ready` (doing nothing) → `Poll` (checking for updates) → + * `Fetch` (downloading an update) → `Apply` (deploying the update’s OSTree, + * before a reboot). Any state may transition to the `Error` state at any time, + * and the daemon may disappear at any time. + * + * This plugin follows the state transitions signalled by the daemon, and + * updates the state of a single #GsApp instance (`os_upgrade`) to reflect the + * OS upgrade in the UI. + * + * Calling gs_plugin_refresh() will result in this plugin calling the `Poll()` + * method on the `eos-updater` daemon to check for a new update. + * + * Calling gs_plugin_app_upgrade_download() will result in this plugin calling + * a sequence of methods on the `eos-updater` daemon to check for, download and + * apply an update. Typically, gs_plugin_app_upgrade_download() should be called + * once `eos-updater` is already in the `UpdateAvailable` state. It will report + * progress information, with the first 75 percentage points of the progress + * reporting the download progress, and the final 25 percentage points reporting + * the OSTree deployment progress. The final 25 percentage points are currently + * faked because we can’t get reasonable progress data out of OSTree. + * + * The proxy object (`updater_proxy`) uses the thread-default main context from + * the gs_plugin_setup() function, which is currently the global default main + * context from gnome-software’s main thread. This means all the signal + * callbacks from the proxy will be executed in the main thread, and *must not + * block*. + * + * The other functions (gs_plugin_refresh(), gs_plugin_app_upgrade_download(), + * etc.) are called in #GTask worker threads. They are allowed to call methods + * on the proxy; the main thread is only allowed to receive signals and check + * properties on the proxy, to avoid blocking. Consequently, worker threads need + * to block on the main thread receiving state change signals from + * `eos-updater`. Receipt of these signals is notified through + * `state_change_cond`. This means that all functions which access the + * `GsPluginData` must lock it using the `mutex`. + * + * `updater_proxy`, `os_upgrade` and `cancellable` are only set in + * gs_plugin_setup(), and are both internally thread-safe — so they can both be + * dereferenced and have their methods called from any thread without + * necessarily holding `mutex`. + * + * Cancellation of any operations on the `eos-updater` daemon (polling, fetching + * or applying) is implemented by calling the `Cancel()` method on it. This is + * permanently connected to the private `cancellable` #GCancellable instance, + * which persists for the lifetime of the plugin. The #GCancellable instances + * for various operations can be temporarily chained to it for the duration of + * each operation. + */ + +static const guint max_progress_for_update = 75; /* percent */ + +typedef enum { + EOS_UPDATER_STATE_NONE = 0, + EOS_UPDATER_STATE_READY, + EOS_UPDATER_STATE_ERROR, + EOS_UPDATER_STATE_POLLING, + EOS_UPDATER_STATE_UPDATE_AVAILABLE, + EOS_UPDATER_STATE_FETCHING, + EOS_UPDATER_STATE_UPDATE_READY, + EOS_UPDATER_STATE_APPLYING_UPDATE, + EOS_UPDATER_STATE_UPDATE_APPLIED, +} EosUpdaterState; +#define EOS_UPDATER_N_STATES (EOS_UPDATER_STATE_UPDATE_APPLIED + 1) + +static const gchar * +eos_updater_state_to_str (EosUpdaterState state) +{ + const gchar * const eos_updater_state_str[] = { + "None", + "Ready", + "Error", + "Polling", + "UpdateAvailable", + "Fetching", + "UpdateReady", + "ApplyingUpdate", + "UpdateApplied", + }; + + G_STATIC_ASSERT (G_N_ELEMENTS (eos_updater_state_str) == EOS_UPDATER_N_STATES); + + g_return_val_if_fail ((gint) state < EOS_UPDATER_N_STATES, "unknown"); + return eos_updater_state_str[state]; +} + +static void +gs_eos_updater_error_convert (GError **perror) +{ + GError *error = perror != NULL ? *perror : NULL; + + /* not set */ + if (error == NULL) + return; + + /* parse remote eos-updater error */ + if (g_dbus_error_is_remote_error (error)) { + g_autofree gchar *remote_error = g_dbus_error_get_remote_error (error); + + g_dbus_error_strip_remote_error (error); + + if (g_str_equal (remote_error, "com.endlessm.Updater.Error.WrongState")) { + error->code = GS_PLUGIN_ERROR_FAILED; + } else if (g_str_equal (remote_error, "com.endlessm.Updater.Error.LiveBoot") || + g_str_equal (remote_error, "com.endlessm.Updater.Error.NotOstreeSystem") || + g_str_equal (remote_error, "org.freedesktop.DBus.Error.ServiceUnknown")) { + error->code = GS_PLUGIN_ERROR_NOT_SUPPORTED; + } else if (g_str_equal (remote_error, "com.endlessm.Updater.Error.WrongConfiguration")) { + error->code = GS_PLUGIN_ERROR_FAILED; + } else if (g_str_equal (remote_error, "com.endlessm.Updater.Error.Fetching")) { + error->code = GS_PLUGIN_ERROR_DOWNLOAD_FAILED; + } else if (g_str_equal (remote_error, "com.endlessm.Updater.Error.MalformedAutoinstallSpec") || + g_str_equal (remote_error, "com.endlessm.Updater.Error.UnknownEntryInAutoinstallSpec") || + g_str_equal (remote_error, "com.endlessm.Updater.Error.FlatpakRemoteConflict")) { + error->code = GS_PLUGIN_ERROR_FAILED; + } else if (g_str_equal (remote_error, "com.endlessm.Updater.Error.MeteredConnection")) { + error->code = GS_PLUGIN_ERROR_NO_NETWORK; + } else if (g_str_equal (remote_error, "com.endlessm.Updater.Error.Cancelled")) { + error->code = GS_PLUGIN_ERROR_CANCELLED; + } else { + g_warning ("Can’t reliably fixup remote error ‘%s’", remote_error); + error->code = GS_PLUGIN_ERROR_FAILED; + } + error->domain = GS_PLUGIN_ERROR; + return; + } + + /* this is allowed for low-level errors */ + if (gs_utils_error_convert_gio (perror)) + return; + + /* this is allowed for low-level errors */ + if (gs_utils_error_convert_gdbus (perror)) + return; +} + +/* the percentage of the progress bar to use for applying the OS upgrade; + * we need to fake the progress in this percentage because applying the OS upgrade + * can take a long time and we don't want the user to think that the upgrade has + * stalled */ +static const guint upgrade_apply_progress_range = 100 - max_progress_for_update; /* percent */ +static const gfloat upgrade_apply_max_time = 600.0; /* sec */ +static const gfloat upgrade_apply_step_time = 0.250; /* sec */ + +static void sync_state_from_updater_unlocked (GsPlugin *plugin); + +struct GsPluginData +{ + /* These members are only set once in gs_plugin_setup(), and are + * internally thread-safe, so can be accessed without holding @mutex: */ + GsEosUpdater *updater_proxy; /* (owned) */ + GsApp *os_upgrade; /* (owned) */ + GCancellable *cancellable; /* (owned) */ + gulong cancelled_id; + + /* These members must only ever be accessed from the main thread, so + * can be accessed without holding @mutex: */ + gfloat upgrade_fake_progress; + guint upgrade_fake_progress_handler; + + /* State synchronisation between threads: */ + GMutex mutex; + GCond state_change_cond; /* locked by @mutex */ +}; + +static void +os_upgrade_cancelled_cb (GCancellable *cancellable, + GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + + g_debug ("%s: Cancelling upgrade", G_STRFUNC); + gs_eos_updater_call_cancel (priv->updater_proxy, NULL, NULL, NULL); +} + +static gboolean +should_add_os_upgrade (AsAppState state) +{ + switch (state) { + case AS_APP_STATE_AVAILABLE: + case AS_APP_STATE_AVAILABLE_LOCAL: + case AS_APP_STATE_UPDATABLE: + case AS_APP_STATE_QUEUED_FOR_INSTALL: + case AS_APP_STATE_INSTALLING: + case AS_APP_STATE_UPDATABLE_LIVE: + return TRUE; + case AS_APP_STATE_UNKNOWN: + case AS_APP_STATE_INSTALLED: + case AS_APP_STATE_UNAVAILABLE: + case AS_APP_STATE_REMOVING: + default: + return FALSE; + } +} + +/* Wrapper around gs_app_set_state() which ensures we also notify of update + * changes if we change between non-upgradable and upgradable states, so that + * the app is notified to appear in the UI. */ +static void +app_set_state (GsPlugin *plugin, + GsApp *app, + AsAppState new_state) +{ + AsAppState old_state = gs_app_get_state (app); + + if (new_state == old_state) + return; + + gs_app_set_state (app, new_state); + + if (should_add_os_upgrade (old_state) != + should_add_os_upgrade (new_state)) { + g_debug ("%s: Calling gs_plugin_updates_changed()", G_STRFUNC); + gs_plugin_updates_changed (plugin); + } +} + +static gboolean +eos_updater_error_is_cancelled (const gchar *error_name) +{ + return (g_strcmp0 (error_name, "com.endlessm.Updater.Error.Cancelled") == 0); +} + +/* This will be invoked in the main thread. */ +static void +updater_state_changed (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex); + + g_debug ("%s", G_STRFUNC); + + sync_state_from_updater_unlocked (plugin); + + /* Signal any blocked threads; typically this will be + * gs_plugin_app_upgrade_download() in a #GTask worker thread. */ + g_cond_broadcast (&priv->state_change_cond); +} + +/* This will be invoked in the main thread. */ +static void +updater_downloaded_bytes_changed (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex); + + sync_state_from_updater_unlocked (plugin); +} + +/* This will be invoked in the main thread, but doesn’t currently need to hold + * `mutex` since it only accesses `priv->updater_proxy` and `priv->os_upgrade`, + * both of which are internally thread-safe. */ +static void +updater_version_changed (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + const gchar *version = gs_eos_updater_get_version (priv->updater_proxy); + + /* If eos-updater goes away, we want to retain the previously set value + * of the version, for use in error messages. */ + if (version != NULL) + gs_app_set_version (priv->os_upgrade, version); +} + +/* This will be invoked in the main thread, but doesn’t currently need to hold + * `mutex` since `priv->updater_proxy` and `priv->os_upgrade` are both + * thread-safe, and `priv->upgrade_fake_progress` and + * `priv->upgrade_fake_progress_handler` are only ever accessed from the main + * thread. */ +static gboolean +fake_os_upgrade_progress_cb (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + gfloat normal_step; + guint new_progress; + const gfloat fake_progress_max = 99.0; + + if (gs_eos_updater_get_state (priv->updater_proxy) != EOS_UPDATER_STATE_APPLYING_UPDATE || + priv->upgrade_fake_progress > fake_progress_max) { + priv->upgrade_fake_progress = 0; + priv->upgrade_fake_progress_handler = 0; + return G_SOURCE_REMOVE; + } + + normal_step = (gfloat) upgrade_apply_progress_range / + (upgrade_apply_max_time / upgrade_apply_step_time); + + priv->upgrade_fake_progress += normal_step; + + new_progress = max_progress_for_update + + (guint) round (priv->upgrade_fake_progress); + gs_app_set_progress (priv->os_upgrade, + MIN (new_progress, (guint) fake_progress_max)); + + g_debug ("OS upgrade fake progress: %f", priv->upgrade_fake_progress); + + return G_SOURCE_CONTINUE; +} + +/* This method deals with the synchronization between the EOS updater's states + * (D-Bus service) and the OS upgrade's states (GsApp), in order to show the user + * what is happening and what they can do. + * + * It must be called with priv->mutex already locked. */ +static void +sync_state_from_updater_unlocked (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + GsApp *app = priv->os_upgrade; + EosUpdaterState state; + AsAppState previous_app_state = gs_app_get_state (app); + AsAppState current_app_state; + + /* in case the OS upgrade has been disabled */ + if (priv->updater_proxy == NULL) { + g_debug ("%s: Updater disabled", G_STRFUNC); + return; + } + + state = gs_eos_updater_get_state (priv->updater_proxy); + g_debug ("EOS Updater state changed: %s", eos_updater_state_to_str (state)); + + switch (state) { + case EOS_UPDATER_STATE_NONE: + case EOS_UPDATER_STATE_READY: { + app_set_state (plugin, app, AS_APP_STATE_UNKNOWN); + break; + } case EOS_UPDATER_STATE_POLLING: { + /* Nothing to do here. */ + break; + } case EOS_UPDATER_STATE_UPDATE_AVAILABLE: { + guint64 total_size; + + app_set_state (plugin, app, AS_APP_STATE_AVAILABLE); + + total_size = gs_eos_updater_get_download_size (priv->updater_proxy); + gs_app_set_size_download (app, total_size); + + break; + } + case EOS_UPDATER_STATE_FETCHING: { + guint64 total_size = 0; + guint64 downloaded = 0; + gfloat progress = 0; + + /* FIXME: Set to QUEUED_FOR_INSTALL if we’re waiting for metered + * data permission. */ + app_set_state (plugin, app, AS_APP_STATE_INSTALLING); + + downloaded = gs_eos_updater_get_downloaded_bytes (priv->updater_proxy); + total_size = gs_eos_updater_get_download_size (priv->updater_proxy); + + if (total_size == 0) { + g_debug ("OS upgrade %s total size is 0!", + gs_app_get_unique_id (app)); + } else { + /* set progress only up to a max percentage, leaving the + * remaining for applying the update */ + progress = (gfloat) downloaded / (gfloat) total_size * + (gfloat) max_progress_for_update; + } + gs_app_set_progress (app, (guint) progress); + + break; + } + case EOS_UPDATER_STATE_UPDATE_READY: { + app_set_state (plugin, app, AS_APP_STATE_UPDATABLE); + break; + } + case EOS_UPDATER_STATE_APPLYING_UPDATE: { + /* set as 'installing' because if it is applying the update, we + * want to show the progress bar */ + app_set_state (plugin, app, AS_APP_STATE_INSTALLING); + + /* set up the fake progress to inform the user that something + * is still being done (we don't get progress reports from + * deploying updates) */ + if (priv->upgrade_fake_progress_handler != 0) + g_source_remove (priv->upgrade_fake_progress_handler); + priv->upgrade_fake_progress = 0; + priv->upgrade_fake_progress_handler = + g_timeout_add ((guint) (1000.0 * upgrade_apply_step_time), + (GSourceFunc) fake_os_upgrade_progress_cb, + plugin); + + break; + } + case EOS_UPDATER_STATE_UPDATE_APPLIED: { + app_set_state (plugin, app, AS_APP_STATE_UPDATABLE); + + break; + } + case EOS_UPDATER_STATE_ERROR: { + const gchar *error_name; + const gchar *error_message; + + error_name = gs_eos_updater_get_error_name (priv->updater_proxy); + error_message = gs_eos_updater_get_error_message (priv->updater_proxy); + + /* unless the error is because the user cancelled the upgrade, + * we should make sure it gets in the journal */ + if (!eos_updater_error_is_cancelled (error_name)) + g_warning ("Got OS upgrade error state with name '%s': %s", + error_name, error_message); + + /* We can’t recover the app state since eos-updater needs to + * go through the ready → poll → fetch → apply loop again in + * order to recover its state. So go back to ‘unknown’. */ + app_set_state (plugin, app, AS_APP_STATE_UNKNOWN); + + /* Cancelling anything in the updater will result in a + * transition to the Error state. Use that as a cue to reset + * our #GCancellable ready for next time. */ + g_cancellable_reset (priv->cancellable); + + break; + } + default: + g_warning ("Encountered unknown eos-updater state: %u", state); + break; + } + + current_app_state = gs_app_get_state (app); + + g_debug ("%s: Old app state: %s; new app state: %s", + G_STRFUNC, as_app_state_to_string (previous_app_state), + as_app_state_to_string (current_app_state)); + + /* if the state changed from or to 'unknown', we need to notify that a + * new update should be shown */ + if (should_add_os_upgrade (previous_app_state) != + should_add_os_upgrade (current_app_state)) { + g_debug ("%s: Calling gs_plugin_updates_changed()", G_STRFUNC); + gs_plugin_updates_changed (plugin); + } +} + +/* This is called in the main thread, so will end up creating an @updater_proxy + * which is tied to the main thread’s #GMainContext. */ +gboolean +gs_plugin_setup (GsPlugin *plugin, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GError) error_local = NULL; + g_autofree gchar *name_owner = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(AsIcon) ic = NULL; + g_autoptr(GMutexLocker) locker = NULL; + + g_debug ("%s", G_STRFUNC); + + g_mutex_init (&priv->mutex); + g_cond_init (&priv->state_change_cond); + + locker = g_mutex_locker_new (&priv->mutex); + + priv->cancellable = g_cancellable_new (); + priv->cancelled_id = + g_cancellable_connect (priv->cancellable, + G_CALLBACK (os_upgrade_cancelled_cb), + plugin, NULL); + + /* Check that the proxy exists (and is owned; it should auto-start) so + * we can disable the plugin for systems which don’t have eos-updater. + * Throughout the rest of the plugin, errors from the daemon + * (particularly where it has disappeared off the bus) are ignored, and + * the poll/fetch/apply sequence is run through again to recover from + * the error. This is the only point in the plugin where we consider an + * error from eos-updater to be fatal to the plugin. */ + priv->updater_proxy = gs_eos_updater_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_NONE, + "com.endlessm.Updater", + "/com/endlessm/Updater", + cancellable, + error); + if (priv->updater_proxy == NULL) { + gs_eos_updater_error_convert (error); + return FALSE; + } + + name_owner = g_dbus_proxy_get_name_owner (G_DBUS_PROXY (priv->updater_proxy)); + + if (name_owner == NULL) { + g_set_error_literal (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NOT_SUPPORTED, + "Couldn’t create EOS Updater proxy: couldn’t get name owner"); + return FALSE; + } + + g_signal_connect_object (priv->updater_proxy, "notify::state", + G_CALLBACK (updater_state_changed), + plugin, G_CONNECT_SWAPPED); + g_signal_connect_object (priv->updater_proxy, + "notify::downloaded-bytes", + G_CALLBACK (updater_downloaded_bytes_changed), + plugin, G_CONNECT_SWAPPED); + g_signal_connect_object (priv->updater_proxy, "notify::version", + G_CALLBACK (updater_version_changed), + plugin, G_CONNECT_SWAPPED); + + /* prepare EOS upgrade app + sync initial state */ + + /* use stock icon */ + ic = as_icon_new (); + as_icon_set_kind (ic, AS_ICON_KIND_STOCK); + as_icon_set_name (ic, "application-x-addon"); + + /* create the OS upgrade */ + app = gs_app_new ("com.endlessm.EOS.upgrade"); + gs_app_add_icon (app, ic); + gs_app_set_scope (app, AS_APP_SCOPE_SYSTEM); + gs_app_set_kind (app, AS_APP_KIND_OS_UPGRADE); + /* TRANSLATORS: ‘Endless OS’ is a brand name; https://endlessos.com/ */ + gs_app_set_name (app, GS_APP_QUALITY_LOWEST, _("Endless OS")); + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, + /* TRANSLATORS: ‘Endless OS’ is a brand name; https://endlessos.com/ */ + _("An Endless OS update with new features and fixes.")); + /* ensure that the version doesn't appear as (NULL) in the banner, it + * should be changed to the right value when it changes in the eos-updater */ + gs_app_set_version (app, ""); + gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT); + gs_app_add_quirk (app, GS_APP_QUIRK_PROVENANCE); + gs_app_add_quirk (app, GS_APP_QUIRK_NOT_REVIEWABLE); + gs_app_set_management_plugin (app, gs_plugin_get_name (plugin)); + gs_app_set_metadata (app, "GnomeSoftware::UpgradeBanner-css", + "background: url('" DATADIR "/gnome-software/upgrade-bg.png');" + "background-size: 100% 100%;"); + + priv->os_upgrade = g_steal_pointer (&app); + + /* sync initial state */ + sync_state_from_updater_unlocked (plugin); + + return TRUE; +} + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + gs_plugin_alloc_data (plugin, sizeof(GsPluginData)); +} + +void +gs_plugin_destroy (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + + if (priv->upgrade_fake_progress_handler != 0) { + g_source_remove (priv->upgrade_fake_progress_handler); + priv->upgrade_fake_progress_handler = 0; + } + + if (priv->updater_proxy != NULL) { + g_signal_handlers_disconnect_by_func (priv->updater_proxy, + G_CALLBACK (updater_state_changed), + plugin); + g_signal_handlers_disconnect_by_func (priv->updater_proxy, + G_CALLBACK (updater_downloaded_bytes_changed), + plugin); + g_signal_handlers_disconnect_by_func (priv->updater_proxy, + G_CALLBACK (updater_version_changed), + plugin); + } + + g_cancellable_cancel (priv->cancellable); + if (priv->cancellable != NULL && priv->cancelled_id != 0) + g_cancellable_disconnect (priv->cancellable, priv->cancelled_id); + g_clear_object (&priv->cancellable); + + g_clear_object (&priv->updater_proxy); + + g_clear_object (&priv->os_upgrade); + + g_cond_clear (&priv->state_change_cond); + g_mutex_clear (&priv->mutex); +} + +/* Called in a #GTask worker thread, but it can run without holding + * `priv->mutex` since it doesn’t need to synchronise on state. */ +gboolean +gs_plugin_refresh (GsPlugin *plugin, + guint cache_age, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + EosUpdaterState updater_state; + gboolean success; + + /* We let the eos-updater daemon do its own caching, so ignore the + * @cache_age, unless it’s %G_MAXUINT, which signifies startup of g-s. + * In that case, it’s probably just going to load the system too much to + * do an update check now. We can wait. */ + g_debug ("%s: cache_age: %u", G_STRFUNC, cache_age); + + if (cache_age == G_MAXUINT) + return TRUE; + + /* check if the OS upgrade has been disabled */ + if (priv->updater_proxy == NULL) { + g_debug ("%s: Updater disabled", G_STRFUNC); + return TRUE; + } + + /* poll in the error/none/ready states to check if there's an + * update available */ + updater_state = gs_eos_updater_get_state (priv->updater_proxy); + switch (updater_state) { + case EOS_UPDATER_STATE_ERROR: + case EOS_UPDATER_STATE_NONE: + case EOS_UPDATER_STATE_READY: + /* This sync call will block the job thread, which is OK. */ + success = gs_eos_updater_call_poll_sync (priv->updater_proxy, + cancellable, error); + gs_eos_updater_error_convert (error); + return success; + default: + g_debug ("%s: Updater in state %s; not polling", + G_STRFUNC, eos_updater_state_to_str (updater_state)); + return TRUE; + } +} + +/* Called in a #GTask worker thread, but it can run without holding + * `priv->mutex` since it doesn’t need to synchronise on state. */ +gboolean +gs_plugin_add_distro_upgrades (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + + g_debug ("%s", G_STRFUNC); + + /* if we are testing the plugin, then always add the OS upgrade */ + if (g_getenv ("GS_PLUGIN_EOS_TEST") != NULL) { + gs_app_set_state (priv->os_upgrade, AS_APP_STATE_AVAILABLE); + gs_app_list_add (list, priv->os_upgrade); + return TRUE; + } + + /* check if the OS upgrade has been disabled */ + if (priv->updater_proxy == NULL) { + g_debug ("%s: Updater disabled", G_STRFUNC); + return TRUE; + } + + if (should_add_os_upgrade (gs_app_get_state (priv->os_upgrade))) { + g_debug ("Adding EOS upgrade: %s", + gs_app_get_unique_id (priv->os_upgrade)); + gs_app_list_add (list, priv->os_upgrade); + } else { + g_debug ("Not adding EOS upgrade"); + } + + return TRUE; +} + +/* Must be called with priv->mutex already locked. */ +static gboolean +wait_for_state_change_unlocked (GsPlugin *plugin, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + EosUpdaterState old_state, new_state; + + old_state = new_state = gs_eos_updater_get_state (priv->updater_proxy); + g_debug ("%s: Old state ‘%s’", G_STRFUNC, eos_updater_state_to_str (old_state)); + + while (new_state == old_state && + !g_cancellable_is_cancelled (cancellable)) { + g_cond_wait (&priv->state_change_cond, &priv->mutex); + new_state = gs_eos_updater_get_state (priv->updater_proxy); + } + + if (!g_cancellable_set_error_if_cancelled (cancellable, error)) { + g_debug ("%s: New state ‘%s’", G_STRFUNC, eos_updater_state_to_str (new_state)); + return TRUE; + } else { + g_debug ("%s: Cancelled", G_STRFUNC); + return FALSE; + } +} + +/* Could be executed in any thread. No need to hold `priv->mutex` since we don’t + * access anything which is not thread-safe. */ +static void +cancelled_cb (GCancellable *ui_cancellable, + gpointer user_data) +{ + GsPlugin *plugin = GS_PLUGIN (user_data); + GsPluginData *priv = gs_plugin_get_data (plugin); + + /* Chain cancellation. */ + g_debug ("Propagating OS download cancellation from %p to %p", + ui_cancellable, priv->cancellable); + g_cancellable_cancel (priv->cancellable); + + /* And wake up anything blocking on a state change. */ + g_cond_broadcast (&priv->state_change_cond); +} + +/* Called in a #GTask worker thread, and it needs to hold `priv->mutex` due to + * synchronising on state with the main thread. */ +gboolean +gs_plugin_app_upgrade_download (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + gulong cancelled_id = 0; + EosUpdaterState state; + gboolean done, allow_restart; + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex); + + /* only process this app if was created by this plugin */ + if (g_strcmp0 (gs_app_get_management_plugin (app), + gs_plugin_get_name (plugin)) != 0) + return TRUE; + + /* if the OS upgrade has been disabled */ + if (priv->updater_proxy == NULL) { + g_set_error (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED, + "The OS upgrade has been disabled in the EOS plugin"); + return FALSE; + } + + g_assert (app == priv->os_upgrade); + + /* Set up cancellation. */ + g_debug ("Chaining cancellation from %p to %p", cancellable, priv->cancellable); + if (cancellable != NULL) { + cancelled_id = g_cancellable_connect (cancellable, + G_CALLBACK (cancelled_cb), + plugin, NULL); + } + + /* Step through the state machine until we are finished downloading and + * applying the update, or until an error occurs. All of the D-Bus calls + * here will block until the method call is complete. */ + state = gs_eos_updater_get_state (priv->updater_proxy); + + done = FALSE; + allow_restart = (state == EOS_UPDATER_STATE_NONE || + state == EOS_UPDATER_STATE_READY || + state == EOS_UPDATER_STATE_ERROR); + + while (!done && !g_cancellable_is_cancelled (cancellable)) { + state = gs_eos_updater_get_state (priv->updater_proxy); + g_debug ("%s: State ‘%s’", G_STRFUNC, eos_updater_state_to_str (state)); + + switch (state) { + case EOS_UPDATER_STATE_NONE: + case EOS_UPDATER_STATE_READY: { + /* Poll for an update. This typically only happens if + * we’ve drifted out of sync with the updater process + * due to it dying. In that case, only restart once + * before giving up, so we don’t end up in an endless + * loop (say, if eos-updater always died 50% of the way + * through a download). */ + if (allow_restart) { + allow_restart = FALSE; + g_debug ("Restarting OS upgrade from none/ready state"); + if (!gs_eos_updater_call_poll_sync (priv->updater_proxy, + cancellable, error)) { + gs_eos_updater_error_convert (error); + return FALSE; + } + } else { + /* Display an error to the user. */ + g_autoptr(GError) error_local = NULL; + g_autoptr(GsPluginEvent) event = gs_plugin_event_new (); + g_set_error_literal (&error_local, GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED, + _("EOS update service could not fetch and apply the update.")); + gs_eos_updater_error_convert (&error_local); + gs_plugin_event_set_app (event, app); + gs_plugin_event_set_action (event, GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD); + gs_plugin_event_set_error (event, error_local); + gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING); + gs_plugin_report_event (plugin, event); + + /* Error out. */ + done = TRUE; + } + + break; + } case EOS_UPDATER_STATE_POLLING: { + /* Nothing to do here. */ + break; + } case EOS_UPDATER_STATE_UPDATE_AVAILABLE: { + g_auto(GVariantDict) options_dict = G_VARIANT_DICT_INIT (NULL); + + /* when the OS upgrade was started by the user and the + * updater reports an available update, (meaning we were + * polling before), we should readily call fetch */ + g_variant_dict_insert (&options_dict, "force", "b", TRUE); + + if (!gs_eos_updater_call_fetch_full_sync (priv->updater_proxy, + g_variant_dict_end (&options_dict), + cancellable, error)) { + gs_eos_updater_error_convert (error); + return FALSE; + } + + break; + } + case EOS_UPDATER_STATE_FETCHING: { + /* Nothing to do here. */ + break; + } + case EOS_UPDATER_STATE_UPDATE_READY: { + /* if there's an update ready to deployed, and it was started by + * the user, we should proceed to applying the upgrade */ + gs_app_set_progress (app, max_progress_for_update); + + if (!gs_eos_updater_call_apply_sync (priv->updater_proxy, + cancellable, error)) { + gs_eos_updater_error_convert (error); + return FALSE; + } + + break; + } + case EOS_UPDATER_STATE_APPLYING_UPDATE: { + /* Nothing to do here. */ + break; + } + case EOS_UPDATER_STATE_UPDATE_APPLIED: { + /* Done! */ + done = TRUE; + break; + } + case EOS_UPDATER_STATE_ERROR: { + const gchar *error_name; + const gchar *error_message; + g_autoptr(GError) error_local = NULL; + + error_name = gs_eos_updater_get_error_name (priv->updater_proxy); + error_message = gs_eos_updater_get_error_message (priv->updater_proxy); + error_local = g_dbus_error_new_for_dbus_error (error_name, error_message); + + /* Display an error to the user, unless they cancelled + * the download. */ + if (!eos_updater_error_is_cancelled (error_name)) { + g_autoptr(GsPluginEvent) event = gs_plugin_event_new (); + gs_eos_updater_error_convert (&error_local); + gs_plugin_event_set_app (event, app); + gs_plugin_event_set_action (event, GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD); + gs_plugin_event_set_error (event, error_local); + gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING); + gs_plugin_report_event (plugin, event); + } + + /* Unconditionally call Poll() to get the updater out + * of the error state and to allow the update to be + * displayed in the UI again and retried. Exit the + * state change loop immediately, though, to prevent + * possible endless loops between the Poll/Error + * states. */ + allow_restart = FALSE; + g_debug ("Restarting OS upgrade on error"); + if (!gs_eos_updater_call_poll_sync (priv->updater_proxy, + cancellable, error)) { + gs_eos_updater_error_convert (error); + return FALSE; + } + + /* Error out. */ + done = TRUE; + + break; + } + default: + g_warning ("Encountered unknown eos-updater state: %u", state); + break; + } + + /* Block on the next state change. */ + if (!done && + !wait_for_state_change_unlocked (plugin, cancellable, error)) { + gs_eos_updater_error_convert (error); + return FALSE; + } + } + + if (cancellable != NULL && cancelled_id != 0) { + g_debug ("Disconnecting cancellable %p", cancellable); + g_cancellable_disconnect (cancellable, cancelled_id); + } + + /* Process the final state. */ + if (gs_eos_updater_get_state (priv->updater_proxy) == EOS_UPDATER_STATE_ERROR) { + const gchar *error_name; + const gchar *error_message; + g_autoptr(GError) error_local = NULL; + + error_name = gs_eos_updater_get_error_name (priv->updater_proxy); + error_message = gs_eos_updater_get_error_message (priv->updater_proxy); + error_local = g_dbus_error_new_for_dbus_error (error_name, error_message); + gs_eos_updater_error_convert (&error_local); + g_propagate_error (error, g_steal_pointer (&error_local)); + + return FALSE; + } else if (g_cancellable_set_error_if_cancelled (cancellable, error)) { + gs_eos_updater_error_convert (error); + return FALSE; + } + + return TRUE; +} diff --git a/plugins/eos-updater/meson.build b/plugins/eos-updater/meson.build new file mode 100644 index 0000000..2a22e88 --- /dev/null +++ b/plugins/eos-updater/meson.build @@ -0,0 +1,28 @@ +cargs = ['-DG_LOG_DOMAIN="GsPluginEosUpdater"'] + +eos_updater_generated = gnome.gdbus_codegen( + 'gs-eos-updater-generated', + sources : ['com.endlessm.Updater.xml'], + interface_prefix : 'com.endlessm.', + namespace : 'GsEos', +) + +shared_module( + 'gs_plugin_eos-updater', + eos_updater_generated, + sources : 'gs-plugin-eos-updater.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : [ + plugin_libs, + ostree, + ], + link_with : [ + libgnomesoftware, + ], +) diff --git a/plugins/eos-updater/tests/eos_updater.py b/plugins/eos-updater/tests/eos_updater.py new file mode 100644 index 0000000..5e4aa8d --- /dev/null +++ b/plugins/eos-updater/tests/eos_updater.py @@ -0,0 +1,414 @@ +'''eos-updater mock template + +This creates a mock eos-updater interface (com.endlessm.Updater), with several +methods on the Mock sidecar interface which allow its internal state flow to be +controlled. + +A typical call chain for this would be: + - Test harness calls SetPollAction('update', {}, '', '') + - SUT calls Poll() + - Test harness calls FinishPoll() + - SUT calls Fetch() + - Test harness calls FinishFetch() + - SUT calls Apply() + - Test harness calls FinishApply() + +Errors can be simulated by specifying an `early-error` or `late-error` as the +action in a Set*Action() call. `early-error` will result in the associated +Poll() call (for example) transitioning to the error state. `late-error` will +result in a transition to the error state only once (for example) FinishPoll() +is called. + +See the implementation of each Set*Action() method for the set of actions it +supports. + +Usage: + python3 -m dbusmock \ + --template ./plugins/eos-updater/tests/mock-eos-updater.py +''' + +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. See http://www.gnu.org/copyleft/lgpl.html for the full +# text of the license. +# +# The LGPL 2.1+ has been chosen as that’s the license eos-updater is under. + +from enum import IntEnum +from gi.repository import GLib +import time + +import dbus +import dbus.mainloop.glib +from dbusmock import MOCK_IFACE + + +__author__ = 'Philip Withnall' +__email__ = 'withnall@endlessm.com' +__copyright__ = '© 2019 Endless Mobile Inc.' +__license__ = 'LGPL 2.1+' + + +class UpdaterState(IntEnum): + NONE = 0 + READY = 1 + ERROR = 2 + POLLING = 3 + UPDATE_AVAILABLE = 4 + FETCHING = 5 + UPDATE_READY = 6 + APPLYING_UPDATE = 7 + UPDATE_APPLIED = 8 + + +BUS_NAME = 'com.endlessm.Updater' +MAIN_OBJ = '/com/endlessm/Updater' +MAIN_IFACE = 'com.endlessm.Updater' +SYSTEM_BUS = True + + +dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) + + +def load(mock, parameters): + mock.AddProperties( + MAIN_IFACE, + dbus.Dictionary({ + 'State': dbus.UInt32(parameters.get('State', 1)), + 'UpdateID': dbus.String(parameters.get('UpdateID', '')), + 'UpdateRefspec': dbus.String(parameters.get('UpdateRefspec', '')), + 'OriginalRefspec': + dbus.String(parameters.get('OriginalRefspec', '')), + 'CurrentID': dbus.String(parameters.get('CurrentID', '')), + 'UpdateLabel': dbus.String(parameters.get('UpdateLabel', '')), + 'UpdateMessage': dbus.String(parameters.get('UpdateMessage', '')), + 'Version': dbus.String(parameters.get('Version', '')), + 'DownloadSize': dbus.Int64(parameters.get('DownloadSize', 0)), + 'DownloadedBytes': + dbus.Int64(parameters.get('DownloadedBytes', 0)), + 'UnpackedSize': dbus.Int64(parameters.get('UnpackedSize', 0)), + 'FullDownloadSize': + dbus.Int64(parameters.get('FullDownloadSize', 0)), + 'FullUnpackedSize': + dbus.Int64(parameters.get('FullUnpackedSize', 0)), + 'ErrorCode': dbus.UInt32(parameters.get('ErrorCode', 0)), + 'ErrorName': dbus.String(parameters.get('ErrorName', '')), + 'ErrorMessage': dbus.String(parameters.get('ErrorMessage', '')), + }, signature='sv')) + + # Set up initial state + mock.__poll_action = 'no-update' + mock.__fetch_action = 'success' + mock.__apply_action = 'success' + + # Set up private methods + mock.__set_properties = __set_properties + mock.__change_state = __change_state + mock.__set_error = __set_error + mock.__check_state = __check_state + + +# +# Internal utility methods +# + +# Values in @properties must have variant_level≥1 +def __set_properties(self, iface, properties): + for key, value in properties.items(): + self.props[iface][key] = value + self.EmitSignal(dbus.PROPERTIES_IFACE, 'PropertiesChanged', 'sa{sv}as', [ + iface, + properties, + [], + ]) + + +def __change_state(self, new_state): + props = { + 'State': dbus.UInt32(new_state, variant_level=1) + } + + # Reset error state if necessary. + if new_state != UpdaterState.ERROR and \ + self.props[MAIN_IFACE]['ErrorName'] != '': + props['ErrorCode'] = dbus.UInt32(0, variant_level=1) + props['ErrorName'] = dbus.String('', variant_level=1) + props['ErrorMessage'] = dbus.String('', variant_level=1) + + self.__set_properties(self, MAIN_IFACE, props) + self.EmitSignal(MAIN_IFACE, 'StateChanged', 'u', [dbus.UInt32(new_state)]) + + +def __set_error(self, error_name, error_message): + assert(error_name != '') + + self.__set_properties(self, MAIN_IFACE, { + 'ErrorName': dbus.String(error_name, variant_level=1), + 'ErrorMessage': dbus.String(error_message, variant_level=1), + 'ErrorCode': dbus.UInt32(1, variant_level=1), + }) + self.__change_state(self, UpdaterState.ERROR) + + +def __check_state(self, allowed_states): + if self.props[MAIN_IFACE]['State'] not in allowed_states: + raise dbus.exceptions.DBusException( + 'Call not allowed in this state', + name='com.endlessm.Updater.Error.WrongState') + + +# +# Updater methods which are too big for squeezing into AddMethod() +# + +@dbus.service.method(MAIN_IFACE, in_signature='', out_signature='') +def Poll(self): + self.__check_state(self, set([ + UpdaterState.READY, + UpdaterState.UPDATE_AVAILABLE, + UpdaterState.UPDATE_READY, + UpdaterState.ERROR, + ])) + + self.__change_state(self, UpdaterState.POLLING) + + if self.__poll_action == 'early-error': + time.sleep(0.5) + self.__set_error(self, self.__poll_error_name, + self.__poll_error_message) + else: + # we now expect the test harness to call FinishPoll() on the mock + # interface + pass + + +@dbus.service.method(MAIN_IFACE, in_signature='s', out_signature='') +def PollVolume(self, path): + # FIXME: Currently unsupported + return self.Poll() + + +@dbus.service.method(MAIN_IFACE, in_signature='', out_signature='') +def Fetch(self): + return self.FetchFull() + + +@dbus.service.method(MAIN_IFACE, in_signature='a{sv}', out_signature='') +def FetchFull(self, options=None): + self.__check_state(self, set([UpdaterState.UPDATE_AVAILABLE])) + + self.__change_state(self, UpdaterState.FETCHING) + + if self.__fetch_action == 'early-error': + time.sleep(0.5) + self.__set_error(self, self.__fetch_error_name, + self.__fetch_error_message) + else: + # we now expect the test harness to call FinishFetch() on the mock + # interface + pass + + +@dbus.service.method(MAIN_IFACE, in_signature='', out_signature='') +def Apply(self): + self.__check_state(self, set([UpdaterState.UPDATE_READY])) + + self.__change_state(self, UpdaterState.APPLYING_UPDATE) + + if self.__apply_action == 'early-error': + time.sleep(0.5) + self.__set_error(self, self.__apply_error_name, + self.__apply_error_message) + else: + # we now expect the test harness to call FinishApply() on the mock + # interface + pass + + +@dbus.service.method(MAIN_IFACE, in_signature='', out_signature='') +def Cancel(self): + self.__check_state(self, set([ + UpdaterState.POLLING, + UpdaterState.FETCHING, + UpdaterState.APPLYING_UPDATE, + ])) + + time.sleep(1) + self.__set_error(self, 'com.endlessm.Updater.Error.Cancelled', + 'Update was cancelled') + + +# +# Convenience methods on the mock +# + +@dbus.service.method(MOCK_IFACE, in_signature='sa{sv}ss', out_signature='') +def SetPollAction(self, action, update_properties, error_name, error_message): + '''Set the action to happen when the SUT calls Poll(). + + This sets the action which will happen when Poll() (and subsequently + FinishPoll()) are called, including the details of the error which will be + returned or the new update which will be advertised. + ''' + # Provide a default update. + if not update_properties: + update_properties = { + 'UpdateID': dbus.String('f' * 64, variant_level=1), + 'UpdateRefspec': + dbus.String('remote:new-refspec', variant_level=1), + 'OriginalRefspec': + dbus.String('remote:old-refspec', variant_level=1), + 'CurrentID': dbus.String('1' * 64, variant_level=1), + 'UpdateLabel': dbus.String('New OS Update', variant_level=1), + 'UpdateMessage': + dbus.String('Some release notes.', variant_level=1), + 'Version': dbus.String('3.7.0', variant_level=1), + 'DownloadSize': dbus.Int64(1000000000, variant_level=1), + 'UnpackedSize': dbus.Int64(1500000000, variant_level=1), + 'FullDownloadSize': dbus.Int64(1000000000 * 0.8, variant_level=1), + 'FullUnpackedSize': dbus.Int64(1500000000 * 0.8, variant_level=1), + } + + self.__poll_action = action + self.__poll_update_properties = update_properties + self.__poll_error_name = error_name + self.__poll_error_message = error_message + + +@dbus.service.method(MOCK_IFACE, in_signature='', out_signature='') +def FinishPoll(self): + self.__check_state(self, set([UpdaterState.POLLING])) + + if self.__poll_action == 'no-update': + self.__change_state(self, UpdaterState.READY) + elif self.__poll_action == 'update': + assert(set([ + 'UpdateID', + 'UpdateRefspec', + 'OriginalRefspec', + 'CurrentID', + 'UpdateLabel', + 'UpdateMessage', + 'Version', + 'FullDownloadSize', + 'FullUnpackedSize', + 'DownloadSize', + 'UnpackedSize', + ]) <= set(self.__poll_update_properties.keys())) + + # Set the initial DownloadedBytes based on whether we know the full + # download size. + props = self.__poll_update_properties + if props['DownloadSize'] < 0: + props['DownloadedBytes'] = dbus.Int64(-1, variant_level=1) + else: + props['DownloadedBytes'] = dbus.Int64(0, variant_level=1) + + self.__set_properties(self, MAIN_IFACE, props) + self.__change_state(self, UpdaterState.UPDATE_AVAILABLE) + elif self.__poll_action == 'early-error': + # Handled in Poll() itself. + pass + elif self.__poll_action == 'late-error': + self.__set_error(self, self.__poll_error_name, + self.__poll_error_message) + else: + assert(False) + + +@dbus.service.method(MOCK_IFACE, in_signature='sss', out_signature='') +def SetFetchAction(self, action, error_name, error_message): + '''Set the action to happen when the SUT calls Fetch(). + + This sets the action which will happen when Fetch() (and subsequently + FinishFetch()) are called, including the details of the error which will be + returned, if applicable. + ''' + self.__fetch_action = action + self.__fetch_error_name = error_name + self.__fetch_error_message = error_message + + +@dbus.service.method(MOCK_IFACE, in_signature='', out_signature='', + async_callbacks=('success_cb', 'error_cb')) +def FinishFetch(self, success_cb, error_cb): + '''Finish a pending client call to Fetch(). + + This is implemented using async_callbacks since if the fetch action is + ‘success’ it will block until the simulated download is complete, emitting + download progress signals throughout. As it’s implemented asynchronously, + this allows any calls to Cancel() to be handled by the mock service + part-way through the fetch. + ''' + self.__check_state(self, set([UpdaterState.FETCHING])) + + if self.__fetch_action == 'success': + # Simulate the download. + i = 0 + download_size = self.props[MAIN_IFACE]['DownloadSize'] + + def _download_progress_cb(): + nonlocal i + + # Allow cancellation. + if self.props[MAIN_IFACE]['State'] != UpdaterState.FETCHING: + return False + + downloaded_bytes = (i / 100.0) * download_size + self.__set_properties(self, MAIN_IFACE, { + 'DownloadedBytes': + dbus.Int64(downloaded_bytes, variant_level=1), + }) + + i += 1 + + # Keep looping until the download is complete. + if i <= 100: + return True + + # When the download is complete, change the service state and + # finish the asynchronous FinishFetch() call. + self.__change_state(self, UpdaterState.UPDATE_READY) + success_cb() + return False + + GLib.timeout_add(100, _download_progress_cb) + elif self.__fetch_action == 'early-error': + # Handled in Fetch() itself. + success_cb() + elif self.__fetch_action == 'late-error': + self.__set_error(self, self.__fetch_error_name, + self.__fetch_error_message) + success_cb() + else: + assert(False) + + +@dbus.service.method(MOCK_IFACE, in_signature='sss', out_signature='') +def SetApplyAction(self, action, error_name, error_message): + '''Set the action to happen when the SUT calls Apply(). + + This sets the action which will happen when Apply() (and subsequently + FinishApply()) are called, including the details of the error which will be + returned, if applicable. + ''' + self.__apply_action = action + self.__apply_error_name = error_name + self.__apply_error_message = error_message + + +@dbus.service.method(MOCK_IFACE, in_signature='', out_signature='') +def FinishApply(self): + self.__check_state(self, set([UpdaterState.APPLYING_UPDATE])) + + if self.__apply_action == 'success': + self.__change_state(self, UpdaterState.UPDATE_APPLIED) + elif self.__apply_action == 'early-error': + # Handled in Apply() itself. + pass + elif self.__apply_action == 'late-error': + self.__set_error(self, self.__apply_error_name, + self.__apply_error_message) + else: + assert(False) diff --git a/plugins/eos-updater/tests/manual-test.py b/plugins/eos-updater/tests/manual-test.py new file mode 100755 index 0000000..b6413d9 --- /dev/null +++ b/plugins/eos-updater/tests/manual-test.py @@ -0,0 +1,434 @@ +#!/usr/bin/python3 + +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1+ of the License, or (at your option) +# any later version. See http://www.gnu.org/copyleft/lgpl.html for the full +# text of the license. +# +# The LGPL 2.1+ has been chosen as that’s the license eos-updater is under. + + +from enum import IntEnum +import os +import time +import unittest +import dbus +import dbusmock +import ddt + + +__author__ = 'Philip Withnall' +__email__ = 'withnall@endlessm.com' +__copyright__ = '© 2019 Endless Mobile Inc.' +__license__ = 'LGPL 2.1+' + + +class UpdaterState(IntEnum): + '''eos-updater states; see its State property''' + NONE = 0 + READY = 1 + ERROR = 2 + POLLING = 3 + UPDATE_AVAILABLE = 4 + FETCHING = 5 + UPDATE_READY = 6 + APPLYING_UPDATE = 7 + UPDATE_APPLIED = 8 + + +@ddt.ddt +class ManualTest(dbusmock.DBusTestCase): + '''A manual test of the eos-updater plugin in gnome-software. + + It creates a mock eos-updater D-Bus daemon, on the real system bus (because + otherwise gnome-software’s other plugins can’t communicate with their + system daemons; to fix this, we’d need to mock those up too). The test + harness provides the user with instructions about how to run gnome-software + and what to do in it, waiting for them to press enter between steps. + + FIXME: This test could potentially eventually be automated by doing the UI + steps using Dogtail or OpenQA. + + It tests various classes of interaction between the plugin and the daemon: + normal update process (with and without an update available); error returns + from the daemon; cancellation of the daemon by another process; + cancellation of the daemon from gnome-software; and the daemon unexpectedly + going away (i.e. crashing). + ''' + + @classmethod + def setUpClass(cls): + # FIXME: See the comment below about why we currently run on the actual + # system bus. + # cls.start_system_bus() + cls.dbus_con = cls.get_dbus(True) + + def setUp(self): + # Work out the path to the dbusmock template in the same directory as + # this file. + self_path = os.path.dirname(os.path.realpath(__file__)) + template_path = os.path.join(self_path, 'eos_updater.py') + + # Spawn a python-dbusmock server. Use the actual system bus, since + # gnome-software needs to access various other services (such as + # packagekit) which we don’t currently mock (FIXME). + (self.p_mock, self.obj_eos_updater) = self.spawn_server_template( + template_path, {}, stdout=None) + self.dbusmock = dbus.Interface(self.obj_eos_updater, + dbusmock.MOCK_IFACE) + + def tearDown(self): + self.kill_gnome_software() + self.p_mock.terminate() + self.p_mock.wait() + + def launch_gnome_software(self): + '''Instruct the user to launch gnome-software''' + print('Launch gnome-software with:') + print('gnome-software --verbose') + self.manual_check('Press enter to continue') + + def kill_gnome_software(self): + '''Instruct the user to kill gnome-software''' + print('Kill gnome-software with:') + print('pkill gnome-software') + self.manual_check('Press enter to continue') + + def await_state(self, state): + '''Block until eos-updater reaches the given `state`''' + print('Awaiting state %u' % state) + props_iface = dbus.Interface(self.obj_eos_updater, + dbus.PROPERTIES_IFACE) + while props_iface.Get('com.endlessm.Updater', 'State') != state: + time.sleep(0.2) + + def manual_check(self, prompt): + '''Instruct the user to do a manual check and block until done''' + input('\033[92;1m' + prompt + '\033[0m\n') + + def test_poll_no_update(self): + '''Test that no updates are shown if eos-updater successfully says + there are none.''' + self.dbusmock.SetPollAction( + 'no-update', dbus.Dictionary({}, signature='sv'), '', '') + + self.launch_gnome_software() + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + + self.manual_check('Check there are no EOS updates listed') + self.await_state(UpdaterState.READY) + + @ddt.data('com.endlessm.Updater.Error.WrongState', + 'com.endlessm.Updater.Error.LiveBoot', + 'com.endlessm.Updater.Error.WrongConfiguration', + 'com.endlessm.Updater.Error.NotOstreeSystem', + 'com.endlessm.Updater.Error.Cancelled') + def test_poll_early_error(self, error_name): + '''Test that a D-Bus error return from Poll() is handled correctly.''' + self.dbusmock.SetPollAction( + 'early-error', dbus.Dictionary({}, signature='sv'), + error_name, 'Some error message.') + + self.launch_gnome_software() + self.await_state(UpdaterState.ERROR) + + if error_name != 'com.endlessm.Updater.Error.Cancelled': + self.manual_check('Check there are no EOS updates listed, and a ' + 'GsPluginEosUpdater error is printed on the ' + 'terminal') + else: + self.manual_check('Check there are no EOS updates listed, and no ' + 'GsPluginEosUpdater cancellation error is ' + 'printed on the terminal') + + @ddt.data('com.endlessm.Updater.Error.WrongState', + 'com.endlessm.Updater.Error.LiveBoot', + 'com.endlessm.Updater.Error.WrongConfiguration', + 'com.endlessm.Updater.Error.NotOstreeSystem', + 'com.endlessm.Updater.Error.Cancelled') + def test_poll_late_error(self, error_name): + '''Test that a transition to the Error state after successfully calling + Poll() is handled correctly.''' + self.dbusmock.SetPollAction( + 'late-error', dbus.Dictionary({}, signature='sv'), + error_name, 'Some error message.') + + self.launch_gnome_software() + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + + if error_name != 'com.endlessm.Updater.Error.Cancelled': + self.manual_check('Check there are no EOS updates listed, and a ' + 'GsPluginEosUpdater error is printed on the ' + 'terminal') + else: + self.manual_check('Check there are no EOS updates listed, and no ' + 'GsPluginEosUpdater cancellation error is ' + 'printed on the terminal') + self.await_state(UpdaterState.ERROR) + + @ddt.data(True, False) + def test_update_available(self, manually_refresh): + '''Test that the entire update process works if an update is + available.''' + self.dbusmock.SetPollAction( + 'update', dbus.Dictionary({}, signature='sv'), '', '') + self.dbusmock.SetFetchAction('success', '', '') + self.dbusmock.SetApplyAction('success', '', '') + + self.launch_gnome_software() + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + + if manually_refresh: + self.manual_check('Check an EOS update is listed; press the ' + 'Refresh button') + + # TODO: if you proceed through the test slowly, this sometimes doesn’t + # work + self.manual_check('Check an EOS update is listed; press the Download ' + 'button') + self.await_state(UpdaterState.FETCHING) + self.dbusmock.FinishFetch() + + self.manual_check('Check the download has paused at ~75% complete ' + '(waiting to apply)') + self.await_state(UpdaterState.APPLYING_UPDATE) + self.dbusmock.FinishApply() + + self.manual_check('Check the banner says to ‘Restart Now’ (don’t ' + 'click it)') + self.await_state(UpdaterState.UPDATE_APPLIED) + + @ddt.data('com.endlessm.Updater.Error.WrongState', + 'com.endlessm.Updater.Error.WrongConfiguration', + 'com.endlessm.Updater.Error.Fetching', + 'com.endlessm.Updater.Error.MalformedAutoinstallSpec', + 'com.endlessm.Updater.Error.UnknownEntryInAutoinstallSpec', + 'com.endlessm.Updater.Error.FlatpakRemoteConflict', + 'com.endlessm.Updater.Error.MeteredConnection', + 'com.endlessm.Updater.Error.Cancelled') + def test_fetch_early_error(self, error_name): + '''Test that a D-Bus error return from Fetch() is handled correctly.''' + self.dbusmock.SetPollAction( + 'update', dbus.Dictionary({}, signature='sv'), '', '') + self.dbusmock.SetFetchAction('early-error', error_name, + 'Some error or other.') + + self.launch_gnome_software() + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + + self.manual_check('Check an EOS update is listed; press the Download ' + 'button') + + if error_name != 'com.endlessm.Updater.Error.Cancelled': + self.manual_check('Check a fetch error is displayed') + else: + self.manual_check('Check no cancellation error is displayed') + + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + self.manual_check('Check an EOS update is listed again') + + @ddt.data('com.endlessm.Updater.Error.WrongState', + 'com.endlessm.Updater.Error.WrongConfiguration', + 'com.endlessm.Updater.Error.Fetching', + 'com.endlessm.Updater.Error.MalformedAutoinstallSpec', + 'com.endlessm.Updater.Error.UnknownEntryInAutoinstallSpec', + 'com.endlessm.Updater.Error.FlatpakRemoteConflict', + 'com.endlessm.Updater.Error.MeteredConnection', + 'com.endlessm.Updater.Error.Cancelled') + def test_fetch_late_error(self, error_name): + '''Test that a transition to the Error state after successfully calling + Fetch() is handled correctly.''' + self.dbusmock.SetPollAction( + 'update', dbus.Dictionary({}, signature='sv'), '', '') + self.dbusmock.SetFetchAction('late-error', error_name, + 'Some error or other.') + + self.launch_gnome_software() + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + + self.manual_check('Check an EOS update is listed; press the Download ' + 'button') + self.await_state(UpdaterState.FETCHING) + self.dbusmock.FinishFetch() + + self.await_state(UpdaterState.ERROR) + if error_name != 'com.endlessm.Updater.Error.Cancelled': + self.manual_check('Check a fetch error is displayed') + else: + self.manual_check('Check no cancellation error is displayed') + + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + self.manual_check('Check an EOS update is listed again') + + @ddt.data('com.endlessm.Updater.Error.WrongState', + 'com.endlessm.Updater.Error.WrongConfiguration', + 'com.endlessm.Updater.Error.Cancelled') + def test_apply_early_error(self, error_name): + '''Test that a D-Bus error return from Apply() is handled correctly.''' + self.dbusmock.SetPollAction( + 'update', dbus.Dictionary({}, signature='sv'), '', '') + self.dbusmock.SetFetchAction('success', '', '') + self.dbusmock.SetApplyAction('early-error', error_name, + 'Some error or other.') + + self.launch_gnome_software() + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + + self.manual_check('Check an EOS update is listed; press the Download ' + 'button') + self.await_state(UpdaterState.FETCHING) + self.dbusmock.FinishFetch() + + self.await_state(UpdaterState.ERROR) + if error_name != 'com.endlessm.Updater.Error.Cancelled': + self.manual_check('Check an apply error is displayed after the ' + 'update reached ~75% completion') + else: + self.manual_check('Check no cancellation error is displayed after ' + 'the update reached ~75% completion') + + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + self.manual_check('Check an EOS update is listed again') + + @ddt.data('com.endlessm.Updater.Error.WrongState', + 'com.endlessm.Updater.Error.WrongConfiguration', + 'com.endlessm.Updater.Error.Cancelled') + def test_apply_late_error(self, error_name): + '''Test that a transition to the Error state after successfully calling + Apply() is handled correctly.''' + self.dbusmock.SetPollAction( + 'update', dbus.Dictionary({}, signature='sv'), '', '') + self.dbusmock.SetFetchAction('success', '', '') + self.dbusmock.SetApplyAction('late-error', error_name, + 'Some error or other.') + + self.launch_gnome_software() + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + + self.manual_check('Check an EOS update is listed; press the Download ' + 'button') + self.await_state(UpdaterState.FETCHING) + self.dbusmock.FinishFetch() + + self.manual_check('Check the download has paused at ~75% complete ' + '(waiting to apply)') + self.await_state(UpdaterState.APPLYING_UPDATE) + self.dbusmock.FinishApply() + + self.await_state(UpdaterState.ERROR) + if error_name != 'com.endlessm.Updater.Error.Cancelled': + self.manual_check('Check an apply error is displayed') + else: + self.manual_check('Check no cancellation error is displayed') + + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + self.manual_check('Check an EOS update is listed again') + + def test_no_eos_updater_running(self): + '''Test that the plugin doesn’t make a fuss if eos-updater is + unavailable.''' + self.p_mock.kill() + + self.launch_gnome_software() + + self.manual_check('Check there are no EOS updates listed, and no ' + 'errors shown') + + def test_fetch_ui_cancellation(self): + '''Test that cancelling a download from the UI works correctly.''' + self.dbusmock.SetPollAction( + 'update', dbus.Dictionary({}, signature='sv'), '', '') + self.dbusmock.SetFetchAction('success', '', '') + + self.launch_gnome_software() + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + + self.manual_check('Check an EOS update is listed; press the Download ' + 'button, then shortly afterwards press the Cancel ' + 'button') + self.await_state(UpdaterState.FETCHING) + self.dbusmock.FinishFetch() + + self.await_state(UpdaterState.ERROR) + self.manual_check('Check a fetch cancellation error is displayed') + + def test_poll_eos_updater_dies(self): + '''Test that gnome-software recovers if eos-updater dies while + polling for updates.''' + self.dbusmock.SetPollAction( + 'update', dbus.Dictionary({}, signature='sv'), '', '') + + self.launch_gnome_software() + self.await_state(UpdaterState.POLLING) + self.p_mock.kill() + + self.manual_check('Check no error is shown for the poll failure') + self.setUp() + self.dbusmock.SetPollAction( + 'update', dbus.Dictionary({}, signature='sv'), '', '') + + self.manual_check('Press the Refresh button and check an update is ' + 'shown') + # TODO: It may take a few minutes for the update to appear on the + # updates page + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + + def test_fetch_eos_updater_dies(self): + '''Test that gnome-software recovers if eos-updater dies while + fetching an update.''' + self.dbusmock.SetPollAction( + 'update', dbus.Dictionary({}, signature='sv'), '', '') + self.dbusmock.SetFetchAction('success', '', '') + + self.launch_gnome_software() + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + + self.manual_check('Check an EOS update is listed; press the Download ' + 'button') + self.await_state(UpdaterState.FETCHING) + self.p_mock.kill() + + self.manual_check('Check an error is shown for the fetch failure') + + def test_apply_eos_updater_dies(self): + '''Test that gnome-software recovers if eos-updater dies while + applying an update.''' + self.dbusmock.SetPollAction( + 'update', dbus.Dictionary({}, signature='sv'), '', '') + self.dbusmock.SetFetchAction('success', '', '') + self.dbusmock.SetApplyAction('success', '', '') + + self.launch_gnome_software() + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + + self.manual_check('Check an EOS update is listed; press the Download ' + 'button') + self.await_state(UpdaterState.FETCHING) + self.dbusmock.FinishFetch() + + self.manual_check('Check the download has paused at ~75% complete ' + '(waiting to apply)') + self.await_state(UpdaterState.APPLYING_UPDATE) + self.p_mock.kill() + + self.manual_check('Check an error is shown for the apply failure') + + +if __name__ == '__main__': + unittest.main() diff --git a/plugins/external-appstream/gs-external-appstream-utils.c b/plugins/external-appstream/gs-external-appstream-utils.c new file mode 100644 index 0000000..7002810 --- /dev/null +++ b/plugins/external-appstream/gs-external-appstream-utils.c @@ -0,0 +1,27 @@ + /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2018 Endless Mobile, Inc. + * + * Authors: Joaquim Rocha <jrocha@endlessm.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "gs-external-appstream-utils.h" + +#define APPSTREAM_SYSTEM_DIR LOCALSTATEDIR "/cache/app-info/xmls" + +gchar * +gs_external_appstream_utils_get_file_cache_path (const gchar *file_name) +{ + g_autofree gchar *prefixed_file_name = g_strdup_printf ("org.gnome.Software-%s", + file_name); + return g_build_filename (APPSTREAM_SYSTEM_DIR, prefixed_file_name, NULL); +} + +const gchar * +gs_external_appstream_utils_get_system_dir (void) +{ + return APPSTREAM_SYSTEM_DIR; +} diff --git a/plugins/external-appstream/gs-external-appstream-utils.h b/plugins/external-appstream/gs-external-appstream-utils.h new file mode 100644 index 0000000..402c27c --- /dev/null +++ b/plugins/external-appstream/gs-external-appstream-utils.h @@ -0,0 +1,17 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2018 Endless Mobile, Inc. + * + * Authors: Joaquim Rocha <jrocha@endlessm.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <config.h> +#include <glib.h> + +const gchar *gs_external_appstream_utils_get_system_dir (void); +gchar *gs_external_appstream_utils_get_file_cache_path (const gchar *file_name); diff --git a/plugins/external-appstream/gs-install-appstream.c b/plugins/external-appstream/gs-install-appstream.c new file mode 100644 index 0000000..fb0d8fa --- /dev/null +++ b/plugins/external-appstream/gs-install-appstream.c @@ -0,0 +1,181 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2009-2016 Richard Hughes <richard@hughsie.com> + * + * Licensed under the GNU General Public License Version 2 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#include <locale.h> +#include <stdlib.h> +#include <sys/stat.h> + +#include <xmlb.h> +#include <glib/gi18n.h> + +#include "gs-external-appstream-utils.h" + +static gboolean +gs_install_appstream_copy_file (GFile *file, GError **error) +{ + g_autofree gchar *basename = g_file_get_basename (file); + g_autofree gchar *cachefn = gs_external_appstream_utils_get_file_cache_path (basename); + g_autoptr(GFile) cachefn_file = g_file_new_for_path (cachefn); + g_autoptr(GFile) cachedir_file = g_file_get_parent (cachefn_file); + + /* make sure the parent directory exists, but if not then create with + * the ownership and permissions of the current process */ + if (!g_file_query_exists (cachedir_file, NULL)) { + if (!g_file_make_directory_with_parents (cachedir_file, NULL, error)) + return FALSE; + } + + /* do the copy, overwriting existing files and setting the permissions + * of the current process (so that should be -rw-r--r--) */ + return g_file_copy (file, cachefn_file, + G_FILE_COPY_OVERWRITE | + G_FILE_COPY_NOFOLLOW_SYMLINKS | + G_FILE_COPY_TARGET_DEFAULT_PERMS, + NULL, NULL, NULL, error); +} + +static gboolean +gs_install_appstream_check_content_type (GFile *file, GError **error) +{ + const gchar *type; + g_autoptr(GError) error_local = NULL; + g_autoptr(GFileInfo) info = NULL; + g_autoptr(GPtrArray) components = NULL; + g_autoptr(XbBuilder) builder = xb_builder_new (); + g_autoptr(XbBuilderSource) source = xb_builder_source_new (); + g_autoptr(XbSilo) silo = NULL; + + /* check is correct type */ + info = g_file_query_info (file, + G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, + G_FILE_QUERY_INFO_NONE, + NULL, error); + if (info == NULL) + return FALSE; + type = g_file_info_get_attribute_string (info, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE); + if (g_strcmp0 (type, "application/gzip") != 0 && + g_strcmp0 (type, "application/xml") != 0) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "Invalid type %s: ", type); + return FALSE; + } + + /* check is an AppStream file */ + if (!xb_builder_source_load_file (source, file, + XB_BUILDER_SOURCE_FLAG_NONE, + NULL, &error_local)) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "Failed to import XML: %s", error_local->message); + return FALSE; + } + xb_builder_import_source (builder, source); + silo = xb_builder_compile (builder, + XB_BUILDER_COMPILE_FLAG_NONE, + NULL, &error_local); + if (silo == NULL) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "Failed to parse XML: %s", error_local->message); + return FALSE; + } + 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)) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "No applications found in the AppStream XML"); + return FALSE; + } + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "Failed to query XML: %s", error_local->message); + return FALSE; + } + + return TRUE; +} + +int +main (int argc, char *argv[]) +{ + g_autoptr(GError) error = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GOptionContext) context = NULL; + + /* setup translations */ + setlocale (LC_ALL, ""); + bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR); + bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); + textdomain (GETTEXT_PACKAGE); + + context = g_option_context_new (NULL); + /* TRANSLATORS: tool that is used when copying profiles system-wide */ + g_option_context_set_summary (context, _("GNOME Software AppStream system-wide installer")); + if (!g_option_context_parse (context, &argc, &argv, &error)) { + g_print ("%s\n", _("Failed to parse command line arguments")); + return EXIT_FAILURE; + } + + /* check input */ + if (g_strv_length (argv) != 2) { + /* TRANSLATORS: user did not specify a valid filename */ + g_print ("%s\n", _("You need to specify exactly one filename")); + return EXIT_FAILURE; + } + + /* check calling process */ + if (getuid () != 0 || geteuid () != 0) { + /* TRANSLATORS: only able to install files as root */ + g_print ("%s\n", _("This program can only be used by the root user")); + return EXIT_FAILURE; + } + + /* check content type for file */ + file = g_file_new_for_path (argv[1]); + if (!gs_install_appstream_check_content_type (file, &error)) { + /* TRANSLATORS: error details */ + g_print ("%s: %s\n", _("Failed to validate content type"), error->message); + return EXIT_FAILURE; + } + + /* Set the umask to ensure it is read-only to all users except root. */ + umask (022); + + /* do the copy */ + if (!gs_install_appstream_copy_file (file, &error)) { + /* TRANSLATORS: error details */ + g_print ("%s: %s\n", _("Failed to copy"), error->message); + return EXIT_FAILURE; + } + + /* success */ + return EXIT_SUCCESS; +} diff --git a/plugins/external-appstream/gs-plugin-external-appstream.c b/plugins/external-appstream/gs-plugin-external-appstream.c new file mode 100644 index 0000000..e94e0c5 --- /dev/null +++ b/plugins/external-appstream/gs-plugin-external-appstream.c @@ -0,0 +1,289 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016-2018 Endless Mobile, Inc. + * + * Authors: Joaquim Rocha <jrocha@endlessm.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> +#include <glib/gi18n.h> + +#include <gnome-software.h> +#include "gs-external-appstream-utils.h" + +struct GsPluginData { + GSettings *settings; +}; + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData)); + const gchar *system_dir = gs_external_appstream_utils_get_system_dir (); + + priv->settings = g_settings_new ("org.gnome.software"); + + /* run it before the appstream plugin */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_BEFORE, "appstream"); + + g_debug ("appstream system dir: %s", system_dir); +} + +void +gs_plugin_destroy (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_object_unref (priv->settings); +} + +static gboolean +gs_plugin_external_appstream_check (const gchar *appstream_path, + guint cache_age) +{ + g_autoptr(GFile) file = g_file_new_for_path (appstream_path); + guint appstream_file_age = gs_utils_get_file_age (file); + return appstream_file_age >= cache_age; +} + +static gboolean +gs_plugin_external_appstream_install (const gchar *appstream_file, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GSubprocess) subprocess = NULL; + const gchar *argv[] = { "pkexec", + LIBEXECDIR "/gnome-software-install-appstream", + appstream_file, NULL}; + g_debug ("Installing the appstream file %s in the system", + appstream_file); + subprocess = g_subprocess_newv (argv, + G_SUBPROCESS_FLAGS_STDOUT_PIPE | + G_SUBPROCESS_FLAGS_STDIN_PIPE, error); + if (subprocess == NULL) + return FALSE; + return g_subprocess_wait_check (subprocess, cancellable, error); +} + +static gchar * +gs_plugin_external_appstream_get_modification_date (const gchar *file_path) +{ +#ifndef GLIB_VERSION_2_62 + GTimeVal time_val; +#endif + g_autoptr(GDateTime) date_time = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GFileInfo) info = NULL; + + file = g_file_new_for_path (file_path); + info = g_file_query_info (file, + G_FILE_ATTRIBUTE_TIME_MODIFIED, + G_FILE_QUERY_INFO_NONE, + NULL, + NULL); + if (info == NULL) + return NULL; +#ifdef GLIB_VERSION_2_62 + date_time = g_file_info_get_modification_date_time (info); +#else + g_file_info_get_modification_time (info, &time_val); + date_time = g_date_time_new_from_timeval_local (&time_val); +#endif + return g_date_time_format (date_time, "%a, %d %b %Y %H:%M:%S %Z"); +} + +static gboolean +gs_plugin_external_appstream_refresh_sys (GsPlugin *plugin, + const gchar *url, + guint cache_age, + GCancellable *cancellable, + GError **error) +{ + GOutputStream *outstream = NULL; + SoupSession *soup_session; + guint status_code; + gboolean file_written; + g_autofree gchar *tmp_file_path = NULL; + g_autofree gchar *file_name = NULL; + g_autofree gchar *local_mod_date = NULL; + g_autofree gchar *target_file_path = NULL; + g_autofree gchar *tmp_file_tmpl = NULL; + g_autoptr(GFileIOStream) iostream = NULL; + g_autoptr(GFile) tmp_file = NULL; + g_autoptr(SoupMessage) msg = NULL; + + /* check age */ + file_name = g_path_get_basename (url); + target_file_path = gs_external_appstream_utils_get_file_cache_path (file_name); + if (!gs_plugin_external_appstream_check (target_file_path, cache_age)) { + g_debug ("skipping updating external appstream file %s: " + "cache age is older than file", + target_file_path); + return TRUE; + } + + msg = soup_message_new (SOUP_METHOD_GET, url); + + /* Set the If-Modified-Since header if the target file exists */ + local_mod_date = gs_plugin_external_appstream_get_modification_date (target_file_path); + if (local_mod_date != NULL) { + g_debug ("Requesting contents of %s if modified since %s", + url, local_mod_date); + soup_message_headers_append (msg->request_headers, + "If-Modified-Since", + local_mod_date); + } + + /* get the data */ + soup_session = gs_plugin_get_soup_session (plugin); + status_code = soup_session_send_message (soup_session, msg); + if (status_code != SOUP_STATUS_OK) { + if (status_code == SOUP_STATUS_NOT_MODIFIED) { + g_debug ("Not updating %s has not modified since %s", + target_file_path, local_mod_date); + return TRUE; + } + + g_set_error (error, GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_DOWNLOAD_FAILED, + "Failed to download appstream file %s: %s", + url, soup_status_get_phrase (status_code)); + return FALSE; + } + + /* write the download contents into a file that will be copied into + * the system */ + tmp_file_path = gs_utils_get_cache_filename ("external-appstream", + file_name, + GS_UTILS_CACHE_FLAG_WRITEABLE, + error); + if (tmp_file_path == NULL) + return FALSE; + + tmp_file = g_file_new_for_path (tmp_file_path); + + /* ensure the file doesn't exist */ + if (g_file_query_exists (tmp_file, cancellable) && + !g_file_delete (tmp_file, cancellable, error)) + return FALSE; + + iostream = g_file_create_readwrite (tmp_file, G_FILE_CREATE_NONE, + cancellable, error); + + if (iostream == NULL) + return FALSE; + + g_debug ("Downloaded appstream file %s", tmp_file_path); + + /* write to file */ + outstream = g_io_stream_get_output_stream (G_IO_STREAM (iostream)); + file_written = g_output_stream_write_all (outstream, + msg->response_body->data, + msg->response_body->length, + NULL, cancellable, error); + + /* close the file */ + g_output_stream_close (outstream, cancellable, NULL); + + /* install file systemwide */ + if (file_written) { + if (gs_plugin_external_appstream_install (tmp_file_path, + cancellable, + error)) { + g_debug ("Installed appstream file %s", tmp_file_path); + } else { + file_written = FALSE; + } + } + + /* clean up the temporary file */ + g_file_delete (tmp_file, cancellable, NULL); + return file_written; +} + +static gboolean +gs_plugin_external_appstream_refresh_user (GsPlugin *plugin, + const gchar *url, + guint cache_age, + GCancellable *cancellable, + GError **error) +{ + guint file_age; + g_autofree gchar *basename = NULL; + g_autofree gchar *fullpath = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GsApp) app_dl = gs_app_new (gs_plugin_get_name (plugin)); + + /* check age */ + basename = g_path_get_basename (url); + fullpath = g_build_filename (g_get_user_data_dir (), + "app-info", + "xmls", + basename, + NULL); + file = g_file_new_for_path (fullpath); + file_age = gs_utils_get_file_age (file); + if (file_age < cache_age) { + g_debug ("skipping %s: cache age is older than file", fullpath); + return TRUE; + } + + /* download file */ + gs_app_set_summary_missing (app_dl, + /* TRANSLATORS: status text when downloading */ + _("Downloading extra metadata files…")); + return gs_plugin_download_file (plugin, app_dl, url, fullpath, + cancellable, error); +} + +static gboolean +gs_plugin_external_appstream_refresh_url (GsPlugin *plugin, + const gchar *url, + guint cache_age, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + if (g_settings_get_strv (priv->settings, "external-appstream-urls")) { + return gs_plugin_external_appstream_refresh_sys (plugin, url, + cache_age, + cancellable, + error); + } + return gs_plugin_external_appstream_refresh_user (plugin, url, cache_age, + cancellable, error); +} + +gboolean +gs_plugin_refresh (GsPlugin *plugin, + guint cache_age, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_auto(GStrv) appstream_urls = NULL; + + appstream_urls = g_settings_get_strv (priv->settings, + "external-appstream-urls"); + for (guint i = 0; appstream_urls[i] != NULL; ++i) { + g_autoptr(GError) error_local = NULL; + if (!g_str_has_prefix (appstream_urls[i], "https")) { + g_warning ("Not considering %s as an external " + "appstream source: please use an https URL", + appstream_urls[i]); + continue; + } + if (!gs_plugin_external_appstream_refresh_url (plugin, + appstream_urls[i], + cache_age, + cancellable, + &error_local)) { + g_warning ("Failed to update external appstream file: %s", + error_local->message); + } + } + + return TRUE; +} diff --git a/plugins/external-appstream/meson.build b/plugins/external-appstream/meson.build new file mode 100644 index 0000000..2252c7d --- /dev/null +++ b/plugins/external-appstream/meson.build @@ -0,0 +1,38 @@ +cargs = ['-DG_LOG_DOMAIN="GsPluginExternalAppstream"'] + +executable( + 'gnome-software-install-appstream', + sources : [ + 'gs-external-appstream-utils.c', + 'gs-install-appstream.c', + ], + include_directories : [ + include_directories('../..'), + ], + dependencies : [ + gio_unix, + libxmlb, + ], + c_args : cargs, + install : true, + install_dir : get_option('libexecdir') +) + +shared_module( + 'gs_plugin_external-appstream', + sources : [ + 'gs-external-appstream-utils.c', + 'gs-plugin-external-appstream.c', + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : [gio_unix, appstream_glib, libsoup, plugin_libs], + link_with : [ + libgnomesoftware + ] +) diff --git a/plugins/fedora-langpacks/gs-plugin-fedora-langpacks.c b/plugins/fedora-langpacks/gs-plugin-fedora-langpacks.c new file mode 100644 index 0000000..ea673b1 --- /dev/null +++ b/plugins/fedora-langpacks/gs-plugin-fedora-langpacks.c @@ -0,0 +1,101 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2019 Sundeep Anand <suanand@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + * + * This plugin does following.. + * 1. locates the active locale, say, xx + * 2. identifies related langpacks-xx + * 3. tries to add langpack-xx in app list + * 4. logs install information; not to try again + */ + +#include <gnome-software.h> + +struct GsPluginData { + GHashTable *locale_langpack_map; +}; + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData)); + + /* this plugin should be fedora specific */ + if (!gs_plugin_check_distro_id (plugin, "fedora")) { + gs_plugin_set_enabled (plugin, FALSE); + return; + } + + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "appstream"); + + /* + * A few language code may have more than one language packs. + * Example: en {en_GB}, pt {pt_BR}, zh {zh_CN, zh_TW} + */ + priv->locale_langpack_map = g_hash_table_new (g_str_hash, g_str_equal); + g_hash_table_insert (priv->locale_langpack_map, "en_GB", "langpacks-en_GB"); + g_hash_table_insert (priv->locale_langpack_map, "pt_BR", "langpacks-pt_BR"); + g_hash_table_insert (priv->locale_langpack_map, "zh_CN", "langpacks-zh_CN"); + g_hash_table_insert (priv->locale_langpack_map, "zh_TW", "langpacks-zh_TW"); +} + +void +gs_plugin_destroy (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + if (priv->locale_langpack_map != NULL) + g_hash_table_unref (priv->locale_langpack_map); +} + +gboolean +gs_plugin_add_langpacks (GsPlugin *plugin, + GsAppList *list, + const gchar *locale, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + const gchar *language_code; + g_autofree gchar *cachefn = NULL; + g_autofree gchar *langpack_pkgname = NULL; + g_auto(GStrv) language_region = NULL; + + if (g_strrstr (locale, "_") != NULL && + !g_hash_table_lookup (priv->locale_langpack_map, locale)) { + /* + * language_code should be the langpack_source_id + * if input language_code is a locale and it doesn't + * not found in locale_langpack_map + */ + language_region = g_strsplit (locale, "_", 2); + language_code = language_region[0]; + } else { + language_code = locale; + } + + /* per-user cache */ + langpack_pkgname = g_strconcat ("langpacks-", language_code, NULL); + cachefn = gs_utils_get_cache_filename ("langpacks", langpack_pkgname, + GS_UTILS_CACHE_FLAG_WRITEABLE, + error); + if (cachefn == NULL) + return FALSE; + if (!g_file_test (cachefn, G_FILE_TEST_EXISTS)) { + g_autoptr(GsApp) app = gs_app_new (NULL); + gs_app_set_metadata (app, "GnomeSoftware::Creator", gs_plugin_get_name (plugin)); + gs_app_set_kind (app, AS_APP_KIND_LOCALIZATION); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_scope (app, AS_APP_SCOPE_SYSTEM); + gs_app_add_source (app, langpack_pkgname); + gs_app_list_add (list, app); + + /* ensure we do not keep trying to install the langpack */ + if (!g_file_set_contents (cachefn, language_code, -1, error)) + return FALSE; + } + + return TRUE; +} diff --git a/plugins/fedora-langpacks/gs-self-test.c b/plugins/fedora-langpacks/gs-self-test.c new file mode 100644 index 0000000..ac8c176 --- /dev/null +++ b/plugins/fedora-langpacks/gs-self-test.c @@ -0,0 +1,88 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2019 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gstdio.h> + +#include "gnome-software-private.h" + +#include "gs-test.h" + +static void +gs_plugins_fedora_langpacks_func (GsPluginLoader *plugin_loader) +{ + g_autofree gchar *cachefn = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* start with a clean slate */ + cachefn = gs_utils_get_cache_filename ("langpacks", "langpacks-ja", + GS_UTILS_CACHE_FLAG_WRITEABLE, + &error); + g_assert_no_error (error); + g_unlink (cachefn); + + /* get langpacks result based on locale */ + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_LANGPACKS, + "search", "ja_JP", + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + NULL); + list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + g_assert_nonnull (list); + g_assert_no_error (error); + + /* check if we have just one app in the list */ + g_assert_cmpint (gs_app_list_length (list), ==, 1); + + /* check app's source and kind */ + app = gs_app_list_index (list, 0); + g_assert_cmpstr (gs_app_get_source_default (app), ==, "langpacks-ja"); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_APP_KIND_LOCALIZATION); +} + +int +main (int argc, char **argv) +{ + gboolean ret; + g_autoptr(GError) error = NULL; + g_autoptr(GsPluginLoader) plugin_loader = NULL; + const gchar *allowlist[] = { + "fedora-langpacks", + NULL + }; + + 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); + + /* 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/fedora-langpacks", + plugin_loader, + (GTestDataFunc) gs_plugins_fedora_langpacks_func); + return g_test_run (); +} diff --git a/plugins/fedora-langpacks/meson.build b/plugins/fedora-langpacks/meson.build new file mode 100644 index 0000000..cdc3757 --- /dev/null +++ b/plugins/fedora-langpacks/meson.build @@ -0,0 +1,40 @@ +cargs = ['-DG_LOG_DOMAIN="GsPluginFedoraLangpacks"'] + +shared_module( + 'gs_plugin_fedora-langpacks', + sources : 'gs-plugin-fedora-langpacks.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : plugin_libs, + link_with : [ + libgnomesoftware + ] +) + +if get_option('tests') + cargs += ['-DLOCALPLUGINDIR="' + meson.current_build_dir() + '"'] + e = executable( + 'gs-self-test-fedora-langpacks', + compiled_schemas, + sources : [ + 'gs-self-test.c', + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + dependencies : [ + plugin_libs, + ], + link_with : [ + libgnomesoftware + ], + c_args : cargs, + ) + test('gs-self-test-fedora-langpacks', e, suite: ['plugins', 'fedora-langpacks'], env: test_env) +endif diff --git a/plugins/fedora-pkgdb-collections/gs-plugin-fedora-pkgdb-collections.c b/plugins/fedora-pkgdb-collections/gs-plugin-fedora-pkgdb-collections.c new file mode 100644 index 0000000..a103f7f --- /dev/null +++ b/plugins/fedora-pkgdb-collections/gs-plugin-fedora-pkgdb-collections.c @@ -0,0 +1,569 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016-2018 Kalev Lember <klember@redhat.com> + * Copyright (C) 2017 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <glib/gi18n.h> +#include <json-glib/json-glib.h> +#include <gnome-software.h> + +#define FEDORA_PKGDB_COLLECTIONS_API_URI "https://admin.fedoraproject.org/pkgdb/api/collections/" + +struct GsPluginData { + gchar *cachefn; + GFileMonitor *cachefn_monitor; + gchar *os_name; + guint64 os_version; + GsApp *cached_origin; + GSettings *settings; + gboolean is_valid; + GPtrArray *distros; + GMutex mutex; +}; + +typedef enum { + PKGDB_ITEM_STATUS_ACTIVE, + PKGDB_ITEM_STATUS_DEVEL, + PKGDB_ITEM_STATUS_EOL, + PKGDB_ITEM_STATUS_LAST +} PkgdbItemStatus; + +typedef struct { + gchar *name; + PkgdbItemStatus status; + guint version; +} PkgdbItem; + +static void +_pkgdb_item_free (PkgdbItem *item) +{ + g_free (item->name); + g_slice_free (PkgdbItem, item); +} + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData)); + + g_mutex_init (&priv->mutex); + + /* check that we are running on Fedora */ + if (!gs_plugin_check_distro_id (plugin, "fedora")) { + gs_plugin_set_enabled (plugin, FALSE); + g_debug ("disabling '%s' as we're not Fedora", gs_plugin_get_name (plugin)); + return; + } + priv->distros = g_ptr_array_new_with_free_func ((GDestroyNotify) _pkgdb_item_free); + priv->settings = g_settings_new ("org.gnome.software"); + + /* require the GnomeSoftware::CpeName metadata */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "os-release"); + + /* old name */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_CONFLICTS, "fedora-distro-upgrades"); +} + +void +gs_plugin_destroy (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + if (priv->cachefn_monitor != NULL) + g_object_unref (priv->cachefn_monitor); + if (priv->cached_origin != NULL) + g_object_unref (priv->cached_origin); + if (priv->settings != NULL) + g_object_unref (priv->settings); + if (priv->distros != NULL) + g_ptr_array_unref (priv->distros); + g_free (priv->os_name); + g_free (priv->cachefn); + g_mutex_clear (&priv->mutex); +} + +static void +_file_changed_cb (GFileMonitor *monitor, + GFile *file, GFile *other_file, + GFileMonitorEvent event_type, + gpointer user_data) +{ + GsPlugin *plugin = GS_PLUGIN (user_data); + GsPluginData *priv = gs_plugin_get_data (plugin); + + g_debug ("cache file changed, so reloading upgrades list"); + gs_plugin_updates_changed (plugin); + priv->is_valid = FALSE; +} + +gboolean +gs_plugin_setup (GsPlugin *plugin, GCancellable *cancellable, GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + const gchar *verstr = NULL; + gchar *endptr = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GsOsRelease) os_release = NULL; + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex); + + /* get the file to cache */ + priv->cachefn = gs_utils_get_cache_filename ("fedora-pkgdb-collections", + "fedora.json", + GS_UTILS_CACHE_FLAG_WRITEABLE, + error); + if (priv->cachefn == NULL) + return FALSE; + + /* watch this in case it is changed by the user */ + file = g_file_new_for_path (priv->cachefn); + priv->cachefn_monitor = g_file_monitor (file, + G_FILE_MONITOR_NONE, + cancellable, + error); + if (priv->cachefn_monitor == NULL) + return FALSE; + g_signal_connect (priv->cachefn_monitor, "changed", + G_CALLBACK (_file_changed_cb), plugin); + + /* read os-release for the current versions */ + os_release = gs_os_release_new (error); + if (os_release == NULL) + return FALSE; + priv->os_name = g_strdup (gs_os_release_get_name (os_release)); + if (priv->os_name == NULL) + return FALSE; + verstr = gs_os_release_get_version_id (os_release); + if (verstr == NULL) + return FALSE; + + /* parse the version */ + priv->os_version = g_ascii_strtoull (verstr, &endptr, 10); + if (endptr == verstr || priv->os_version > G_MAXUINT) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "Failed parse VERSION_ID: %s", verstr); + return FALSE; + } + + /* add source */ + priv->cached_origin = gs_app_new (gs_plugin_get_name (plugin)); + gs_app_set_kind (priv->cached_origin, AS_APP_KIND_SOURCE); + gs_app_set_origin_hostname (priv->cached_origin, + FEDORA_PKGDB_COLLECTIONS_API_URI); + + /* add the source to the plugin cache which allows us to match the + * unique ID to a GsApp when creating an event */ + gs_plugin_cache_add (plugin, + gs_app_get_unique_id (priv->cached_origin), + priv->cached_origin); + + /* success */ + return TRUE; +} + +static gboolean +_refresh_cache (GsPlugin *plugin, + guint cache_age, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GsApp) app_dl = gs_app_new (gs_plugin_get_name (plugin)); + + /* check cache age */ + if (cache_age > 0) { + g_autoptr(GFile) file = g_file_new_for_path (priv->cachefn); + guint tmp = gs_utils_get_file_age (file); + if (tmp < cache_age) { + g_debug ("%s is only %u seconds old", + priv->cachefn, tmp); + return TRUE; + } + } + + /* download new file */ + gs_app_set_summary_missing (app_dl, + /* TRANSLATORS: status text when downloading */ + _("Downloading upgrade information…")); + if (!gs_plugin_download_file (plugin, app_dl, + FEDORA_PKGDB_COLLECTIONS_API_URI, + priv->cachefn, + cancellable, + error)) { + gs_utils_error_add_origin_id (error, priv->cached_origin); + return FALSE; + } + + /* success */ + priv->is_valid = FALSE; + return TRUE; +} + +gboolean +gs_plugin_refresh (GsPlugin *plugin, + guint cache_age, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex); + return _refresh_cache (plugin, cache_age, cancellable, error); +} + +static gchar * +_get_upgrade_css_background (guint version) +{ + g_autofree gchar *filename1 = NULL; + g_autofree gchar *filename2 = NULL; + + filename1 = g_strdup_printf ("/usr/share/backgrounds/f%u/default/standard/f%u.png", version, version); + if (g_file_test (filename1, G_FILE_TEST_EXISTS)) + return g_strdup_printf ("url('%s')", filename1); + + filename2 = g_strdup_printf ("/usr/share/gnome-software/backgrounds/f%u.png", version); + if (g_file_test (filename2, G_FILE_TEST_EXISTS)) + return g_strdup_printf ("url('%s')", filename2); + + return NULL; +} + +static gint +_sort_items_cb (gconstpointer a, gconstpointer b) +{ + PkgdbItem *item_a = *((PkgdbItem **) a); + PkgdbItem *item_b = *((PkgdbItem **) b); + + if (item_a->version > item_b->version) + return 1; + if (item_a->version < item_b->version) + return -1; + return 0; +} + +static GsApp * +_create_upgrade_from_info (GsPlugin *plugin, PkgdbItem *item) +{ + GsApp *app; + g_autofree gchar *app_id = NULL; + g_autofree gchar *app_version = NULL; + g_autofree gchar *background = NULL; + g_autofree gchar *cache_key = NULL; + g_autofree gchar *css = NULL; + g_autofree gchar *url = NULL; + g_autoptr(AsIcon) ic = NULL; + + /* search in the cache */ + cache_key = g_strdup_printf ("release-%u", item->version); + app = gs_plugin_cache_lookup (plugin, cache_key); + if (app != NULL) + return app; + + app_id = g_strdup_printf ("org.fedoraproject.Fedora-%u", item->version); + app_version = g_strdup_printf ("%u", item->version); + + /* icon from disk */ + ic = as_icon_new (); + as_icon_set_kind (ic, AS_ICON_KIND_LOCAL); + as_icon_set_filename (ic, "/usr/share/pixmaps/fedora-logo-sprite.png"); + + /* create */ + app = gs_app_new (app_id); + gs_app_set_state (app, AS_APP_STATE_AVAILABLE); + gs_app_set_kind (app, AS_APP_KIND_OS_UPGRADE); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_name (app, GS_APP_QUALITY_LOWEST, item->name); + gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, + /* TRANSLATORS: this is a title for Fedora distro upgrades */ + _("Upgrade for the latest features, performance and stability improvements.")); + gs_app_set_version (app, app_version); + gs_app_set_size_installed (app, 1024 * 1024 * 1024); /* estimate */ + gs_app_set_size_download (app, 256 * 1024 * 1024); /* estimate */ + gs_app_set_license (app, GS_APP_QUALITY_LOWEST, "LicenseRef-free"); + gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT); + gs_app_add_quirk (app, GS_APP_QUIRK_PROVENANCE); + gs_app_add_quirk (app, GS_APP_QUIRK_NOT_REVIEWABLE); + gs_app_add_icon (app, ic); + + /* show a Fedora magazine article for the release */ + url = g_strdup_printf ("https://fedoramagazine.org/whats-new-fedora-%u-workstation", + item->version); + gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, url); + + /* use a fancy background if possible */ + background = _get_upgrade_css_background (item->version); + if (background != NULL) { + css = g_strdup_printf ("background: %s;" + "background-position: top;" + "background-size: cover;" + "color: white; text-shadow: 0 2px 2px rgba(0,0,0,0.5);", + background); + gs_app_set_metadata (app, "GnomeSoftware::UpgradeBanner-css", css); + } + + /* save in the cache */ + gs_plugin_cache_add (plugin, cache_key, app); + + /* success */ + return app; +} + +static gboolean +_is_valid_upgrade (GsPlugin *plugin, PkgdbItem *item) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + + /* only interested in upgrades to the same distro */ + if (g_strcmp0 (item->name, priv->os_name) != 0) + return FALSE; + + /* only interested in newer versions, but not more than N+2 */ + if (item->version <= priv->os_version || + item->version > priv->os_version + 2) + return FALSE; + + /* only interested in non-devel distros */ + if (!g_settings_get_boolean (priv->settings, "show-upgrade-prerelease")) { + if (item->status == PKGDB_ITEM_STATUS_DEVEL) + return FALSE; + } + + /* success */ + return TRUE; +} + +static gboolean +_ensure_cache (GsPlugin *plugin, GCancellable *cancellable, GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + JsonArray *collections; + JsonObject *root; +#if !JSON_CHECK_VERSION(1, 6, 0) + gsize len; + g_autofree gchar *data = NULL; +#endif /* json-glib < 1.6.0 */ + g_autoptr(JsonParser) parser = NULL; + + /* already done */ + if (priv->is_valid) + return TRUE; + + /* just ensure there is any data, no matter how old */ + if (!_refresh_cache (plugin, G_MAXUINT, cancellable, error)) + return FALSE; + +#if JSON_CHECK_VERSION(1, 6, 0) + parser = json_parser_new_immutable (); + if (!json_parser_load_from_mapped_file (parser, priv->cachefn, error)) + return FALSE; +#else /* if json-glib < 1.6.0 */ + /* get cached file */ + if (!g_file_get_contents (priv->cachefn, &data, &len, error)) { + gs_utils_error_convert_gio (error); + return FALSE; + } + + /* parse data */ + parser = json_parser_new (); + if (!json_parser_load_from_data (parser, data, len, error)) + return FALSE; +#endif /* json-glib < 1.6.0 */ + + root = json_node_get_object (json_parser_get_root (parser)); + if (root == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "no root object"); + return FALSE; + } + + collections = json_object_get_array_member (root, "collections"); + if (collections == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "no collections object"); + return FALSE; + } + + g_ptr_array_set_size (priv->distros, 0); + for (guint i = 0; i < json_array_get_length (collections); i++) { + PkgdbItem *item; + JsonObject *collection; + PkgdbItemStatus status; + const gchar *name; + const gchar *status_str; + const gchar *version_str; + gchar *endptr = NULL; + guint64 version; + + collection = json_array_get_object_element (collections, i); + if (collection == NULL) + continue; + + name = json_object_get_string_member (collection, "name"); + if (name == NULL) + continue; + + status_str = json_object_get_string_member (collection, "status"); + if (status_str == NULL) + continue; + + if (g_strcmp0 (status_str, "Active") == 0) + status = PKGDB_ITEM_STATUS_ACTIVE; + else if (g_strcmp0 (status_str, "Under Development") == 0) + status = PKGDB_ITEM_STATUS_DEVEL; + else if (g_strcmp0 (status_str, "EOL") == 0) + status = PKGDB_ITEM_STATUS_EOL; + else + continue; + + version_str = json_object_get_string_member (collection, "version"); + if (version_str == NULL) + continue; + + version = g_ascii_strtoull (version_str, &endptr, 10); + if (endptr == version_str || version > G_MAXUINT) + continue; + + /* add item */ + item = g_slice_new0 (PkgdbItem); + item->name = g_strdup (name); + item->status = status; + item->version = (guint) version; + g_ptr_array_add (priv->distros, item); + } + + /* ensure in correct order */ + g_ptr_array_sort (priv->distros, _sort_items_cb); + + /* success */ + priv->is_valid = TRUE; + return TRUE; +} + +static PkgdbItem * +_get_item_by_cpe_name (GsPlugin *plugin, const gchar *cpe_name) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + guint64 version; + g_auto(GStrv) split = NULL; + + /* split up 'cpe:/o:fedoraproject:fedora:26' to sections */ + split = g_strsplit (cpe_name, ":", -1); + if (g_strv_length (split) < 5) { + g_warning ("CPE invalid format: %s", cpe_name); + return NULL; + } + + /* find the correct collection */ + version = g_ascii_strtoull (split[4], NULL, 10); + if (version == 0) { + g_warning ("failed to parse CPE version: %s", split[4]); + return NULL; + } + for (guint i = 0; i < priv->distros->len; i++) { + PkgdbItem *item = g_ptr_array_index (priv->distros, i); + if (g_ascii_strcasecmp (item->name, split[3]) == 0 && + item->version == version) + return item; + } + return NULL; +} + +gboolean +gs_plugin_add_distro_upgrades (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex); + + /* ensure valid data is loaded */ + if (!_ensure_cache (plugin, cancellable, error)) + return FALSE; + + /* are any distros upgradable */ + for (guint i = 0; i < priv->distros->len; i++) { + PkgdbItem *item = g_ptr_array_index (priv->distros, i); + if (_is_valid_upgrade (plugin, item)) { + g_autoptr(GsApp) app = NULL; + app = _create_upgrade_from_info (plugin, item); + gs_app_list_add (list, app); + } + } + + return TRUE; +} + +static gboolean +refine_app_locked (GsPlugin *plugin, + GsApp *app, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + PkgdbItem *item; + const gchar *cpe_name; + + /* not for us */ + if (gs_app_get_kind (app) != AS_APP_KIND_OS_UPGRADE) + return TRUE; + + /* not enough metadata */ + cpe_name = gs_app_get_metadata_item (app, "GnomeSoftware::CpeName"); + if (cpe_name == NULL) + return TRUE; + + /* find item */ + item = _get_item_by_cpe_name (plugin, cpe_name); + if (item == NULL) { + g_warning ("did not find %s", cpe_name); + return TRUE; + } + + /* fix the state */ + switch (item->status) { + case PKGDB_ITEM_STATUS_ACTIVE: + case PKGDB_ITEM_STATUS_DEVEL: + gs_app_set_state (app, AS_APP_STATE_UPDATABLE); + break; + case PKGDB_ITEM_STATUS_EOL: + gs_app_set_state (app, AS_APP_STATE_UNAVAILABLE); + break; + default: + break; + } + + return TRUE; +} + +gboolean +gs_plugin_refine (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex); + + /* ensure valid data is loaded */ + if (!_ensure_cache (plugin, cancellable, error)) + return FALSE; + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + if (!refine_app_locked (plugin, app, flags, cancellable, error)) + return FALSE; + } + + return TRUE; +} diff --git a/plugins/fedora-pkgdb-collections/meson.build b/plugins/fedora-pkgdb-collections/meson.build new file mode 100644 index 0000000..ae093ba --- /dev/null +++ b/plugins/fedora-pkgdb-collections/meson.build @@ -0,0 +1,17 @@ +cargs = ['-DG_LOG_DOMAIN="GsPluginFedoraPkgdbCollections"'] + +shared_module( + 'gs_plugin_fedora-pkgdb-collections', + sources : 'gs-plugin-fedora-pkgdb-collections.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : plugin_libs, + link_with : [ + libgnomesoftware + ] +) diff --git a/plugins/flatpak/gs-appstream.c b/plugins/flatpak/gs-appstream.c new file mode 120000 index 0000000..96326ab --- /dev/null +++ b/plugins/flatpak/gs-appstream.c @@ -0,0 +1 @@ +../core/gs-appstream.c
\ No newline at end of file diff --git a/plugins/flatpak/gs-appstream.h b/plugins/flatpak/gs-appstream.h new file mode 120000 index 0000000..4eabcb3 --- /dev/null +++ b/plugins/flatpak/gs-appstream.h @@ -0,0 +1 @@ +../core/gs-appstream.h
\ No newline at end of file diff --git a/plugins/flatpak/gs-flatpak-app.c b/plugins/flatpak/gs-flatpak-app.c new file mode 100644 index 0000000..cf98248 --- /dev/null +++ b/plugins/flatpak/gs-flatpak-app.c @@ -0,0 +1,178 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2017-2018 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <string.h> + +#include "gs-flatpak-app.h" + +const gchar * +gs_flatpak_app_get_ref_name (GsApp *app) +{ + return gs_app_get_metadata_item (app, "flatpak::RefName"); +} + +const gchar * +gs_flatpak_app_get_ref_arch (GsApp *app) +{ + return gs_app_get_metadata_item (app, "flatpak::RefArch"); +} + +const gchar * +gs_flatpak_app_get_commit (GsApp *app) +{ + return gs_app_get_metadata_item (app, "flatpak::Commit"); +} + +GsFlatpakAppFileKind +gs_flatpak_app_get_file_kind (GsApp *app) +{ + GVariant *tmp = gs_app_get_metadata_variant (app, "flatpak::FileKind"); + if (tmp == NULL) + return GS_FLATPAK_APP_FILE_KIND_UNKNOWN; + return g_variant_get_uint32 (tmp); +} + +const gchar * +gs_flatpak_app_get_runtime_url (GsApp *app) +{ + return gs_app_get_metadata_item (app, "flatpak::RuntimeUrl"); +} + +FlatpakRefKind +gs_flatpak_app_get_ref_kind (GsApp *app) +{ + GVariant *tmp = gs_app_get_metadata_variant (app, "flatpak::RefKind"); + if (tmp == NULL) + return FLATPAK_REF_KIND_APP; + return g_variant_get_uint32 (tmp); +} + +const gchar * +gs_flatpak_app_get_ref_kind_as_str (GsApp *app) +{ + FlatpakRefKind ref_kind = gs_flatpak_app_get_ref_kind (app); + if (ref_kind == FLATPAK_REF_KIND_APP) + return "app"; + if (ref_kind == FLATPAK_REF_KIND_RUNTIME) + return "runtime"; + return NULL; +} + +const gchar * +gs_flatpak_app_get_object_id (GsApp *app) +{ + return gs_app_get_metadata_item (app, "flatpak::ObjectID"); +} + +const gchar * +gs_flatpak_app_get_repo_gpgkey (GsApp *app) +{ + return gs_app_get_metadata_item (app, "flatpak::RepoGpgKey"); +} + +const gchar * +gs_flatpak_app_get_repo_url (GsApp *app) +{ + return gs_app_get_metadata_item (app, "flatpak::RepoUrl"); +} + +gchar * +gs_flatpak_app_get_ref_display (GsApp *app) +{ + const gchar *ref_kind_as_str = gs_flatpak_app_get_ref_kind_as_str (app); + const gchar *ref_name = gs_flatpak_app_get_ref_name (app); + const gchar *ref_arch = gs_flatpak_app_get_ref_arch (app); + const gchar *ref_branch = gs_app_get_branch (app); + + g_return_val_if_fail (ref_kind_as_str != NULL, NULL); + g_return_val_if_fail (ref_name != NULL, NULL); + g_return_val_if_fail (ref_arch != NULL, NULL); + g_return_val_if_fail (ref_branch != NULL, NULL); + + return g_strdup_printf ("%s/%s/%s/%s", + ref_kind_as_str, + ref_name, + ref_arch, + ref_branch); +} + +void +gs_flatpak_app_set_ref_name (GsApp *app, const gchar *val) +{ + gs_app_set_metadata (app, "flatpak::RefName", val); +} + +void +gs_flatpak_app_set_ref_arch (GsApp *app, const gchar *val) +{ + gs_app_set_metadata (app, "flatpak::RefArch", val); +} + +void +gs_flatpak_app_set_commit (GsApp *app, const gchar *val) +{ + gs_app_set_metadata (app, "flatpak::Commit", val); +} + +void +gs_flatpak_app_set_file_kind (GsApp *app, GsFlatpakAppFileKind file_kind) +{ + g_autoptr(GVariant) tmp = g_variant_new_uint32 (file_kind); + gs_app_set_metadata_variant (app, "flatpak::FileKind", tmp); +} + +void +gs_flatpak_app_set_runtime_url (GsApp *app, const gchar *val) +{ + gs_app_set_metadata (app, "flatpak::RuntimeUrl", val); +} + +void +gs_flatpak_app_set_ref_kind (GsApp *app, FlatpakRefKind ref_kind) +{ + g_autoptr(GVariant) tmp = g_variant_new_uint32 (ref_kind); + gs_app_set_metadata_variant (app, "flatpak::RefKind", tmp); +} + +void +gs_flatpak_app_set_object_id (GsApp *app, const gchar *val) +{ + gs_app_set_metadata (app, "flatpak::ObjectID", val); +} + +void +gs_flatpak_app_set_repo_gpgkey (GsApp *app, const gchar *val) +{ + gs_app_set_metadata (app, "flatpak::RepoGpgKey", val); +} + +void +gs_flatpak_app_set_repo_url (GsApp *app, const gchar *val) +{ + gs_app_set_metadata (app, "flatpak::RepoUrl", val); +} + +GsApp * +gs_flatpak_app_new (const gchar *id) +{ + return GS_APP (g_object_new (GS_TYPE_APP, "id", id, NULL)); +} + +void +gs_flatpak_app_set_main_app_ref_name (GsApp *app, const gchar *main_app_ref) +{ + gs_app_set_metadata (app, "flatpak::mainApp", main_app_ref); +} + +const gchar * +gs_flatpak_app_get_main_app_ref_name (GsApp *app) +{ + return gs_app_get_metadata_item (app, "flatpak::mainApp"); +} diff --git a/plugins/flatpak/gs-flatpak-app.h b/plugins/flatpak/gs-flatpak-app.h new file mode 100644 index 0000000..ab6c10a --- /dev/null +++ b/plugins/flatpak/gs-flatpak-app.h @@ -0,0 +1,62 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2017-2018 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gnome-software.h> +#include <flatpak.h> + +G_BEGIN_DECLS + +typedef enum { + GS_FLATPAK_APP_FILE_KIND_UNKNOWN, + GS_FLATPAK_APP_FILE_KIND_REPO, + GS_FLATPAK_APP_FILE_KIND_REF, + GS_FLATPAK_APP_FILE_KIND_BUNDLE, + GS_FLATPAK_APP_FILE_KIND_LAST, +} GsFlatpakAppFileKind; + +GsApp *gs_flatpak_app_new (const gchar *id); + +const gchar *gs_flatpak_app_get_ref_name (GsApp *app); +const gchar *gs_flatpak_app_get_ref_arch (GsApp *app); +FlatpakRefKind gs_flatpak_app_get_ref_kind (GsApp *app); +const gchar *gs_flatpak_app_get_ref_kind_as_str (GsApp *app); +gchar *gs_flatpak_app_get_ref_display (GsApp *app); + +const gchar *gs_flatpak_app_get_commit (GsApp *app); +const gchar *gs_flatpak_app_get_object_id (GsApp *app); +const gchar *gs_flatpak_app_get_repo_gpgkey (GsApp *app); +const gchar *gs_flatpak_app_get_repo_url (GsApp *app); +GsFlatpakAppFileKind gs_flatpak_app_get_file_kind (GsApp *app); +const gchar *gs_flatpak_app_get_runtime_url (GsApp *app); + +void gs_flatpak_app_set_ref_name (GsApp *app, + const gchar *val); +void gs_flatpak_app_set_ref_arch (GsApp *app, + const gchar *val); +void gs_flatpak_app_set_ref_kind (GsApp *app, + FlatpakRefKind ref_kind); + +void gs_flatpak_app_set_commit (GsApp *app, + const gchar *val); +void gs_flatpak_app_set_object_id (GsApp *app, + const gchar *val); +void gs_flatpak_app_set_repo_gpgkey (GsApp *app, + const gchar *val); +void gs_flatpak_app_set_repo_url (GsApp *app, + const gchar *val); +void gs_flatpak_app_set_file_kind (GsApp *app, + GsFlatpakAppFileKind file_kind); +void gs_flatpak_app_set_runtime_url (GsApp *app, + const gchar *val); +void gs_flatpak_app_set_main_app_ref_name (GsApp *app, + const gchar *main_app_ref); +const gchar *gs_flatpak_app_get_main_app_ref_name (GsApp *app); + +G_END_DECLS diff --git a/plugins/flatpak/gs-flatpak-transaction.c b/plugins/flatpak/gs-flatpak-transaction.c new file mode 100644 index 0000000..ffff22e --- /dev/null +++ b/plugins/flatpak/gs-flatpak-transaction.c @@ -0,0 +1,804 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2018 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include "gs-flatpak-app.h" +#include "gs-flatpak-transaction.h" + +struct _GsFlatpakTransaction { + FlatpakTransaction parent_instance; + GHashTable *refhash; /* ref:GsApp */ + GError *first_operation_error; +#if !FLATPAK_CHECK_VERSION(1,5,1) + gboolean no_deploy; +#endif +}; + + +#if !FLATPAK_CHECK_VERSION(1,5,1) +typedef enum { + PROP_NO_DEPLOY = 1, +} GsFlatpakTransactionProperty; +#endif + +enum { + SIGNAL_REF_TO_APP, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0 }; + +G_DEFINE_TYPE (GsFlatpakTransaction, gs_flatpak_transaction, FLATPAK_TYPE_TRANSACTION) + +static void +gs_flatpak_transaction_finalize (GObject *object) +{ + GsFlatpakTransaction *self; + g_return_if_fail (GS_IS_FLATPAK_TRANSACTION (object)); + self = GS_FLATPAK_TRANSACTION (object); + + g_assert (self != NULL); + g_hash_table_unref (self->refhash); + if (self->first_operation_error != NULL) + g_error_free (self->first_operation_error); + + G_OBJECT_CLASS (gs_flatpak_transaction_parent_class)->finalize (object); +} + + +#if !FLATPAK_CHECK_VERSION(1,5,1) +void +gs_flatpak_transaction_set_no_deploy (FlatpakTransaction *transaction, gboolean no_deploy) +{ + GsFlatpakTransaction *self; + + g_return_if_fail (GS_IS_FLATPAK_TRANSACTION (transaction)); + + self = GS_FLATPAK_TRANSACTION (transaction); + if (self->no_deploy == no_deploy) + return; + self->no_deploy = no_deploy; + flatpak_transaction_set_no_deploy (transaction, no_deploy); + + g_object_notify (G_OBJECT (self), "no-deploy"); +} +#endif + +GsApp * +gs_flatpak_transaction_get_app_by_ref (FlatpakTransaction *transaction, const gchar *ref) +{ + GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (transaction); + return g_hash_table_lookup (self->refhash, ref); +} + +static void +gs_flatpak_transaction_add_app_internal (GsFlatpakTransaction *self, GsApp *app) +{ + g_autofree gchar *ref = gs_flatpak_app_get_ref_display (app); + g_hash_table_insert (self->refhash, g_steal_pointer (&ref), g_object_ref (app)); +} + +void +gs_flatpak_transaction_add_app (FlatpakTransaction *transaction, GsApp *app) +{ + GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (transaction); + gs_flatpak_transaction_add_app_internal (self, app); + if (gs_app_get_runtime (app) != NULL) + gs_flatpak_transaction_add_app_internal (self, gs_app_get_runtime (app)); +} + +static GsApp * +_ref_to_app (GsFlatpakTransaction *self, const gchar *ref) +{ + GsApp *app = g_hash_table_lookup (self->refhash, ref); + if (app != NULL) + return g_object_ref (app); + g_signal_emit (self, signals[SIGNAL_REF_TO_APP], 0, ref, &app); + return app; +} + +static void +_transaction_operation_set_app (FlatpakTransactionOperation *op, GsApp *app) +{ + g_object_set_data_full (G_OBJECT (op), "GsApp", + g_object_ref (app), (GDestroyNotify) g_object_unref); +} + +static GsApp * +_transaction_operation_get_app (FlatpakTransactionOperation *op) +{ + return g_object_get_data (G_OBJECT (op), "GsApp"); +} + +gboolean +gs_flatpak_transaction_run (FlatpakTransaction *transaction, + GCancellable *cancellable, + GError **error) + +{ + GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (transaction); + g_autoptr(GError) error_local = NULL; + + if (!flatpak_transaction_run (transaction, cancellable, &error_local)) { + /* whole transaction failed; restore the state for all the apps involved */ + g_autolist(GObject) ops = flatpak_transaction_get_operations (transaction); + for (GList *l = ops; l != NULL; l = l->next) { + FlatpakTransactionOperation *op = l->data; + const gchar *ref = flatpak_transaction_operation_get_ref (op); + g_autoptr(GsApp) app = _ref_to_app (self, ref); + if (app == NULL) { + g_warning ("failed to find app for %s", ref); + continue; + } + gs_app_set_state_recover (app); + } + + if (self->first_operation_error != NULL) { + g_propagate_error (error, g_steal_pointer (&self->first_operation_error)); + return FALSE; + } else { + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + } + + return TRUE; +} + +static gboolean +_transaction_ready (FlatpakTransaction *transaction) +{ + GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (transaction); + g_autolist(GObject) ops = NULL; + + /* nothing to do */ + ops = flatpak_transaction_get_operations (transaction); + if (ops == NULL) + return TRUE; // FIXME: error? + for (GList *l = ops; l != NULL; l = l->next) { + FlatpakTransactionOperation *op = l->data; + const gchar *ref = flatpak_transaction_operation_get_ref (op); + g_autoptr(GsApp) app = _ref_to_app (self, ref); + if (app != NULL) { + _transaction_operation_set_app (op, app); + /* if we're updating a component, then mark all the apps + * involved to ensure updating the button state */ + if (flatpak_transaction_operation_get_operation_type (op) == + FLATPAK_TRANSACTION_OPERATION_UPDATE) + gs_app_set_state (app, AS_APP_STATE_INSTALLING); + } + +#if FLATPAK_CHECK_VERSION(1, 7, 3) + /* Debug dump. */ + { + GPtrArray *related_to_ops = flatpak_transaction_operation_get_related_to_ops (op); + g_autoptr(GString) debug_message = g_string_new (""); + + g_string_append_printf (debug_message, + "%s: op %p, app %s (%p), download size %" G_GUINT64_FORMAT ", related-to:", + G_STRFUNC, op, + app ? gs_app_get_unique_id (app) : "?", + app, + flatpak_transaction_operation_get_download_size (op)); + for (gsize i = 0; related_to_ops != NULL && i < related_to_ops->len; i++) { + FlatpakTransactionOperation *related_to_op = g_ptr_array_index (related_to_ops, i); + g_string_append_printf (debug_message, + "\n ├ %s (%p)", flatpak_transaction_operation_get_ref (related_to_op), related_to_op); + } + g_string_append (debug_message, "\n └ (end)"); + g_debug ("%s", debug_message->str); + } +#endif /* flatpak ≥ 1.7.3 */ + } + return TRUE; +} + +typedef struct +{ + GsFlatpakTransaction *transaction; /* (owned) */ + FlatpakTransactionOperation *operation; /* (owned) */ + GsApp *app; /* (owned) */ +} ProgressData; + +static void +progress_data_free (ProgressData *data) +{ + g_clear_object (&data->operation); + g_clear_object (&data->app); + g_clear_object (&data->transaction); + g_free (data); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (ProgressData, progress_data_free) + +#if FLATPAK_CHECK_VERSION(1, 7, 3) +static gboolean +op_is_related_to_op (FlatpakTransactionOperation *op, + FlatpakTransactionOperation *root_op) +{ + GPtrArray *related_to_ops; /* (element-type FlatpakTransactionOperation) */ + + if (op == root_op) + return TRUE; + + related_to_ops = flatpak_transaction_operation_get_related_to_ops (op); + for (gsize i = 0; related_to_ops != NULL && i < related_to_ops->len; i++) { + FlatpakTransactionOperation *related_to_op = g_ptr_array_index (related_to_ops, i); + if (related_to_op == root_op || op_is_related_to_op (related_to_op, root_op)) + return TRUE; + } + + return FALSE; +} + +static guint64 +saturated_uint64_add (guint64 a, guint64 b) +{ + return (a <= G_MAXUINT64 - b) ? a + b : G_MAXUINT64; +} + +/* + * update_progress_for_op: + * @self: a #GsFlatpakTransaction + * @current_progress: progress reporting object + * @ops: results of calling flatpak_transaction_get_operations() on @self, for performance + * @current_op: the #FlatpakTransactionOperation which the @current_progress is + * for; this is the operation currently being run by libflatpak + * @root_op: the #FlatpakTransactionOperation at the root of the operation subtree + * to calculate progress for + * + * Calculate and update the #GsApp:progress for each app associated with + * @root_op in a flatpak transaction. This will include the #GsApp for the app + * being installed (for example), but also the #GsApps for all of its runtimes + * and locales, and any other dependencies of them. + * + * Each #GsApp:progress is calculated based on the sum of the progress of all + * the apps related to that one — so the progress for an app will factor in the + * progress for all its runtimes. + */ +static void +update_progress_for_op (GsFlatpakTransaction *self, + FlatpakTransactionProgress *current_progress, + GList *ops, + FlatpakTransactionOperation *current_op, + FlatpakTransactionOperation *root_op) +{ + g_autoptr(GsApp) root_app = NULL; + guint64 related_prior_download_bytes = 0; + guint64 related_download_bytes = 0; + guint64 current_bytes_transferred = flatpak_transaction_progress_get_bytes_transferred (current_progress); + gboolean seen_current_op = FALSE, seen_root_op = FALSE; + gboolean root_op_skipped = flatpak_transaction_operation_get_is_skipped (root_op); + guint percent; + + /* If @root_op is being skipped and its GsApp isn't being + * installed/removed, don't update the progress on it. It may be that + * @root_op is the runtime of an app and the app is the thing the + * transaction was created for. + */ + if (root_op_skipped) { + /* _transaction_operation_set_app() is only called on non-skipped ops */ + const gchar *ref = flatpak_transaction_operation_get_ref (root_op); + root_app = _ref_to_app (self, ref); + if (root_app == NULL) { + g_warning ("Couldn't find GsApp for transaction operation %s", + flatpak_transaction_operation_get_ref (root_op)); + return; + } + if (gs_app_get_state (root_app) != AS_APP_STATE_INSTALLING && + gs_app_get_state (root_app) != AS_APP_STATE_REMOVING) + return; + } else { + GsApp *unskipped_root_app = _transaction_operation_get_app (root_op); + if (unskipped_root_app == NULL) { + g_warning ("Couldn't find GsApp for transaction operation %s", + flatpak_transaction_operation_get_ref (root_op)); + return; + } + root_app = g_object_ref (unskipped_root_app); + } + + /* This relies on ops in a #FlatpakTransaction being run in the order + * they’re returned by flatpak_transaction_get_operations(), which is true. */ + for (GList *l = ops; l != NULL; l = l->next) { + FlatpakTransactionOperation *op = FLATPAK_TRANSACTION_OPERATION (l->data); + guint64 op_download_size = flatpak_transaction_operation_get_download_size (op); + + if (op == current_op) + seen_current_op = TRUE; + if (op == root_op) + seen_root_op = TRUE; + + /* Currently libflatpak doesn't return skipped ops in + * flatpak_transaction_get_operations(), but check just in case. + */ + if (op == root_op && root_op_skipped) + continue; + + if (op_is_related_to_op (op, root_op)) { + /* Saturate instead of overflowing */ + related_download_bytes = saturated_uint64_add (related_download_bytes, op_download_size); + if (!seen_current_op) + related_prior_download_bytes = saturated_uint64_add (related_prior_download_bytes, op_download_size); + } + } + + g_assert (related_prior_download_bytes <= related_download_bytes); + g_assert (seen_root_op || root_op_skipped); + + /* Avoid overflows when converting to percent, at the cost of losing + * some precision in the least significant digits. */ + if (related_prior_download_bytes > G_MAXUINT64 / 100 || + current_bytes_transferred > G_MAXUINT64 / 100) { + related_prior_download_bytes /= 100; + current_bytes_transferred /= 100; + related_download_bytes /= 100; + } + + /* Update the progress of @root_app. */ + if (related_download_bytes > 0) + percent = ((related_prior_download_bytes * 100 / related_download_bytes) + + (current_bytes_transferred * 100 / related_download_bytes)); + else + percent = 0; + + if (gs_app_get_progress (root_app) == 100 || + gs_app_get_progress (root_app) == GS_APP_PROGRESS_UNKNOWN || + gs_app_get_progress (root_app) <= percent) { + gs_app_set_progress (root_app, percent); + } else { + g_warning ("ignoring percentage %u%% -> %u%% as going down on app %s", + gs_app_get_progress (root_app), percent, + gs_app_get_unique_id (root_app)); + } +} +#endif /* flatpak 1.7.3 */ + +#if FLATPAK_CHECK_VERSION(1, 7, 3) +static void +update_progress_for_op_recurse_up (GsFlatpakTransaction *self, + FlatpakTransactionProgress *progress, + GList *ops, + FlatpakTransactionOperation *current_op, + FlatpakTransactionOperation *root_op) +{ + GPtrArray *related_to_ops = flatpak_transaction_operation_get_related_to_ops (root_op); + + /* Update progress for @root_op */ + update_progress_for_op (self, progress, ops, current_op, root_op); + + /* Update progress for ops related to @root_op, e.g. apps whose runtime is @root_op */ + for (gsize i = 0; related_to_ops != NULL && i < related_to_ops->len; i++) { + FlatpakTransactionOperation *related_to_op = g_ptr_array_index (related_to_ops, i); + update_progress_for_op_recurse_up (self, progress, ops, current_op, related_to_op); + } +} +#endif /* flatpak 1.7.3 */ + +static void +_transaction_progress_changed_cb (FlatpakTransactionProgress *progress, + gpointer user_data) +{ + ProgressData *data = user_data; + GsApp *app = data->app; +#if FLATPAK_CHECK_VERSION(1, 7, 3) + GsFlatpakTransaction *self = data->transaction; + g_autolist(FlatpakTransactionOperation) ops = NULL; +#else + guint percent; +#endif + + if (flatpak_transaction_progress_get_is_estimating (progress)) { + /* "Estimating" happens while fetching the metadata, which + * flatpak arbitrarily decides happens during the first 5% of + * each operation. At this point, no more detailed progress + * information is available. */ + gs_app_set_progress (app, GS_APP_PROGRESS_UNKNOWN); + return; + } + +#if FLATPAK_CHECK_VERSION(1, 7, 3) + /* Update the progress on this app, and then do the same for each + * related parent app up the hierarchy. For example, @data->operation + * could be for a runtime which was added to the transaction because of + * an app — so we need to update the progress on the app too. + * + * It’s important to note that a new @data->progress is created by + * libflatpak for each @data->operation, and there are multiple + * operations in a transaction. There is no #FlatpakTransactionProgress + * which represents the progress of the whole transaction. + * + * There may be arbitrary many levels of related-to ops. For example, + * one common situation would be to install an app which needs a new + * runtime, and that runtime needs a locale to be installed, which would + * give three levels of related-to relation: + * locale → runtime → app → (null) + * + * In addition, libflatpak may decide to skip some operations (if they + * turn out to not be necessary). These skipped operations are not + * included in the list returned by flatpak_transaction_get_operations(), + * but they can be accessed via + * flatpak_transaction_operation_get_related_to_ops(), so have to be + * ignored manually. + */ + ops = flatpak_transaction_get_operations (FLATPAK_TRANSACTION (self)); + update_progress_for_op_recurse_up (self, progress, ops, data->operation, data->operation); +#else /* if !flatpak 1.7.3 */ + percent = flatpak_transaction_progress_get_progress (progress); + + if (gs_app_get_progress (app) != 100 && + gs_app_get_progress (app) != GS_APP_PROGRESS_UNKNOWN && + gs_app_get_progress (app) > percent) { + g_warning ("ignoring percentage %u%% -> %u%% as going down...", + gs_app_get_progress (app), percent); + return; + } + + gs_app_set_progress (app, percent); +#endif /* !flatpak 1.7.3 */ +} + +static const gchar * +_flatpak_transaction_operation_type_to_string (FlatpakTransactionOperationType ot) +{ + if (ot == FLATPAK_TRANSACTION_OPERATION_INSTALL) + return "install"; + if (ot == FLATPAK_TRANSACTION_OPERATION_UPDATE) + return "update"; + if (ot == FLATPAK_TRANSACTION_OPERATION_INSTALL_BUNDLE) + return "install-bundle"; + if (ot == FLATPAK_TRANSACTION_OPERATION_UNINSTALL) + return "uninstall"; + return NULL; +} + +static void +progress_data_free_closure (gpointer user_data, + GClosure *closure) +{ + progress_data_free (user_data); +} + +static void +_transaction_new_operation (FlatpakTransaction *transaction, + FlatpakTransactionOperation *operation, + FlatpakTransactionProgress *progress) +{ + GsApp *app; + g_autoptr(ProgressData) progress_data = NULL; + + /* find app */ + app = _transaction_operation_get_app (operation); + if (app == NULL) { + FlatpakTransactionOperationType ot; + ot = flatpak_transaction_operation_get_operation_type (operation); + g_warning ("failed to find app for %s during %s", + flatpak_transaction_operation_get_ref (operation), + _flatpak_transaction_operation_type_to_string (ot)); + return; + } + + /* report progress */ + progress_data = g_new0 (ProgressData, 1); + progress_data->transaction = GS_FLATPAK_TRANSACTION (g_object_ref (transaction)); + progress_data->app = g_object_ref (app); + progress_data->operation = g_object_ref (operation); + + g_signal_connect_data (progress, "changed", + G_CALLBACK (_transaction_progress_changed_cb), + g_steal_pointer (&progress_data), + progress_data_free_closure, + 0 /* flags */); + flatpak_transaction_progress_set_update_frequency (progress, 500); /* FIXME? */ + + /* set app status */ + switch (flatpak_transaction_operation_get_operation_type (operation)) { + case FLATPAK_TRANSACTION_OPERATION_INSTALL: + if (gs_app_get_state (app) == AS_APP_STATE_UNKNOWN) + gs_app_set_state (app, AS_APP_STATE_AVAILABLE); + gs_app_set_state (app, AS_APP_STATE_INSTALLING); + break; + case FLATPAK_TRANSACTION_OPERATION_INSTALL_BUNDLE: + if (gs_app_get_state (app) == AS_APP_STATE_UNKNOWN) + gs_app_set_state (app, AS_APP_STATE_AVAILABLE_LOCAL); + gs_app_set_state (app, AS_APP_STATE_INSTALLING); + break; + case FLATPAK_TRANSACTION_OPERATION_UPDATE: + if (gs_app_get_state (app) == AS_APP_STATE_UNKNOWN) + gs_app_set_state (app, AS_APP_STATE_UPDATABLE_LIVE); + gs_app_set_state (app, AS_APP_STATE_INSTALLING); + break; + case FLATPAK_TRANSACTION_OPERATION_UNINSTALL: + gs_app_set_state (app, AS_APP_STATE_REMOVING); + break; + default: + break; + } +} + +#if FLATPAK_CHECK_VERSION(1, 7, 3) +static gboolean +later_op_also_related (GList *ops, + FlatpakTransactionOperation *current_op, + FlatpakTransactionOperation *related_to_current_op) +{ + /* Here we're determining if anything in @ops which comes after + * @current_op is related to @related_to_current_op and not skipped + * (but all @ops are not skipped so no need to check explicitly) + */ + gboolean found_later_op = FALSE, seen_current_op = FALSE; + for (GList *l = ops; l != NULL; l = l->next) { + FlatpakTransactionOperation *op = l->data; + GPtrArray *related_to_ops; + if (current_op == op) { + seen_current_op = TRUE; + continue; + } + if (!seen_current_op) + continue; + + related_to_ops = flatpak_transaction_operation_get_related_to_ops (op); + for (gsize i = 0; related_to_ops != NULL && i < related_to_ops->len; i++) { + FlatpakTransactionOperation *related_to_op = g_ptr_array_index (related_to_ops, i); + if (related_to_op == related_to_current_op) { + g_assert (flatpak_transaction_operation_get_is_skipped (related_to_op)); + found_later_op = TRUE; + } + } + } + + return found_later_op; +} + +static void +set_skipped_related_apps_to_installed (GsFlatpakTransaction *self, + FlatpakTransaction *transaction, + FlatpakTransactionOperation *operation) +{ + /* It's possible the thing being updated/installed, @operation, is a + * related ref (e.g. extension or runtime) of an app which itself doesn't + * need an update and therefore won't have _transaction_operation_done() + * called for it directly. So we have to set the main app to installed + * here. + */ + g_autolist(GObject) ops = flatpak_transaction_get_operations (transaction); + GPtrArray *related_to_ops = flatpak_transaction_operation_get_related_to_ops (operation); + + for (gsize i = 0; related_to_ops != NULL && i < related_to_ops->len; i++) { + FlatpakTransactionOperation *related_to_op = g_ptr_array_index (related_to_ops, i); + if (flatpak_transaction_operation_get_is_skipped (related_to_op)) { + const gchar *ref; + g_autoptr(GsApp) related_to_app = NULL; + + /* Check that no later op is also related to related_to_op, in + * which case we want to let that operation finish before setting + * the main app to installed. + */ + if (later_op_also_related (ops, operation, related_to_op)) + continue; + + ref = flatpak_transaction_operation_get_ref (related_to_op); + related_to_app = _ref_to_app (self, ref); + if (related_to_app != NULL) + gs_app_set_state (related_to_app, AS_APP_STATE_INSTALLED); + } + } +} +#endif /* flatpak 1.7.3 */ + +static void +_transaction_operation_done (FlatpakTransaction *transaction, + FlatpakTransactionOperation *operation, + const gchar *commit, + FlatpakTransactionResult details) +{ +#if !FLATPAK_CHECK_VERSION(1,5,1) || FLATPAK_CHECK_VERSION(1,7,3) + GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (transaction); +#endif + /* invalidate */ + GsApp *app = _transaction_operation_get_app (operation); + if (app == NULL) { + g_warning ("failed to find app for %s", + flatpak_transaction_operation_get_ref (operation)); + return; + } + switch (flatpak_transaction_operation_get_operation_type (operation)) { + case FLATPAK_TRANSACTION_OPERATION_INSTALL: + case FLATPAK_TRANSACTION_OPERATION_INSTALL_BUNDLE: + gs_app_set_state (app, AS_APP_STATE_INSTALLED); + +#if FLATPAK_CHECK_VERSION(1,7,3) + set_skipped_related_apps_to_installed (self, transaction, operation); +#endif + break; + case FLATPAK_TRANSACTION_OPERATION_UPDATE: + gs_app_set_version (app, gs_app_get_update_version (app)); + gs_app_set_update_details (app, NULL); + gs_app_set_update_urgency (app, AS_URGENCY_KIND_UNKNOWN); + gs_app_set_update_version (app, NULL); + /* force getting the new runtime */ + gs_app_remove_kudo (app, GS_APP_KUDO_SANDBOXED); + /* downloaded, but not yet installed */ +#if !FLATPAK_CHECK_VERSION(1,5,1) + if (self->no_deploy) +#else + if (flatpak_transaction_get_no_deploy (transaction)) +#endif + gs_app_set_state (app, AS_APP_STATE_UPDATABLE_LIVE); + else + gs_app_set_state (app, AS_APP_STATE_INSTALLED); + +#if FLATPAK_CHECK_VERSION(1,7,3) + set_skipped_related_apps_to_installed (self, transaction, operation); +#endif + break; + case FLATPAK_TRANSACTION_OPERATION_UNINSTALL: + /* we don't actually know if this app is re-installable */ + gs_flatpak_app_set_commit (app, NULL); + gs_app_set_state (app, AS_APP_STATE_UNKNOWN); + break; + default: + gs_app_set_state (app, AS_APP_STATE_UNKNOWN); + break; + } +} + +static gboolean +_transaction_operation_error (FlatpakTransaction *transaction, + FlatpakTransactionOperation *operation, + const GError *error, + FlatpakTransactionErrorDetails detail) +{ + GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (transaction); + FlatpakTransactionOperationType operation_type = flatpak_transaction_operation_get_operation_type (operation); + GsApp *app = _transaction_operation_get_app (operation); + const gchar *ref = flatpak_transaction_operation_get_ref (operation); + + if (g_error_matches (error, FLATPAK_ERROR, FLATPAK_ERROR_SKIPPED)) { + g_debug ("skipped to %s %s: %s", + _flatpak_transaction_operation_type_to_string (operation_type), + ref, + error->message); + return TRUE; /* continue */ + } + + if (detail & FLATPAK_TRANSACTION_ERROR_DETAILS_NON_FATAL) { + g_warning ("failed to %s %s (non fatal): %s", + _flatpak_transaction_operation_type_to_string (operation_type), + ref, + error->message); + return TRUE; /* continue */ + } + + if (self->first_operation_error == NULL) { + g_propagate_error (&self->first_operation_error, + g_error_copy (error)); + if (app != NULL) + gs_utils_error_add_app_id (&self->first_operation_error, app); + } + return FALSE; /* stop */ +} + +static int +_transaction_choose_remote_for_ref (FlatpakTransaction *transaction, + const char *for_ref, + const char *runtime_ref, + const char * const *remotes) +{ + //FIXME: do something smarter + return 0; +} + +static void +_transaction_end_of_lifed (FlatpakTransaction *transaction, + const gchar *ref, + const gchar *reason, + const gchar *rebase) +{ + if (rebase) { + g_printerr ("%s is end-of-life, in preference of %s\n", ref, rebase); + } else if (reason) { + g_printerr ("%s is end-of-life, with reason: %s\n", ref, reason); + } + //FIXME: show something in the UI +} + +static gboolean +_transaction_add_new_remote (FlatpakTransaction *transaction, + FlatpakTransactionRemoteReason reason, + const char *from_id, + const char *remote_name, + const char *url) +{ + /* additional applications */ + if (reason == FLATPAK_TRANSACTION_REMOTE_GENERIC_REPO) { + g_debug ("configuring %s as new generic remote", url); + return TRUE; //FIXME? + } + + /* runtime deps always make sense */ + if (reason == FLATPAK_TRANSACTION_REMOTE_RUNTIME_DEPS) { + g_debug ("configuring %s as new remote for deps", url); + return TRUE; + } + + return FALSE; +} + +#if !FLATPAK_CHECK_VERSION(1,5,1) +static void +gs_flatpak_transaction_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) +{ + FlatpakTransaction *transaction = FLATPAK_TRANSACTION (object); + + switch ((GsFlatpakTransactionProperty) prop_id) { + case PROP_NO_DEPLOY: + gs_flatpak_transaction_set_no_deploy (transaction, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} +#endif + +static void +gs_flatpak_transaction_class_init (GsFlatpakTransactionClass *klass) +{ + +#if !FLATPAK_CHECK_VERSION(1,5,1) + GParamSpec *pspec; +#endif + GObjectClass *object_class = G_OBJECT_CLASS (klass); + FlatpakTransactionClass *transaction_class = FLATPAK_TRANSACTION_CLASS (klass); + object_class->finalize = gs_flatpak_transaction_finalize; + transaction_class->ready = _transaction_ready; + transaction_class->add_new_remote = _transaction_add_new_remote; + transaction_class->new_operation = _transaction_new_operation; + transaction_class->operation_done = _transaction_operation_done; + transaction_class->operation_error = _transaction_operation_error; + transaction_class->choose_remote_for_ref = _transaction_choose_remote_for_ref; + transaction_class->end_of_lifed = _transaction_end_of_lifed; +#if !FLATPAK_CHECK_VERSION(1,5,1) + object_class->set_property = gs_flatpak_transaction_set_property; + + pspec = g_param_spec_boolean ("no-deploy", NULL, + "Whether the current transaction will deploy the downloaded objects", + FALSE, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT); + g_object_class_install_property (object_class, PROP_NO_DEPLOY, pspec); +#endif + + signals[SIGNAL_REF_TO_APP] = + g_signal_new ("ref-to-app", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, G_TYPE_OBJECT, 1, G_TYPE_STRING); +} + +static void +gs_flatpak_transaction_init (GsFlatpakTransaction *self) +{ + self->refhash = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, (GDestroyNotify) g_object_unref); +} + +FlatpakTransaction * +gs_flatpak_transaction_new (FlatpakInstallation *installation, + GCancellable *cancellable, + GError **error) +{ + GsFlatpakTransaction *self; + self = g_initable_new (GS_TYPE_FLATPAK_TRANSACTION, + cancellable, error, + "installation", installation, + NULL); + if (self == NULL) + return NULL; + return FLATPAK_TRANSACTION (self); +} diff --git a/plugins/flatpak/gs-flatpak-transaction.h b/plugins/flatpak/gs-flatpak-transaction.h new file mode 100644 index 0000000..97a4e10 --- /dev/null +++ b/plugins/flatpak/gs-flatpak-transaction.h @@ -0,0 +1,35 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2018 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gnome-software.h> +#include <flatpak.h> + +G_BEGIN_DECLS + +#define GS_TYPE_FLATPAK_TRANSACTION (gs_flatpak_transaction_get_type ()) + +G_DECLARE_FINAL_TYPE (GsFlatpakTransaction, gs_flatpak_transaction, GS, FLATPAK_TRANSACTION, FlatpakTransaction) + +FlatpakTransaction *gs_flatpak_transaction_new (FlatpakInstallation *installation, + GCancellable *cancellable, + GError **error); +GsApp *gs_flatpak_transaction_get_app_by_ref (FlatpakTransaction *transaction, + const gchar *ref); +void gs_flatpak_transaction_add_app (FlatpakTransaction *transaction, + GsApp *app); +gboolean gs_flatpak_transaction_run (FlatpakTransaction *transaction, + GCancellable *cancellable, + GError **error); +#if !FLATPAK_CHECK_VERSION(1,5,1) +void gs_flatpak_transaction_set_no_deploy (FlatpakTransaction *transaction, + gboolean no_deploy); +#endif + +G_END_DECLS diff --git a/plugins/flatpak/gs-flatpak-utils.c b/plugins/flatpak/gs-flatpak-utils.c new file mode 100644 index 0000000..4dee104 --- /dev/null +++ b/plugins/flatpak/gs-flatpak-utils.c @@ -0,0 +1,212 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2017-2018 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> +#include <ostree.h> + +#include "gs-flatpak-app.h" +#include "gs-flatpak.h" +#include "gs-flatpak-utils.h" + +void +gs_flatpak_error_convert (GError **perror) +{ + GError *error = perror != NULL ? *perror : NULL; + + /* not set */ + if (error == NULL) + return; + + /* this are allowed for low-level errors */ + if (gs_utils_error_convert_gio (perror)) + return; + + /* this are allowed for low-level errors */ + if (gs_utils_error_convert_gdbus (perror)) + return; + + /* this are allowed for network ops */ + if (gs_utils_error_convert_gresolver (perror)) + return; + + /* custom to this plugin */ + if (error->domain == FLATPAK_ERROR) { + switch (error->code) { + case FLATPAK_ERROR_ALREADY_INSTALLED: + case FLATPAK_ERROR_NOT_INSTALLED: + case FLATPAK_ERROR_REMOTE_NOT_FOUND: + case FLATPAK_ERROR_RUNTIME_NOT_FOUND: + error->code = GS_PLUGIN_ERROR_NOT_SUPPORTED; + break; + default: + error->code = GS_PLUGIN_ERROR_FAILED; + break; + } + } else if (error->domain == OSTREE_GPG_ERROR) { + error->code = GS_PLUGIN_ERROR_NO_SECURITY; + } else { + g_warning ("can't reliably fixup error from domain %s: %s", + g_quark_to_string (error->domain), + error->message); + error->code = GS_PLUGIN_ERROR_FAILED; + } + error->domain = GS_PLUGIN_ERROR; +} + +GsApp * +gs_flatpak_app_new_from_remote (FlatpakRemote *xremote) +{ + g_autofree gchar *title = NULL; + g_autofree gchar *url = NULL; + g_autoptr(GsApp) app = NULL; + + app = gs_flatpak_app_new (flatpak_remote_get_name (xremote)); + gs_app_set_kind (app, AS_APP_KIND_SOURCE); + gs_app_set_state (app, flatpak_remote_get_disabled (xremote) ? + AS_APP_STATE_AVAILABLE : AS_APP_STATE_INSTALLED); + gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE); + gs_app_set_name (app, GS_APP_QUALITY_LOWEST, + flatpak_remote_get_name (xremote)); + gs_app_set_size_download (app, GS_APP_SIZE_UNKNOWABLE); + + /* title */ + title = flatpak_remote_get_title (xremote); + if (title != NULL) + gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, title); + + /* url */ + url = flatpak_remote_get_url (xremote); + if (url != NULL) + gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, url); + + /* success */ + return g_steal_pointer (&app); +} + +GsApp * +gs_flatpak_app_new_from_repo_file (GFile *file, + GCancellable *cancellable, + GError **error) +{ + gchar *tmp; + g_autofree gchar *basename = NULL; + g_autofree gchar *filename = NULL; + g_autofree gchar *repo_comment = NULL; + g_autofree gchar *repo_default_branch = NULL; + g_autofree gchar *repo_description = NULL; + g_autofree gchar *repo_gpgkey = NULL; + g_autofree gchar *repo_homepage = NULL; + g_autofree gchar *repo_icon = NULL; + g_autofree gchar *repo_id = NULL; + g_autofree gchar *repo_title = NULL; + g_autofree gchar *repo_url = NULL; + g_autoptr(GError) error_local = NULL; + g_autoptr(GKeyFile) kf = NULL; + g_autoptr(GsApp) app = NULL; + + /* read the file */ + kf = g_key_file_new (); + filename = g_file_get_path (file); + if (!g_key_file_load_from_file (kf, filename, + G_KEY_FILE_NONE, + &error_local)) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "failed to load flatpakrepo: %s", + error_local->message); + return NULL; + } + + /* get the ID from the basename */ + basename = g_file_get_basename (file); + + /* ensure this is valid for flatpak */ + repo_id = g_str_to_ascii (basename, NULL); + tmp = g_strrstr (repo_id, "."); + if (tmp != NULL) + *tmp = '\0'; + for (guint i = 0; repo_id[i] != '\0'; i++) { + if (!g_ascii_isalnum (repo_id[i])) + repo_id[i] = '_'; + } + + /* create source */ + repo_title = g_key_file_get_string (kf, "Flatpak Repo", "Title", NULL); + repo_url = g_key_file_get_string (kf, "Flatpak Repo", "Url", NULL); + if (repo_title == NULL || repo_url == NULL || + repo_title[0] == '\0' || repo_url[0] == '\0') { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "not enough data in file, " + "expected at least Title and Url"); + return NULL; + } + + /* check version */ + if (g_key_file_has_key (kf, "Flatpak Repo", "Version", NULL)) { + guint64 ver = g_key_file_get_uint64 (kf, "Flatpak Repo", "Version", NULL); + if (ver != 1) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "unsupported version %" G_GUINT64_FORMAT, ver); + return NULL; + } + } + + /* create source */ + app = gs_flatpak_app_new (repo_id); + gs_flatpak_app_set_file_kind (app, GS_FLATPAK_APP_FILE_KIND_REPO); + gs_app_set_kind (app, AS_APP_KIND_SOURCE); + gs_app_set_state (app, AS_APP_STATE_AVAILABLE_LOCAL); + gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE); + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, repo_title); + gs_app_set_size_download (app, GS_APP_SIZE_UNKNOWABLE); + gs_flatpak_app_set_repo_url (app, repo_url); + gs_app_set_origin_hostname (app, repo_url); + + /* user specified a URL */ + repo_gpgkey = g_key_file_get_string (kf, "Flatpak Repo", "GPGKey", NULL); + if (repo_gpgkey != NULL) { + if (g_str_has_prefix (repo_gpgkey, "http://") || + g_str_has_prefix (repo_gpgkey, "https://")) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "Base64 encoded GPGKey required, not URL"); + return NULL; + } + gs_flatpak_app_set_repo_gpgkey (app, repo_gpgkey); + } + + /* optional data */ + repo_homepage = g_key_file_get_string (kf, "Flatpak Repo", "Homepage", NULL); + if (repo_homepage != NULL) + gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, repo_homepage); + repo_comment = g_key_file_get_string (kf, "Flatpak Repo", "Comment", NULL); + if (repo_comment != NULL) + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, repo_comment); + repo_description = g_key_file_get_string (kf, "Flatpak Repo", "Description", NULL); + if (repo_description != NULL) + gs_app_set_description (app, GS_APP_QUALITY_NORMAL, repo_description); + repo_default_branch = g_key_file_get_string (kf, "Flatpak Repo", "DefaultBranch", NULL); + if (repo_default_branch != NULL) + gs_app_set_branch (app, repo_default_branch); + repo_icon = g_key_file_get_string (kf, "Flatpak Repo", "Icon", NULL); + if (repo_icon != NULL) { + g_autoptr(AsIcon) ic = as_icon_new (); + as_icon_set_kind (ic, AS_ICON_KIND_REMOTE); + as_icon_set_url (ic, repo_icon); + gs_app_add_icon (app, ic); + } + + /* success */ + return g_steal_pointer (&app); +} diff --git a/plugins/flatpak/gs-flatpak-utils.h b/plugins/flatpak/gs-flatpak-utils.h new file mode 100644 index 0000000..61cd62d --- /dev/null +++ b/plugins/flatpak/gs-flatpak-utils.h @@ -0,0 +1,21 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2017 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +G_BEGIN_DECLS + +#include <gnome-software.h> + +void gs_flatpak_error_convert (GError **perror); +GsApp *gs_flatpak_app_new_from_remote (FlatpakRemote *xremote); +GsApp *gs_flatpak_app_new_from_repo_file (GFile *file, + GCancellable *cancellable, + GError **error); + +G_END_DECLS diff --git a/plugins/flatpak/gs-flatpak.c b/plugins/flatpak/gs-flatpak.c new file mode 100644 index 0000000..cf86a7f --- /dev/null +++ b/plugins/flatpak/gs-flatpak.c @@ -0,0 +1,3258 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Joaquim Rocha <jrocha@endlessm.com> + * Copyright (C) 2016-2018 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2016-2019 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/* Notes: + * + * All GsApp's created have management-plugin set to flatpak + * The GsApp:origin is the remote name, e.g. test-repo + */ + +#include <config.h> + +#include <glib/gi18n.h> +#include <xmlb.h> + +#include "gs-appstream.h" +#include "gs-flatpak-app.h" +#include "gs-flatpak.h" +#include "gs-flatpak-utils.h" + +struct _GsFlatpak { + GObject parent_instance; + GsFlatpakFlags flags; + FlatpakInstallation *installation; + GPtrArray *installed_refs; /* must be entirely replaced rather than updated internally */ + GMutex installed_refs_mutex; + GHashTable *broken_remotes; + GMutex broken_remotes_mutex; + GFileMonitor *monitor; + AsAppScope scope; + GsPlugin *plugin; + XbSilo *silo; + GRWLock silo_lock; + gchar *id; + guint changed_id; + GHashTable *app_silos; + GMutex app_silos_mutex; +}; + +G_DEFINE_TYPE (GsFlatpak, gs_flatpak, G_TYPE_OBJECT) + +static gboolean +gs_flatpak_refresh_appstream (GsFlatpak *self, guint cache_age, + GCancellable *cancellable, GError **error); + +static void +gs_plugin_refine_item_scope (GsFlatpak *self, GsApp *app) +{ + if (gs_app_get_scope (app) == AS_APP_SCOPE_UNKNOWN) { + gboolean is_user = flatpak_installation_get_is_user (self->installation); + gs_app_set_scope (app, is_user ? AS_APP_SCOPE_USER : AS_APP_SCOPE_SYSTEM); + } +} + +static void +gs_flatpak_claim_app (GsFlatpak *self, GsApp *app) +{ + if (gs_app_get_management_plugin (app) != NULL) + return; + gs_app_set_management_plugin (app, gs_plugin_get_name (self->plugin)); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_FLATPAK); + + /* only when we have a non-temp object */ + if ((self->flags & GS_FLATPAK_FLAG_IS_TEMPORARY) == 0) { + gs_app_set_scope (app, self->scope); + gs_flatpak_app_set_object_id (app, gs_flatpak_get_id (self)); + } +} + +static void +gs_flatpak_claim_app_list (GsFlatpak *self, GsAppList *list) +{ + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + gs_flatpak_claim_app (self, app); + } +} + +static void +gs_flatpak_set_kind_from_flatpak (GsApp *app, FlatpakRef *xref) +{ + if (flatpak_ref_get_kind (xref) == FLATPAK_REF_KIND_APP) { + gs_app_set_kind (app, AS_APP_KIND_DESKTOP); + } else if (flatpak_ref_get_kind (xref) == FLATPAK_REF_KIND_RUNTIME) { + const gchar *id = gs_app_get_id (app); + /* this is anything that's not an app, including locales + * sources and debuginfo */ + if (g_str_has_suffix (id, ".Locale")) { + gs_app_set_kind (app, AS_APP_KIND_LOCALIZATION); + } else if (g_str_has_suffix (id, ".Debug") || + g_str_has_suffix (id, ".Sources") || + g_str_has_prefix (id, "org.freedesktop.Platform.Icontheme.") || + g_str_has_prefix (id, "org.gtk.Gtk3theme.")) { + gs_app_set_kind (app, AS_APP_KIND_GENERIC); + } else { + gs_app_set_kind (app, AS_APP_KIND_RUNTIME); + } + } +} + +static GsAppPermissions +perms_from_metadata (GKeyFile *keyfile) +{ + char **strv; + char *str; + GsAppPermissions permissions = GS_APP_PERMISSIONS_UNKNOWN; + + strv = g_key_file_get_string_list (keyfile, "Context", "sockets", NULL, NULL); + if (strv != NULL && g_strv_contains ((const gchar * const*)strv, "system-bus")) + permissions |= GS_APP_PERMISSIONS_SYSTEM_BUS; + if (strv != NULL && g_strv_contains ((const gchar * const*)strv, "session-bus")) + permissions |= GS_APP_PERMISSIONS_SESSION_BUS; + if (strv != NULL && + !g_strv_contains ((const gchar * const*)strv, "fallback-x11") && + g_strv_contains ((const gchar * const*)strv, "x11")) + permissions |= GS_APP_PERMISSIONS_X11; + g_strfreev (strv); + + strv = g_key_file_get_string_list (keyfile, "Context", "devices", NULL, NULL); + if (strv != NULL && g_strv_contains ((const gchar * const*)strv, "all")) + permissions |= GS_APP_PERMISSIONS_DEVICES; + g_strfreev (strv); + + strv = g_key_file_get_string_list (keyfile, "Context", "shared", NULL, NULL); + if (strv != NULL && g_strv_contains ((const gchar * const*)strv, "network")) + permissions |= GS_APP_PERMISSIONS_NETWORK; + g_strfreev (strv); + + strv = g_key_file_get_string_list (keyfile, "Context", "filesystems", NULL, NULL); + if (strv != NULL && (g_strv_contains ((const gchar * const *)strv, "home") || + g_strv_contains ((const gchar * const *)strv, "home:rw"))) + permissions |= GS_APP_PERMISSIONS_HOME_FULL; + else if (strv != NULL && g_strv_contains ((const gchar * const *)strv, "home:ro")) + permissions |= GS_APP_PERMISSIONS_HOME_READ; + if (strv != NULL && (g_strv_contains ((const gchar * const *)strv, "host") || + g_strv_contains ((const gchar * const *)strv, "host:rw"))) + permissions |= GS_APP_PERMISSIONS_FILESYSTEM_FULL; + else if (strv != NULL && g_strv_contains ((const gchar * const *)strv, "host:ro")) + permissions |= GS_APP_PERMISSIONS_FILESYSTEM_READ; + if (strv != NULL && (g_strv_contains ((const gchar * const *)strv, "xdg-download") || + g_strv_contains ((const gchar * const *)strv, "xdg-download:rw"))) + permissions |= GS_APP_PERMISSIONS_DOWNLOADS_FULL; + else if (strv != NULL && g_strv_contains ((const gchar * const *)strv, "xdg-download:ro")) + permissions |= GS_APP_PERMISSIONS_DOWNLOADS_READ; + g_strfreev (strv); + + str = g_key_file_get_string (keyfile, "Session Bus Policy", "ca.desrt.dconf", NULL); + if (str != NULL && g_str_equal (str, "talk")) + permissions |= GS_APP_PERMISSIONS_SETTINGS; + g_free (str); + + str = g_key_file_get_string (keyfile, "Session Bus Policy", "org.freedesktop.Flatpak", NULL); + if (str != NULL && g_str_equal (str, "talk")) + permissions |= GS_APP_PERMISSIONS_ESCAPE_SANDBOX; + g_free (str); + + /* no permissions set */ + if (permissions == GS_APP_PERMISSIONS_UNKNOWN) + return GS_APP_PERMISSIONS_NONE; + + return permissions; +} + +static void +gs_flatpak_set_update_permissions (GsFlatpak *self, GsApp *app, FlatpakInstalledRef *xref) +{ + g_autoptr(GBytes) old_bytes = NULL; + g_autoptr(GKeyFile) old_keyfile = NULL; + g_autoptr(GBytes) bytes = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + GsAppPermissions permissions; + g_autoptr(GError) error_local = NULL; + + old_bytes = flatpak_installed_ref_load_metadata (FLATPAK_INSTALLED_REF (xref), NULL, NULL); + old_keyfile = g_key_file_new (); + g_key_file_load_from_data (old_keyfile, + g_bytes_get_data (old_bytes, NULL), + g_bytes_get_size (old_bytes), + 0, NULL); + + bytes = flatpak_installation_fetch_remote_metadata_sync (self->installation, + gs_app_get_origin (app), + FLATPAK_REF (xref), + NULL, + &error_local); + if (bytes == NULL) { + g_debug ("Failed to get metadata for remote ‘%s’: %s", + gs_app_get_origin (app), error_local->message); + g_clear_error (&error_local); + permissions = GS_APP_PERMISSIONS_UNKNOWN; + } else { + keyfile = g_key_file_new (); + g_key_file_load_from_data (keyfile, + g_bytes_get_data (bytes, NULL), + g_bytes_get_size (bytes), + 0, NULL); + permissions = perms_from_metadata (keyfile) & ~perms_from_metadata (old_keyfile); + } + + /* no new permissions set */ + if (permissions == GS_APP_PERMISSIONS_UNKNOWN) + permissions = GS_APP_PERMISSIONS_NONE; + + gs_app_set_update_permissions (app, permissions); + + if (permissions != GS_APP_PERMISSIONS_NONE) + gs_app_add_quirk (app, GS_APP_QUIRK_NEW_PERMISSIONS); +} + +static void +gs_flatpak_set_metadata (GsFlatpak *self, GsApp *app, FlatpakRef *xref) +{ + g_autofree gchar *ref_tmp = flatpak_ref_format_ref (FLATPAK_REF (xref)); + + /* core */ + gs_flatpak_claim_app (self, app); + gs_app_set_branch (app, flatpak_ref_get_branch (xref)); + gs_app_add_source (app, ref_tmp); + gs_plugin_refine_item_scope (self, app); + + /* flatpak specific */ + gs_flatpak_app_set_ref_kind (app, flatpak_ref_get_kind (xref)); + gs_flatpak_app_set_ref_name (app, flatpak_ref_get_name (xref)); + gs_flatpak_app_set_ref_arch (app, flatpak_ref_get_arch (xref)); + gs_flatpak_app_set_commit (app, flatpak_ref_get_commit (xref)); + + /* map the flatpak kind to the gnome-software kind */ + if (gs_app_get_kind (app) == AS_APP_KIND_UNKNOWN || + gs_app_get_kind (app) == AS_APP_KIND_GENERIC) { + gs_flatpak_set_kind_from_flatpak (app, xref); + } +} + +static GsApp * +gs_flatpak_create_app (GsFlatpak *self, const gchar *origin, FlatpakRef *xref) +{ + GsApp *app_cached; + g_autoptr(GsApp) app = NULL; + + /* create a temp GsApp */ + app = gs_app_new (flatpak_ref_get_name (xref)); + gs_flatpak_set_metadata (self, app, xref); + if (origin != NULL) + gs_app_set_origin (app, origin); + + /* return the ref'd cached copy */ + app_cached = gs_plugin_cache_lookup (self->plugin, gs_app_get_unique_id (app)); + if (app_cached != NULL) + return app_cached; + + /* fallback values */ + if (gs_flatpak_app_get_ref_kind (app) == FLATPAK_REF_KIND_RUNTIME) { + g_autoptr(AsIcon) icon = NULL; + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, + flatpak_ref_get_name (FLATPAK_REF (xref))); + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, + "Framework for applications"); + gs_app_set_version (app, flatpak_ref_get_branch (FLATPAK_REF (xref))); + icon = as_icon_new (); + as_icon_set_kind (icon, AS_ICON_KIND_STOCK); + as_icon_set_name (icon, "system-run-symbolic"); + gs_app_add_icon (app, icon); + } + + /* Don't add NULL origin apps to the cache. If the app is later set to + * origin x the cache may return it as a match for origin y since the cache + * hash table uses as_utils_unique_id_equal() as the equal func and a NULL + * origin becomes a "*" in as_utils_unique_id_build(). + */ + if (origin != NULL) + gs_plugin_cache_add (self->plugin, NULL, app); + + /* no existing match, just steal the temp object */ + return g_steal_pointer (&app); +} + +static GsApp * +gs_flatpak_create_source (GsFlatpak *self, FlatpakRemote *xremote) +{ + GsApp *app_cached; + g_autoptr(GsApp) app = NULL; + + /* create a temp GsApp */ + app = gs_flatpak_app_new_from_remote (xremote); + gs_flatpak_claim_app (self, app); + + /* we already have one, returned the ref'd cached copy */ + app_cached = gs_plugin_cache_lookup (self->plugin, gs_app_get_unique_id (app)); + if (app_cached != NULL) + return app_cached; + + /* no existing match, just steal the temp object */ + gs_plugin_cache_add (self->plugin, NULL, app); + return g_steal_pointer (&app); +} + +static void +gs_plugin_flatpak_changed_cb (GFileMonitor *monitor, + GFile *child, + GFile *other_file, + GFileMonitorEvent event_type, + GsFlatpak *self) +{ + g_autoptr(GError) error = NULL; + g_autoptr(GMutexLocker) locker = NULL; + + /* manually drop the cache */ + if (!flatpak_installation_drop_caches (self->installation, + NULL, &error)) { + g_warning ("failed to drop cache: %s", error->message); + return; + } + + /* drop the installed refs cache */ + locker = g_mutex_locker_new (&self->installed_refs_mutex); + g_clear_pointer (&self->installed_refs, g_ptr_array_unref); +} + +static gboolean +gs_flatpak_add_flatpak_keyword_cb (XbBuilderFixup *self, + XbBuilderNode *bn, + gpointer user_data, + GError **error) +{ + if (g_strcmp0 (xb_builder_node_get_element (bn), "component") == 0) + gs_appstream_component_add_keyword (bn, "flatpak"); + return TRUE; +} + +static gboolean +gs_flatpak_fix_id_desktop_suffix_cb (XbBuilderFixup *self, + XbBuilderNode *bn, + gpointer user_data, + GError **error) +{ + if (g_strcmp0 (xb_builder_node_get_element (bn), "component") == 0) { + g_auto(GStrv) split = NULL; + g_autoptr(XbBuilderNode) id = xb_builder_node_get_child (bn, "id", NULL); + g_autoptr(XbBuilderNode) bundle = xb_builder_node_get_child (bn, "bundle", NULL); + if (id == NULL || bundle == NULL) + return TRUE; + split = g_strsplit (xb_builder_node_get_text (bundle), "/", -1); + if (g_strv_length (split) != 4) + return TRUE; + if (g_strcmp0 (xb_builder_node_get_text (id), split[1]) != 0) { + g_debug ("fixing up <id>%s</id> to %s", + xb_builder_node_get_text (id), split[1]); + gs_appstream_component_add_provide (bn, xb_builder_node_get_text (id)); + xb_builder_node_set_text (id, split[1], -1); + } + } + return TRUE; +} + +static gboolean +gs_flatpak_add_bundle_tag_cb (XbBuilderFixup *self, + XbBuilderNode *bn, + gpointer user_data, + GError **error) +{ + const char *app_ref = (char *)user_data; + if (g_strcmp0 (xb_builder_node_get_element (bn), "component") == 0) { + g_autoptr(XbBuilderNode) id = xb_builder_node_get_child (bn, "id", NULL); + g_autoptr(XbBuilderNode) bundle = xb_builder_node_get_child (bn, "bundle", NULL); + if (id == NULL || bundle != NULL) + return TRUE; + g_debug ("adding <bundle> tag for %s", app_ref); + xb_builder_node_insert_text (bn, "bundle", app_ref, "type", "flatpak", NULL); + } + return TRUE; +} + +static gboolean +gs_flatpak_fix_metadata_tag_cb (XbBuilderFixup *self, + XbBuilderNode *bn, + gpointer user_data, + GError **error) +{ + if (g_strcmp0 (xb_builder_node_get_element (bn), "component") == 0) { + g_autoptr(XbBuilderNode) metadata = xb_builder_node_get_child (bn, "metadata", NULL); + if (metadata != NULL) + xb_builder_node_set_element (metadata, "custom"); + } + return TRUE; +} + +static gboolean +gs_flatpak_set_origin_cb (XbBuilderFixup *self, + XbBuilderNode *bn, + gpointer user_data, + GError **error) +{ + const char *remote_name = (char *)user_data; + if (g_strcmp0 (xb_builder_node_get_element (bn), "components") == 0) { + xb_builder_node_set_attr (bn, "origin", + remote_name); + } + return TRUE; +} + +static gboolean +gs_flatpak_filter_default_branch_cb (XbBuilderFixup *self, + XbBuilderNode *bn, + gpointer user_data, + GError **error) +{ + const gchar *default_branch = (const gchar *) user_data; + if (g_strcmp0 (xb_builder_node_get_element (bn), "component") == 0) { + g_autoptr(XbBuilderNode) bc = xb_builder_node_get_child (bn, "bundle", NULL); + g_auto(GStrv) split = NULL; + if (bc == NULL) { + g_debug ("no bundle for component"); + return TRUE; + } + split = g_strsplit (xb_builder_node_get_text (bc), "/", -1); + if (split == NULL || g_strv_length (split) != 4) + return TRUE; + if (g_strcmp0 (split[3], default_branch) != 0) { + g_debug ("not adding app with branch %s as filtering to %s", + split[3], default_branch); + xb_builder_node_add_flag (bn, XB_BUILDER_NODE_FLAG_IGNORE); + } + } + return TRUE; +} + +static gboolean +gs_flatpak_filter_noenumerate_cb (XbBuilderFixup *self, + XbBuilderNode *bn, + gpointer user_data, + GError **error) +{ + const gchar *main_ref = (const gchar *) user_data; + + if (g_strcmp0 (xb_builder_node_get_element (bn), "component") == 0) { + g_autoptr(XbBuilderNode) bc = xb_builder_node_get_child (bn, "bundle", NULL); + if (bc == NULL) { + g_debug ("no bundle for component"); + return TRUE; + } + if (g_strcmp0 (xb_builder_node_get_text (bc), main_ref) != 0) { + g_debug ("not adding app %s as filtering to %s", + xb_builder_node_get_text (bc), main_ref); + xb_builder_node_add_flag (bn, XB_BUILDER_NODE_FLAG_IGNORE); + } + } + return TRUE; +} + +#if !FLATPAK_CHECK_VERSION(1,1,1) +static gchar * +gs_flatpak_get_xremote_main_ref (GsFlatpak *self, FlatpakRemote *xremote, GError **error) +{ + g_autoptr(GFile) dir = NULL; + g_autofree gchar *dir_path = NULL; + g_autofree gchar *config_fn = NULL; + g_autofree gchar *group = NULL; + g_autofree gchar *main_ref = NULL; + g_autoptr(GKeyFile) kf = NULL; + + /* figure out the path to the config keyfile */ + dir = flatpak_installation_get_path (self->installation); + if (dir == NULL) + return NULL; + dir_path = g_file_get_path (dir); + if (dir_path == NULL) + return NULL; + config_fn = g_build_filename (dir_path, "repo", "config", NULL); + + kf = g_key_file_new (); + if (!g_key_file_load_from_file (kf, config_fn, G_KEY_FILE_NONE, error)) + return NULL; + + group = g_strdup_printf ("remote \"%s\"", flatpak_remote_get_name (xremote)); + main_ref = g_key_file_get_string (kf, group, "xa.main-ref", error); + return g_steal_pointer (&main_ref); +} +#endif + +static void +fixup_flatpak_appstream_xml (XbBuilderSource *source, + const char *origin) +{ + g_autoptr(XbBuilderFixup) fixup1 = NULL; + g_autoptr(XbBuilderFixup) fixup2 = NULL; + g_autoptr(XbBuilderFixup) fixup3 = NULL; + + /* add the flatpak search keyword */ + fixup1 = xb_builder_fixup_new ("AddKeywordFlatpak", + gs_flatpak_add_flatpak_keyword_cb, + NULL, NULL); + xb_builder_fixup_set_max_depth (fixup1, 2); + xb_builder_source_add_fixup (source, fixup1); + + /* ensure the <id> matches the flatpak ref ID */ + fixup2 = xb_builder_fixup_new ("FixIdDesktopSuffix", + gs_flatpak_fix_id_desktop_suffix_cb, + NULL, NULL); + xb_builder_fixup_set_max_depth (fixup2, 2); + xb_builder_source_add_fixup (source, fixup2); + + /* Fixup <metadata> to <custom> for appstream versions >= 0.9 */ + fixup3 = xb_builder_fixup_new ("FixMetadataTag", + gs_flatpak_fix_metadata_tag_cb, + NULL, NULL); + xb_builder_fixup_set_max_depth (fixup3, 2); + xb_builder_source_add_fixup (source, fixup3); + + if (origin != NULL) { + g_autoptr(XbBuilderFixup) fixup4 = NULL; + + /* override the *AppStream* origin */ + fixup4 = xb_builder_fixup_new ("SetOrigin", + gs_flatpak_set_origin_cb, + g_strdup (origin), g_free); + xb_builder_fixup_set_max_depth (fixup4, 1); + xb_builder_source_add_fixup (source, fixup4); + } +} + +static gboolean +gs_flatpak_add_apps_from_xremote (GsFlatpak *self, + XbBuilder *builder, + FlatpakRemote *xremote, + GCancellable *cancellable, + GError **error) +{ + g_autofree gchar *appstream_dir_fn = NULL; + g_autofree gchar *appstream_fn = NULL; + g_autofree gchar *icon_prefix = NULL; + g_autofree gchar *default_branch = NULL; + g_autoptr(GFile) appstream_dir = NULL; + g_autoptr(GFile) file_xml = NULL; + g_autoptr(GSettings) settings = NULL; + g_autoptr(XbBuilderNode) info = NULL; + g_autoptr(XbBuilderSource) source = xb_builder_source_new (); + + /* get the AppStream data location */ + appstream_dir = flatpak_remote_get_appstream_dir (xremote, NULL); + if (appstream_dir == NULL) { + g_debug ("no appstream dir for %s, skipping", + flatpak_remote_get_name (xremote)); + return TRUE; + } + + /* load the file into a temp silo */ + appstream_dir_fn = g_file_get_path (appstream_dir); + appstream_fn = g_build_filename (appstream_dir_fn, "appstream.xml.gz", NULL); + if (!g_file_test (appstream_fn, G_FILE_TEST_EXISTS)) { + g_debug ("no %s appstream metadata found: %s", + flatpak_remote_get_name (xremote), + appstream_fn); + return TRUE; + } + + /* add source */ + file_xml = g_file_new_for_path (appstream_fn); + if (!xb_builder_source_load_file (source, file_xml, + XB_BUILDER_SOURCE_FLAG_WATCH_FILE | + XB_BUILDER_SOURCE_FLAG_LITERAL_TEXT, + cancellable, + error)) + return FALSE; + + fixup_flatpak_appstream_xml (source, flatpak_remote_get_name (xremote)); + + /* add metadata */ + icon_prefix = g_build_filename (appstream_dir_fn, "icons", NULL); + info = xb_builder_node_insert (NULL, "info", NULL); + xb_builder_node_insert_text (info, "scope", as_app_scope_to_string (self->scope), NULL); + xb_builder_node_insert_text (info, "icon-prefix", icon_prefix, NULL); + xb_builder_source_set_info (source, info); + + /* only add the specific app for noenumerate=true */ + if (flatpak_remote_get_noenumerate (xremote)) { + g_autofree gchar *main_ref = NULL; +#if FLATPAK_CHECK_VERSION(1,1,1) + main_ref = flatpak_remote_get_main_ref (xremote); +#else + g_autoptr(GError) error_local = NULL; + main_ref = gs_flatpak_get_xremote_main_ref (self, xremote, &error_local); + if (main_ref == NULL) { + g_warning ("failed to get main ref: %s", error_local->message); + g_clear_error (&error_local); + } +#endif + if (main_ref != NULL) { + g_autoptr(XbBuilderFixup) fixup = NULL; + fixup = xb_builder_fixup_new ("FilterNoEnumerate", + gs_flatpak_filter_noenumerate_cb, + g_strdup (main_ref), + g_free); + xb_builder_fixup_set_max_depth (fixup, 2); + xb_builder_source_add_fixup (source, fixup); + } + } + + /* do we want to filter to the default branch */ + settings = g_settings_new ("org.gnome.software"); + default_branch = flatpak_remote_get_default_branch (xremote); + if (g_settings_get_boolean (settings, "filter-default-branch") && + default_branch != NULL) { + g_autoptr(XbBuilderFixup) fixup = NULL; + fixup = xb_builder_fixup_new ("FilterDefaultbranch", + gs_flatpak_filter_default_branch_cb, + flatpak_remote_get_default_branch (xremote), + g_free); + xb_builder_fixup_set_max_depth (fixup, 2); + xb_builder_source_add_fixup (source, fixup); + } + + /* success */ + xb_builder_import_source (builder, source); + return TRUE; +} + +static GInputStream * +gs_plugin_appstream_load_desktop_cb (XbBuilderSource *self, + XbBuilderSourceCtx *ctx, + gpointer user_data, + GCancellable *cancellable, + GError **error) +{ + 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_flatpak_load_desktop_fn (GsFlatpak *self, + XbBuilder *builder, + const gchar *filename, + const gchar *icon_prefix, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GFile) file = g_file_new_for_path (filename); + g_autoptr(XbBuilderNode) info = NULL; + g_autoptr(XbBuilderSource) source = xb_builder_source_new (); + g_autoptr(XbBuilderFixup) fixup = NULL; + + /* add support for desktop files */ + xb_builder_source_add_adapter (source, "application/x-desktop", + gs_plugin_appstream_load_desktop_cb, NULL, NULL); + + /* add the flatpak search keyword */ + fixup = xb_builder_fixup_new ("AddKeywordFlatpak", + gs_flatpak_add_flatpak_keyword_cb, + self, NULL); + xb_builder_fixup_set_max_depth (fixup, 2); + xb_builder_source_add_fixup (source, fixup); + + /* set the component metadata */ + info = xb_builder_node_insert (NULL, "info", NULL); + xb_builder_node_insert_text (info, "scope", as_app_scope_to_string (self->scope), NULL); + xb_builder_node_insert_text (info, "icon-prefix", icon_prefix, NULL); + xb_builder_source_set_info (source, info); + + /* add source */ + if (!xb_builder_source_load_file (source, file, +#if LIBXMLB_CHECK_VERSION(0, 2, 0) + XB_BUILDER_SOURCE_FLAG_WATCH_DIRECTORY, +#else + XB_BUILDER_SOURCE_FLAG_WATCH_FILE, +#endif + cancellable, + error)) { + return FALSE; + } + + /* success */ + xb_builder_import_source (builder, source); + return TRUE; +} + +static void +gs_flatpak_rescan_installed (GsFlatpak *self, + XbBuilder *builder, + GCancellable *cancellable, + GError **error) +{ + const gchar *fn; + g_autoptr(GFile) path = NULL; + g_autoptr(GDir) dir = NULL; + g_autofree gchar *path_str = NULL; + g_autofree gchar *path_exports = NULL; + g_autofree gchar *path_apps = NULL; + + /* add all installed desktop files */ + path = flatpak_installation_get_path (self->installation); + path_str = g_file_get_path (path); + path_exports = g_build_filename (path_str, "exports", NULL); + path_apps = g_build_filename (path_exports, "share", "applications", NULL); + dir = g_dir_open (path_apps, 0, NULL); + if (dir == NULL) + return; + while ((fn = g_dir_read_name (dir)) != NULL) { + g_autofree gchar *filename = NULL; + g_autoptr(GError) error_local = NULL; + + /* ignore */ + if (g_strcmp0 (fn, "mimeinfo.cache") == 0) + continue; + + /* parse desktop files */ + filename = g_build_filename (path_apps, fn, NULL); + if (!gs_flatpak_load_desktop_fn (self, + builder, + filename, + path_exports, + cancellable, + &error_local)) { + g_debug ("ignoring %s: %s", filename, error_local->message); + continue; + } + } +} + +static gboolean +gs_flatpak_rescan_appstream_store (GsFlatpak *self, + GCancellable *cancellable, + GError **error) +{ + const gchar *const *locales = g_get_language_names (); + g_autofree gchar *blobfn = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GPtrArray) xremotes = NULL; + g_autoptr(GRWLockReaderLocker) reader_locker = NULL; + g_autoptr(GRWLockWriterLocker) writer_locker = NULL; + g_autoptr(XbBuilder) builder = xb_builder_new (); + + reader_locker = g_rw_lock_reader_locker_new (&self->silo_lock); + /* everything is okay */ + if (self->silo != NULL && xb_silo_is_valid (self->silo)) + return TRUE; + g_clear_pointer (&reader_locker, g_rw_lock_reader_locker_free); + + /* drat! silo needs regenerating */ + writer_locker = g_rw_lock_writer_locker_new (&self->silo_lock); + g_clear_object (&self->silo); + + /* verbose profiling */ + if (g_getenv ("GS_XMLB_VERBOSE") != NULL) { + xb_builder_set_profile_flags (builder, + XB_SILO_PROFILE_FLAG_XPATH | + XB_SILO_PROFILE_FLAG_DEBUG); + } + + /* add current locales */ + for (guint i = 0; locales[i] != NULL; i++) + xb_builder_add_locale (builder, locales[i]); + + /* go through each remote adding metadata */ + xremotes = flatpak_installation_list_remotes (self->installation, + cancellable, + error); + if (xremotes == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + for (guint i = 0; i < xremotes->len; i++) { + g_autoptr(GError) error_local = NULL; + FlatpakRemote *xremote = g_ptr_array_index (xremotes, i); + if (flatpak_remote_get_disabled (xremote)) + continue; + g_debug ("found remote %s", + flatpak_remote_get_name (xremote)); + if (!gs_flatpak_add_apps_from_xremote (self, builder, xremote, cancellable, &error_local)) { + g_debug ("Failed to add apps from remote ‘%s’; skipping: %s", + flatpak_remote_get_name (xremote), error_local->message); + } + } + + /* add any installed files without AppStream info */ + gs_flatpak_rescan_installed (self, builder, cancellable, error); + + /* create per-user cache */ + blobfn = gs_utils_get_cache_filename (gs_flatpak_get_id (self), + "components.xmlb", + GS_UTILS_CACHE_FLAG_WRITEABLE, + error); + if (blobfn == NULL) + return FALSE; + file = g_file_new_for_path (blobfn); + g_debug ("ensuring %s", blobfn); + self->silo = xb_builder_ensure (builder, file, + XB_BUILDER_COMPILE_FLAG_IGNORE_INVALID | + XB_BUILDER_COMPILE_FLAG_SINGLE_LANG, + NULL, error); + if (self->silo == NULL) + return FALSE; + + /* success */ + return TRUE; +} + +gboolean +gs_flatpak_setup (GsFlatpak *self, GCancellable *cancellable, GError **error) +{ + /* watch for changes */ + self->monitor = flatpak_installation_create_monitor (self->installation, + cancellable, + error); + if (self->monitor == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + self->changed_id = + g_signal_connect (self->monitor, "changed", + G_CALLBACK (gs_plugin_flatpak_changed_cb), self); + + /* success */ + return TRUE; +} + +typedef struct { + GsPlugin *plugin; + GsApp *app; +} GsFlatpakProgressHelper; + +static void +gs_flatpak_progress_helper_free (GsFlatpakProgressHelper *phelper) +{ + g_object_unref (phelper->plugin); + if (phelper->app != NULL) + g_object_unref (phelper->app); + g_slice_free (GsFlatpakProgressHelper, phelper); +} + +static GsFlatpakProgressHelper * +gs_flatpak_progress_helper_new (GsPlugin *plugin, GsApp *app) +{ + GsFlatpakProgressHelper *phelper; + phelper = g_slice_new0 (GsFlatpakProgressHelper); + phelper->plugin = g_object_ref (plugin); + if (app != NULL) + phelper->app = g_object_ref (app); + return phelper; +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(GsFlatpakProgressHelper, gs_flatpak_progress_helper_free) + +static void +gs_flatpak_progress_cb (const gchar *status, + guint progress, + gboolean estimating, + gpointer user_data) +{ + GsFlatpakProgressHelper *phelper = (GsFlatpakProgressHelper *) user_data; + GsPluginStatus plugin_status = GS_PLUGIN_STATUS_DOWNLOADING; + + if (phelper->app != NULL) { + if (estimating) + gs_app_set_progress (phelper->app, GS_APP_PROGRESS_UNKNOWN); + else + gs_app_set_progress (phelper->app, progress); + + switch (gs_app_get_state (phelper->app)) { + case AS_APP_STATE_INSTALLING: + plugin_status = GS_PLUGIN_STATUS_INSTALLING; + break; + case AS_APP_STATE_REMOVING: + plugin_status = GS_PLUGIN_STATUS_REMOVING; + break; + default: + break; + } + } + gs_plugin_status_update (phelper->plugin, phelper->app, plugin_status); +} + +static gboolean +gs_flatpak_refresh_appstream_remote (GsFlatpak *self, + const gchar *remote_name, + GCancellable *cancellable, + GError **error) +{ + g_autofree gchar *str = NULL; + g_autoptr(GsApp) app_dl = gs_app_new (gs_plugin_get_name (self->plugin)); + g_autoptr(GsFlatpakProgressHelper) phelper = NULL; + g_autoptr(GError) error_local = NULL; + + /* TRANSLATORS: status text when downloading new metadata */ + str = g_strdup_printf (_("Getting flatpak metadata for %s…"), remote_name); + gs_app_set_summary_missing (app_dl, str); + gs_plugin_status_update (self->plugin, app_dl, GS_PLUGIN_STATUS_DOWNLOADING); + + if (!flatpak_installation_update_remote_sync (self->installation, + remote_name, + cancellable, + &error_local)) { + g_debug ("Failed to update metadata for remote %s: %s\n", + remote_name, error_local->message); + gs_flatpak_error_convert (&error_local); + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + phelper = gs_flatpak_progress_helper_new (self->plugin, app_dl); + if (!flatpak_installation_update_appstream_full_sync (self->installation, + remote_name, + NULL, /* arch */ + gs_flatpak_progress_cb, + phelper, + NULL, /* out_changed */ + cancellable, + error)) { + gs_flatpak_error_convert (error); + return FALSE; + } + + /* success */ + gs_app_set_progress (app_dl, 100); + return TRUE; +} + +static gboolean +gs_flatpak_refresh_appstream (GsFlatpak *self, guint cache_age, + GCancellable *cancellable, GError **error) +{ + gboolean ret; + g_autoptr(GPtrArray) xremotes = NULL; + + /* get remotes */ + xremotes = flatpak_installation_list_remotes (self->installation, + cancellable, + error); + if (xremotes == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + for (guint i = 0; i < xremotes->len; i++) { + const gchar *remote_name; + guint tmp; + g_autoptr(GError) error_local = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GFile) file_timestamp = NULL; + g_autofree gchar *appstream_fn = NULL; + FlatpakRemote *xremote = g_ptr_array_index (xremotes, i); + g_autoptr(GMutexLocker) locker = NULL; + + /* not enabled */ + if (flatpak_remote_get_disabled (xremote)) + continue; + + locker = g_mutex_locker_new (&self->broken_remotes_mutex); + + /* skip known-broken repos */ + remote_name = flatpak_remote_get_name (xremote); + if (g_hash_table_lookup (self->broken_remotes, remote_name) != NULL) { + g_debug ("skipping known broken remote: %s", remote_name); + continue; + } + + /* is the timestamp new enough */ + file_timestamp = flatpak_remote_get_appstream_timestamp (xremote, NULL); + tmp = gs_utils_get_file_age (file_timestamp); + if (tmp < cache_age) { + g_autofree gchar *fn = g_file_get_path (file_timestamp); + g_debug ("%s is only %u seconds old, so ignoring refresh", + fn, tmp); + continue; + } + + /* download new data */ + g_debug ("%s is %u seconds old, so downloading new data", + remote_name, tmp); + ret = gs_flatpak_refresh_appstream_remote (self, + remote_name, + cancellable, + &error_local); + if (!ret) { + g_autoptr(GsPluginEvent) event = NULL; + if (g_error_matches (error_local, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED)) { + g_debug ("Failed to get AppStream metadata: %s", + error_local->message); + /* don't try to fetch this again until refresh() */ + g_hash_table_insert (self->broken_remotes, + g_strdup (remote_name), + GUINT_TO_POINTER (1)); + continue; + } + + /* allow the plugin loader to decide if this should be + * shown the user, possibly only for interactive jobs */ + event = gs_plugin_event_new (); + gs_flatpak_error_convert (&error_local); + gs_plugin_event_set_error (event, error_local); + gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING); + gs_plugin_report_event (self->plugin, event); + continue; + } + + /* add the new AppStream repo to the shared silo */ + file = flatpak_remote_get_appstream_dir (xremote, NULL); + appstream_fn = g_file_get_path (file); + g_debug ("using AppStream metadata found at: %s", appstream_fn); + } + + /* ensure the AppStream silo is up to date */ + if (!gs_flatpak_rescan_appstream_store (self, cancellable, error)) + return FALSE; + + return TRUE; +} + +static void +gs_flatpak_set_metadata_installed (GsFlatpak *self, GsApp *app, + FlatpakInstalledRef *xref) +{ +#if FLATPAK_CHECK_VERSION(1,1,3) + const gchar *appdata_version; +#endif + guint64 mtime; + guint64 size_installed; + g_autofree gchar *metadata_fn = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GFileInfo) info = NULL; + + /* for all types */ + gs_flatpak_set_metadata (self, app, FLATPAK_REF (xref)); + if (gs_app_get_metadata_item (app, "GnomeSoftware::Creator") == NULL) { + gs_app_set_metadata (app, "GnomeSoftware::Creator", + gs_plugin_get_name (self->plugin)); + } + + /* get the last time the app was updated */ + metadata_fn = g_build_filename (flatpak_installed_ref_get_deploy_dir (xref), + "..", + "active", + "metadata", + NULL); + file = g_file_new_for_path (metadata_fn); + info = g_file_query_info (file, + G_FILE_ATTRIBUTE_TIME_MODIFIED, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + NULL, NULL); + if (info != NULL) { + mtime = g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED); + gs_app_set_install_date (app, mtime); + } + + /* If it's a runtime, check if the main-app info should be set. Note that + * checking the app for AS_APP_KIND_RUNTIME is not good enough because it + * could be e.g. AS_APP_KIND_LOCALIZATION and still be a runtime from + * Flatpak's perspective. + */ + if (gs_flatpak_app_get_ref_kind (app) == FLATPAK_REF_KIND_RUNTIME && + gs_flatpak_app_get_main_app_ref_name (app) == NULL) { + g_autoptr(GError) error = NULL; + g_autoptr(GKeyFile) metadata_file = NULL; + metadata_file = g_key_file_new (); + if (g_key_file_load_from_file (metadata_file, metadata_fn, + G_KEY_FILE_NONE, &error)) { + g_autofree gchar *main_app = g_key_file_get_string (metadata_file, + "ExtensionOf", + "ref", NULL); + if (main_app != NULL) + gs_flatpak_app_set_main_app_ref_name (app, main_app); + } else { + g_warning ("Error loading the metadata file for '%s': %s", + gs_app_get_unique_id (app), error->message); + } + } + + /* this is faster than resolving */ + if (gs_app_get_origin (app) == NULL) + gs_app_set_origin (app, flatpak_installed_ref_get_origin (xref)); + + /* this is faster than flatpak_installation_fetch_remote_size_sync() */ + size_installed = flatpak_installed_ref_get_installed_size (xref); + if (size_installed != 0) + gs_app_set_size_installed (app, size_installed); + +#if FLATPAK_CHECK_VERSION(1,1,3) + appdata_version = flatpak_installed_ref_get_appdata_version (xref); + if (appdata_version != NULL) + gs_app_set_version (app, appdata_version); +#endif +} + +static GsApp * +gs_flatpak_create_installed (GsFlatpak *self, + FlatpakInstalledRef *xref) +{ + g_autoptr(GsApp) app = NULL; + const gchar *origin; + + g_return_val_if_fail (xref != NULL, NULL); + + /* create new object */ + origin = flatpak_installed_ref_get_origin (xref); + app = gs_flatpak_create_app (self, origin, FLATPAK_REF (xref)); + if (gs_app_get_state (app) == AS_APP_STATE_UNKNOWN) + gs_app_set_state (app, AS_APP_STATE_INSTALLED); + gs_flatpak_set_metadata_installed (self, app, xref); + return g_steal_pointer (&app); +} + +gboolean +gs_flatpak_add_installed (GsFlatpak *self, GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GPtrArray) xrefs = NULL; + + /* get apps and runtimes */ + xrefs = flatpak_installation_list_installed_refs (self->installation, + cancellable, error); + if (xrefs == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + for (guint i = 0; i < xrefs->len; i++) { + FlatpakInstalledRef *xref = g_ptr_array_index (xrefs, i); + g_autoptr(GsApp) app = gs_flatpak_create_installed (self, xref); + gs_app_list_add (list, app); + } + + return TRUE; +} + +gboolean +gs_flatpak_add_sources (GsFlatpak *self, GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GPtrArray) xrefs = NULL; + g_autoptr(GPtrArray) xremotes = NULL; + + /* refresh */ + if (!gs_flatpak_rescan_appstream_store (self, cancellable, error)) + return FALSE; + + /* get installed apps and runtimes */ + xrefs = flatpak_installation_list_installed_refs (self->installation, + cancellable, + error); + if (xrefs == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + + /* get available remotes */ + xremotes = flatpak_installation_list_remotes (self->installation, + cancellable, + error); + if (xremotes == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + for (guint i = 0; i < xremotes->len; i++) { + FlatpakRemote *xremote = g_ptr_array_index (xremotes, i); + g_autoptr(GsApp) app = NULL; + + /* apps installed from bundles add their own remote that only + * can be used for updating that app only -- so hide them */ + if (flatpak_remote_get_noenumerate (xremote)) + continue; + + /* create app */ + app = gs_flatpak_create_source (self, xremote); + gs_app_list_add (list, app); + + /* add related apps, i.e. what was installed from there */ + for (guint j = 0; j < xrefs->len; j++) { + FlatpakInstalledRef *xref = g_ptr_array_index (xrefs, j); + g_autoptr(GsApp) related = NULL; + + /* only apps */ + if (flatpak_ref_get_kind (FLATPAK_REF (xref)) != FLATPAK_REF_KIND_APP) + continue; + if (g_strcmp0 (flatpak_installed_ref_get_origin (xref), + flatpak_remote_get_name (xremote)) != 0) + continue; + related = gs_flatpak_create_installed (self, xref); + gs_app_add_related (app, related); + } + } + return TRUE; +} + +GsApp * +gs_flatpak_find_source_by_url (GsFlatpak *self, + const gchar *url, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GPtrArray) xremotes = NULL; + + g_return_val_if_fail (url != NULL, NULL); + + xremotes = flatpak_installation_list_remotes (self->installation, cancellable, error); + if (xremotes == NULL) + return NULL; + for (guint i = 0; i < xremotes->len; i++) { + FlatpakRemote *xremote = g_ptr_array_index (xremotes, i); + g_autofree gchar *url_tmp = flatpak_remote_get_url (xremote); + if (g_strcmp0 (url, url_tmp) == 0) + return gs_flatpak_create_source (self, xremote); + } + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "cannot find %s", url); + return NULL; +} + +/* transfer full */ +GsApp * +gs_flatpak_ref_to_app (GsFlatpak *self, const gchar *ref, + GCancellable *cancellable, GError **error) +{ + g_autoptr(GPtrArray) xremotes = NULL; + g_autoptr(GPtrArray) xrefs = NULL; + + g_return_val_if_fail (ref != NULL, NULL); + + /* get all the installed apps (no network I/O) */ + xrefs = flatpak_installation_list_installed_refs (self->installation, + cancellable, + error); + if (xrefs == NULL) { + gs_flatpak_error_convert (error); + return NULL; + } + for (guint i = 0; i < xrefs->len; i++) { + FlatpakInstalledRef *xref = g_ptr_array_index (xrefs, i); + g_autofree gchar *ref_tmp = flatpak_ref_format_ref (FLATPAK_REF (xref)); + if (g_strcmp0 (ref, ref_tmp) == 0) + return gs_flatpak_create_installed (self, xref); + } + + /* look at each remote xref */ + xremotes = flatpak_installation_list_remotes (self->installation, + cancellable, error); + if (xremotes == NULL) { + gs_flatpak_error_convert (error); + return NULL; + } + for (guint i = 0; i < xremotes->len; i++) { + FlatpakRemote *xremote = g_ptr_array_index (xremotes, i); + g_autoptr(GError) error_local = NULL; + g_autoptr(GPtrArray) refs_remote = NULL; + + /* disabled */ + if (flatpak_remote_get_disabled (xremote)) + continue; + refs_remote = flatpak_installation_list_remote_refs_sync (self->installation, + flatpak_remote_get_name (xremote), + cancellable, + &error_local); + if (refs_remote == NULL) { + g_debug ("failed to list refs in '%s': %s", + flatpak_remote_get_name (xremote), + error_local->message); + continue; + } + for (guint j = 0; j < refs_remote->len; j++) { + FlatpakRef *xref = g_ptr_array_index (refs_remote, j); + g_autofree gchar *ref_tmp = flatpak_ref_format_ref (xref); + if (g_strcmp0 (ref, ref_tmp) == 0) { + const gchar *origin = flatpak_remote_get_name (xremote); + return gs_flatpak_create_app (self, origin, xref); + } + } + } + + /* nothing found */ + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "cannot find %s", ref); + return NULL; +} + +static FlatpakRemote * +gs_flatpak_create_new_remote (GsFlatpak *self, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + const gchar *gpg_key; + const gchar *branch; + g_autoptr(FlatpakRemote) xremote = NULL; + + /* create a new remote */ + xremote = flatpak_remote_new (gs_app_get_id (app)); + flatpak_remote_set_url (xremote, gs_flatpak_app_get_repo_url (app)); + flatpak_remote_set_noenumerate (xremote, FALSE); + if (gs_app_get_summary (app) != NULL) + flatpak_remote_set_title (xremote, gs_app_get_summary (app)); + + /* decode GPG key if set */ + gpg_key = gs_flatpak_app_get_repo_gpgkey (app); + if (gpg_key != NULL) { + gsize data_len = 0; + g_autofree guchar *data = NULL; + g_autoptr(GBytes) bytes = NULL; + data = g_base64_decode (gpg_key, &data_len); + bytes = g_bytes_new (data, data_len); + flatpak_remote_set_gpg_verify (xremote, TRUE); + flatpak_remote_set_gpg_key (xremote, bytes); + } else { + flatpak_remote_set_gpg_verify (xremote, FALSE); + } + + /* default branch */ + branch = gs_app_get_branch (app); + if (branch != NULL) + flatpak_remote_set_default_branch (xremote, branch); + + return g_steal_pointer (&xremote); +} + +gboolean +gs_flatpak_app_install_source (GsFlatpak *self, GsApp *app, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(FlatpakRemote) xremote = NULL; + + xremote = flatpak_installation_get_remote_by_name (self->installation, + gs_app_get_id (app), + cancellable, NULL); + if (xremote != NULL) { + /* if the remote already exists, just enable it */ + g_debug ("enabling existing remote %s", flatpak_remote_get_name (xremote)); + flatpak_remote_set_disabled (xremote, FALSE); + } else { + /* create a new remote */ + xremote = gs_flatpak_create_new_remote (self, app, cancellable, error); + } + + /* install it */ + gs_app_set_state (app, AS_APP_STATE_INSTALLING); + if (!flatpak_installation_modify_remote (self->installation, + xremote, + cancellable, + error)) { + gs_flatpak_error_convert (error); + g_prefix_error (error, "cannot modify remote: "); + gs_app_set_state_recover (app); + return FALSE; + } + + /* invalidate cache */ + g_rw_lock_reader_lock (&self->silo_lock); + if (self->silo != NULL) + xb_silo_invalidate (self->silo); + g_rw_lock_reader_unlock (&self->silo_lock); + + /* success */ + gs_app_set_state (app, AS_APP_STATE_INSTALLED); + return TRUE; +} + +static GsApp * +get_main_app_of_related (GsFlatpak *self, + GsApp *related_app, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(FlatpakInstalledRef) ref = NULL; + const gchar *ref_name; + g_auto(GStrv) app_tokens = NULL; + + ref_name = gs_flatpak_app_get_main_app_ref_name (related_app); + if (ref_name == NULL) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "%s doesn't have a main app set to it.", + gs_app_get_unique_id (related_app)); + return NULL; + } + + app_tokens = g_strsplit (ref_name, "/", -1); + if (g_strv_length (app_tokens) != 4) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, + "The main app of %s has an invalid name: %s", + gs_app_get_unique_id (related_app), ref_name); + return NULL; + } + + /* this function only returns G_IO_ERROR_NOT_FOUND when the metadata file + * is missing, but if that's the case then things should have broken before + * this point */ + ref = flatpak_installation_get_installed_ref (self->installation, + FLATPAK_REF_KIND_APP, + app_tokens[1], + app_tokens[2], + app_tokens[3], + cancellable, + error); + if (ref == NULL) + return NULL; + + return gs_flatpak_create_installed (self, ref); +} + +static GsApp * +get_real_app_for_update (GsFlatpak *self, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsApp *main_app = NULL; + g_autoptr(GError) error_local = NULL; + + if (gs_flatpak_app_get_ref_kind (app) == FLATPAK_REF_KIND_RUNTIME) + main_app = get_main_app_of_related (self, app, cancellable, &error_local); + + if (main_app == NULL) { + /* not all runtimes are extensions, and in that case we get the + * not-found error, so we only report other types of errors */ + if (error_local != NULL && + !g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) { + g_propagate_error (error, g_steal_pointer (&error_local)); + gs_flatpak_error_convert (error); + return NULL; + } + + main_app = g_object_ref (app); + } else { + g_debug ("Related extension app %s of main app %s is updatable, so " + "setting the latter's state instead.", gs_app_get_unique_id (app), + gs_app_get_unique_id (main_app)); + gs_app_set_state (main_app, AS_APP_STATE_UPDATABLE_LIVE); + } + + return main_app; +} + +gboolean +gs_flatpak_add_updates (GsFlatpak *self, GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GPtrArray) xrefs = NULL; + + /* ensure valid */ + if (!gs_flatpak_rescan_appstream_store (self, cancellable, error)) + return FALSE; + + /* get all the updatable apps and runtimes */ + xrefs = flatpak_installation_list_installed_refs_for_update (self->installation, + cancellable, + error); + if (xrefs == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + + /* look at each installed xref */ + for (guint i = 0; i < xrefs->len; i++) { + FlatpakInstalledRef *xref = g_ptr_array_index (xrefs, i); + const gchar *commit; + const gchar *latest_commit; + g_autoptr(GsApp) app = NULL; + g_autoptr(GError) error_local = NULL; + g_autoptr(GsApp) main_app = NULL; + + /* check the application has already been downloaded */ + commit = flatpak_ref_get_commit (FLATPAK_REF (xref)); + latest_commit = flatpak_installed_ref_get_latest_commit (xref); + if (latest_commit == NULL) { + g_debug ("could not get latest commit for %s", + flatpak_ref_get_name (FLATPAK_REF (xref))); + continue; + } + + app = gs_flatpak_create_installed (self, xref); + main_app = get_real_app_for_update (self, app, cancellable, &error_local); + if (main_app == NULL) { + g_debug ("Couldn't get the main app for updatable app extension %s: " + "%s; adding the app itself to the updates list...", + gs_app_get_unique_id (app), error_local->message); + g_clear_error (&error_local); + main_app = g_object_ref (app); + } + + /* if for some reason the app is already getting updated, then + * don't change its state */ + if (gs_app_get_state (main_app) != AS_APP_STATE_INSTALLING) + gs_app_set_state (main_app, AS_APP_STATE_UPDATABLE_LIVE); + + /* set updatable state on the extension too, as it will have + * its state updated to installing then installed later on */ + if (gs_app_get_state (app) != AS_APP_STATE_INSTALLING) + gs_app_set_state (app, AS_APP_STATE_UPDATABLE_LIVE); + + /* already downloaded */ + if (g_strcmp0 (commit, latest_commit) != 0) { + g_debug ("%s has a downloaded update %s->%s", + flatpak_ref_get_name (FLATPAK_REF (xref)), + commit, latest_commit); + gs_app_set_update_details (main_app, NULL); + gs_app_set_update_version (main_app, NULL); + gs_app_set_update_urgency (main_app, AS_URGENCY_KIND_UNKNOWN); + gs_app_set_size_download (main_app, 0); + gs_app_list_add (list, main_app); + + /* needs download */ + } else { + guint64 download_size = 0; + g_debug ("%s needs update", + flatpak_ref_get_name (FLATPAK_REF (xref))); + + /* get the current download size */ + if (gs_app_get_size_download (main_app) == 0) { + if (!flatpak_installation_fetch_remote_size_sync (self->installation, + gs_app_get_origin (app), + FLATPAK_REF (xref), + &download_size, + NULL, + cancellable, + &error_local)) { + g_warning ("failed to get download size: %s", + error_local->message); + g_clear_error (&error_local); + gs_app_set_size_download (main_app, GS_APP_SIZE_UNKNOWABLE); + } else { + gs_app_set_size_download (main_app, download_size); + } + } + } + gs_flatpak_set_update_permissions (self, main_app, xref); + gs_app_list_add (list, main_app); + } + + /* success */ + return TRUE; +} + +gboolean +gs_flatpak_refresh (GsFlatpak *self, + guint cache_age, + GCancellable *cancellable, + GError **error) +{ + /* give all the repos a second chance */ + g_mutex_lock (&self->broken_remotes_mutex); + g_hash_table_remove_all (self->broken_remotes); + g_mutex_unlock (&self->broken_remotes_mutex); + + /* manually drop the cache */ + if (!flatpak_installation_drop_caches (self->installation, + cancellable, + error)) { + gs_flatpak_error_convert (error); + return FALSE; + } + + /* drop the installed refs cache */ + g_mutex_lock (&self->installed_refs_mutex); + g_clear_pointer (&self->installed_refs, g_ptr_array_unref); + g_mutex_unlock (&self->installed_refs_mutex); + + /* manually do this in case we created the first appstream file */ + g_rw_lock_reader_lock (&self->silo_lock); + if (self->silo != NULL) + xb_silo_invalidate (self->silo); + g_rw_lock_reader_unlock (&self->silo_lock); + + /* update AppStream metadata */ + if (!gs_flatpak_refresh_appstream (self, cache_age, cancellable, error)) + return FALSE; + + /* ensure valid */ + if (!gs_flatpak_rescan_appstream_store (self, cancellable, error)) + return FALSE; + + /* success */ + return TRUE; +} + +static gboolean +gs_plugin_refine_item_origin_hostname (GsFlatpak *self, GsApp *app, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(FlatpakRemote) xremote = NULL; + g_autofree gchar *url = NULL; + g_autoptr(GError) error_local = NULL; + + /* already set */ + if (gs_app_get_origin_hostname (app) != NULL) + return TRUE; + + /* no origin */ + if (gs_app_get_origin (app) == NULL) + return TRUE; + + /* get the remote */ + xremote = flatpak_installation_get_remote_by_name (self->installation, + gs_app_get_origin (app), + cancellable, + &error_local); + if (xremote == NULL) { + if (g_error_matches (error_local, + FLATPAK_ERROR, + FLATPAK_ERROR_REMOTE_NOT_FOUND)) { + /* if the user deletes the -origin remote for a locally + * installed flatpakref file then we should just show + * 'localhost' and not return an error */ + gs_app_set_origin_hostname (app, ""); + return TRUE; + } + g_propagate_error (error, g_steal_pointer (&error_local)); + gs_flatpak_error_convert (error); + return FALSE; + } + url = flatpak_remote_get_url (xremote); + if (url == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "no URL for remote %s", + flatpak_remote_get_name (xremote)); + return FALSE; + } + gs_app_set_origin_hostname (app, url); + return TRUE; +} + +static gboolean +gs_refine_item_metadata (GsFlatpak *self, GsApp *app, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(FlatpakRef) xref = NULL; + + /* already set */ + if (gs_flatpak_app_get_ref_name (app) != NULL) + return TRUE; + + /* not a valid type */ + if (gs_app_get_kind (app) == AS_APP_KIND_SOURCE) + return TRUE; + + /* AppStream sets the source to appname/arch/branch, if this isn't set + * we can't break out the fields */ + if (gs_app_get_source_default (app) == NULL) { + g_autofree gchar *tmp = gs_app_to_string (app); + g_warning ("no source set by appstream for %s: %s", + gs_plugin_get_name (self->plugin), tmp); + return TRUE; + } + + /* parse the ref */ + xref = flatpak_ref_parse (gs_app_get_source_default (app), error); + if (xref == NULL) { + gs_flatpak_error_convert (error); + g_prefix_error (error, "failed to parse '%s': ", + gs_app_get_source_default (app)); + return FALSE; + } + gs_flatpak_set_metadata (self, app, xref); + + /* success */ + return TRUE; +} + +static gboolean +gs_plugin_refine_item_origin (GsFlatpak *self, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + g_autofree gchar *ref_display = NULL; + g_autoptr(GPtrArray) xremotes = NULL; + + /* already set */ + if (gs_app_get_origin (app) != NULL) + return TRUE; + + /* not applicable */ + if (gs_app_get_state (app) == AS_APP_STATE_AVAILABLE_LOCAL) + return TRUE; + + /* ensure metadata exists */ + if (!gs_refine_item_metadata (self, app, cancellable, error)) + return FALSE; + + /* find list of remotes */ + ref_display = gs_flatpak_app_get_ref_display (app); + g_debug ("looking for a remote for %s", ref_display); + xremotes = flatpak_installation_list_remotes (self->installation, + cancellable, error); + if (xremotes == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + for (guint i = 0; i < xremotes->len; i++) { + const gchar *remote_name; + FlatpakRemote *xremote = g_ptr_array_index (xremotes, i); + g_autoptr(FlatpakRemoteRef) xref = NULL; + g_autoptr(GError) error_local = NULL; + + /* not enabled */ + if (flatpak_remote_get_disabled (xremote)) + continue; + + /* sync */ + remote_name = flatpak_remote_get_name (xremote); + g_debug ("looking at remote %s", remote_name); + xref = flatpak_installation_fetch_remote_ref_sync (self->installation, + remote_name, + gs_flatpak_app_get_ref_kind (app), + gs_flatpak_app_get_ref_name (app), + gs_flatpak_app_get_ref_arch (app), + gs_app_get_branch (app), + cancellable, + &error_local); + if (xref != NULL) { + g_debug ("found remote %s", remote_name); + gs_app_set_origin (app, remote_name); + gs_flatpak_app_set_commit (app, flatpak_ref_get_commit (FLATPAK_REF (xref))); + gs_plugin_refine_item_scope (self, app); + return TRUE; + } + g_debug ("%s failed to find remote %s: %s", + ref_display, remote_name, error_local->message); + } + + /* not found */ + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "%s not found in any remote", + ref_display); + return FALSE; +} + +static FlatpakRef * +gs_flatpak_create_fake_ref (GsApp *app, GError **error) +{ + FlatpakRef *xref; + g_autofree gchar *id = NULL; + id = g_strdup_printf ("%s/%s/%s/%s", + gs_flatpak_app_get_ref_kind_as_str (app), + gs_flatpak_app_get_ref_name (app), + gs_flatpak_app_get_ref_arch (app), + gs_app_get_branch (app)); + xref = flatpak_ref_parse (id, error); + if (xref == NULL) { + gs_flatpak_error_convert (error); + return NULL; + } + return xref; +} + +/* the _unlocked() version doesn't call gs_flatpak_rescan_appstream_store, + * in order to avoid taking the writer lock on self->silo_lock */ +static gboolean +gs_flatpak_refine_app_state_unlocked (GsFlatpak *self, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(FlatpakInstalledRef) ref = NULL; + g_autoptr(GPtrArray) installed_refs = NULL; + + /* already found */ + if (gs_app_get_state (app) != AS_APP_STATE_UNKNOWN) + return TRUE; + + /* need broken out metadata */ + if (!gs_refine_item_metadata (self, app, cancellable, error)) + return FALSE; + + /* find the app using the origin and the ID */ + g_mutex_lock (&self->installed_refs_mutex); + + if (self->installed_refs == NULL) { + self->installed_refs = flatpak_installation_list_installed_refs (self->installation, + cancellable, error); + + if (self->installed_refs == NULL) { + g_mutex_unlock (&self->installed_refs_mutex); + gs_flatpak_error_convert (error); + return FALSE; + } + } + + installed_refs = g_ptr_array_ref (self->installed_refs); + g_mutex_unlock (&self->installed_refs_mutex); + + for (guint i = 0; i < installed_refs->len; i++) { + FlatpakInstalledRef *ref_tmp = g_ptr_array_index (installed_refs, i); + const gchar *origin = flatpak_installed_ref_get_origin (ref_tmp); + const gchar *name = flatpak_ref_get_name (FLATPAK_REF (ref_tmp)); + const gchar *arch = flatpak_ref_get_arch (FLATPAK_REF (ref_tmp)); + const gchar *branch = flatpak_ref_get_branch (FLATPAK_REF (ref_tmp)); + if (g_strcmp0 (origin, gs_app_get_origin (app)) == 0 && + g_strcmp0 (name, gs_flatpak_app_get_ref_name (app)) == 0 && + g_strcmp0 (arch, gs_flatpak_app_get_ref_arch (app)) == 0 && + g_strcmp0 (branch, gs_app_get_branch (app)) == 0) { + ref = g_object_ref (ref_tmp); + break; + } + } + if (ref != NULL) { + g_debug ("marking %s as installed with flatpak", + gs_app_get_unique_id (app)); + gs_flatpak_set_metadata_installed (self, app, ref); + if (gs_app_get_state (app) == AS_APP_STATE_UNKNOWN) + gs_app_set_state (app, AS_APP_STATE_INSTALLED); + + /* flatpak only allows one installed app to be launchable */ + if (flatpak_installed_ref_get_is_current (ref)) { + gs_app_remove_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE); + } else { + g_debug ("%s is not current, and therefore not launchable", + gs_app_get_unique_id (app)); + gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE); + } + return TRUE; + } + + /* ensure origin set */ + if (!gs_plugin_refine_item_origin (self, app, cancellable, error)) + return FALSE; + + /* anything not installed just check the remote is still present */ + if (gs_app_get_state (app) == AS_APP_STATE_UNKNOWN && + gs_app_get_origin (app) != NULL) { + g_autoptr(FlatpakRemote) xremote = NULL; + xremote = flatpak_installation_get_remote_by_name (self->installation, + gs_app_get_origin (app), + cancellable, NULL); + if (xremote != NULL) { + if (flatpak_remote_get_disabled (xremote)) { + g_debug ("%s is available with flatpak " + "but %s is disabled", + gs_app_get_unique_id (app), + flatpak_remote_get_name (xremote)); + gs_app_set_state (app, AS_APP_STATE_UNAVAILABLE); + } else { + g_debug ("marking %s as available with flatpak", + gs_app_get_unique_id (app)); + gs_app_set_state (app, AS_APP_STATE_AVAILABLE); + } + } else { + gs_app_set_state (app, AS_APP_STATE_UNKNOWN); + g_debug ("failed to find %s remote %s for %s", + self->id, + gs_app_get_origin (app), + gs_app_get_unique_id (app)); + } + } + + /* success */ + return TRUE; +} + +gboolean +gs_flatpak_refine_app_state (GsFlatpak *self, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + /* ensure valid */ + if (!gs_flatpak_rescan_appstream_store (self, cancellable, error)) + return FALSE; + + return gs_flatpak_refine_app_state_unlocked (self, app, cancellable, error); +} + +static GsApp * +gs_flatpak_create_runtime (GsFlatpak *self, GsApp *parent, const gchar *runtime) +{ + g_autofree gchar *source = NULL; + g_auto(GStrv) split = NULL; + g_autoptr(GsApp) app_cache = NULL; + g_autoptr(GsApp) app = NULL; + + /* get the name/arch/branch */ + split = g_strsplit (runtime, "/", -1); + if (g_strv_length (split) != 3) + return NULL; + + /* create the complete GsApp from the single string */ + app = gs_app_new (split[0]); + gs_flatpak_claim_app (self, app); + source = g_strdup_printf ("runtime/%s", runtime); + gs_app_add_source (app, source); + gs_app_set_kind (app, AS_APP_KIND_RUNTIME); + gs_app_set_branch (app, split[2]); + + /* search in the cache */ + app_cache = gs_plugin_cache_lookup (self->plugin, gs_app_get_unique_id (app)); + if (app_cache != NULL) { + /* since the cached runtime can have been created somewhere else + * (we're using a global cache), we need to make sure that a + * source is set */ + if (gs_app_get_source_default (app_cache) == NULL) + gs_app_add_source (app_cache, source); + return g_steal_pointer (&app_cache); + } + + /* if the app is per-user we can also use the installed system runtime */ + if (gs_app_get_scope (parent) == AS_APP_SCOPE_USER) { + gs_app_set_scope (app, AS_APP_SCOPE_UNKNOWN); + app_cache = gs_plugin_cache_lookup (self->plugin, gs_app_get_unique_id (app)); + if (app_cache != NULL) + return g_steal_pointer (&app_cache); + } + + /* set superclassed app properties */ + gs_flatpak_app_set_ref_kind (app, FLATPAK_REF_KIND_RUNTIME); + gs_flatpak_app_set_ref_name (app, split[0]); + gs_flatpak_app_set_ref_arch (app, split[1]); + + /* save in the cache */ + gs_plugin_cache_add (self->plugin, NULL, app); + return g_steal_pointer (&app); +} + +static gboolean +gs_flatpak_set_app_metadata (GsFlatpak *self, + GsApp *app, + const gchar *data, + gsize length, + GError **error) +{ + gboolean secure = TRUE; + g_autofree gchar *name = NULL; + g_autofree gchar *runtime = NULL; + g_autoptr(GKeyFile) kf = NULL; + g_autoptr(GsApp) app_runtime = NULL; + g_auto(GStrv) shared = NULL; + g_auto(GStrv) sockets = NULL; + g_auto(GStrv) filesystems = NULL; + + kf = g_key_file_new (); + if (!g_key_file_load_from_data (kf, data, length, G_KEY_FILE_NONE, error)) { + gs_flatpak_error_convert (error); + return FALSE; + } + name = g_key_file_get_string (kf, "Application", "name", error); + if (name == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + gs_flatpak_app_set_ref_name (app, name); + runtime = g_key_file_get_string (kf, "Application", "runtime", error); + if (runtime == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + + shared = g_key_file_get_string_list (kf, "Context", "shared", NULL, NULL); + if (shared != NULL) { + /* SHM isn't secure enough */ + if (g_strv_contains ((const gchar * const *) shared, "ipc")) + secure = FALSE; + } + sockets = g_key_file_get_string_list (kf, "Context", "sockets", NULL, NULL); + if (sockets != NULL) { + /* X11 isn't secure enough */ + if (g_strv_contains ((const gchar * const *) sockets, "x11")) + secure = FALSE; + } + filesystems = g_key_file_get_string_list (kf, "Context", "filesystems", NULL, NULL); + if (filesystems != NULL) { + /* secure apps should be using portals */ + if (g_strv_contains ((const gchar * const *) filesystems, "home")) + secure = FALSE; + } + + gs_app_set_permissions (app, perms_from_metadata (kf)); + /* this is actually quite hard to achieve */ + if (secure) + gs_app_add_kudo (app, GS_APP_KUDO_SANDBOXED_SECURE); + + /* create runtime */ + app_runtime = gs_flatpak_create_runtime (self, app, runtime); + if (app_runtime != NULL) { + gs_plugin_refine_item_scope (self, app_runtime); + gs_app_set_runtime (app, app_runtime); + } + + /* we always get this, but it's a low bar... */ + gs_app_add_kudo (app, GS_APP_KUDO_SANDBOXED); + + return TRUE; +} + +static GBytes * +gs_flatpak_fetch_remote_metadata (GsFlatpak *self, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GBytes) data = NULL; + g_autoptr(FlatpakRef) xref = NULL; + + /* no origin */ + if (gs_app_get_origin (app) == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "no origin set when getting metadata for %s", + gs_app_get_unique_id (app)); + return NULL; + } + + /* fetch from the server */ + xref = gs_flatpak_create_fake_ref (app, error); + if (xref == NULL) + return NULL; + data = flatpak_installation_fetch_remote_metadata_sync (self->installation, + gs_app_get_origin (app), + xref, + cancellable, + error); + if (data == NULL) { + gs_flatpak_error_convert (error); + return NULL; + } + return g_steal_pointer (&data); +} + +static gboolean +gs_plugin_refine_item_metadata (GsFlatpak *self, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + const gchar *str; + gsize len = 0; + g_autofree gchar *contents = NULL; + g_autofree gchar *installation_path_str = NULL; + g_autofree gchar *install_path = NULL; + g_autoptr(GBytes) data = NULL; + g_autoptr(GFile) installation_path = NULL; + + /* not applicable */ + if (gs_app_get_kind (app) == AS_APP_KIND_SOURCE) + return TRUE; + if (gs_flatpak_app_get_ref_kind (app) != FLATPAK_REF_KIND_APP) + return TRUE; + + /* already done */ + if (gs_app_has_kudo (app, GS_APP_KUDO_SANDBOXED)) + return TRUE; + + /* this is quicker than doing network IO */ + installation_path = flatpak_installation_get_path (self->installation); + installation_path_str = g_file_get_path (installation_path); + install_path = g_build_filename (installation_path_str, + gs_flatpak_app_get_ref_kind_as_str (app), + gs_flatpak_app_get_ref_name (app), + gs_flatpak_app_get_ref_arch (app), + gs_app_get_branch (app), + "active", + "metadata", + NULL); + if (g_file_test (install_path, G_FILE_TEST_EXISTS)) { + if (!g_file_get_contents (install_path, &contents, &len, error)) + return FALSE; + str = contents; + } else { + data = gs_flatpak_fetch_remote_metadata (self, app, cancellable, + error); + if (data == NULL) + return FALSE; + str = g_bytes_get_data (data, &len); + } + + /* parse key file */ + if (!gs_flatpak_set_app_metadata (self, app, str, len, error)) + return FALSE; + return TRUE; +} + +static FlatpakInstalledRef * +gs_flatpak_get_installed_ref (GsFlatpak *self, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + FlatpakInstalledRef *ref; + ref = flatpak_installation_get_installed_ref (self->installation, + gs_flatpak_app_get_ref_kind (app), + gs_flatpak_app_get_ref_name (app), + gs_flatpak_app_get_ref_arch (app), + gs_app_get_branch (app), + cancellable, + error); + if (ref == NULL) + gs_flatpak_error_convert (error); + return ref; +} + +static gboolean +gs_plugin_refine_item_size (GsFlatpak *self, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + gboolean ret; + guint64 download_size = GS_APP_SIZE_UNKNOWABLE; + guint64 installed_size = GS_APP_SIZE_UNKNOWABLE; + + /* not applicable */ + if (gs_app_get_state (app) == AS_APP_STATE_AVAILABLE_LOCAL) + return TRUE; + if (gs_app_get_kind (app) == AS_APP_KIND_SOURCE) + return TRUE; + + /* already set */ + if (gs_app_is_installed (app)) { + /* only care about the installed size if the app is installed */ + if (gs_app_get_size_installed (app) > 0) + return TRUE; + } else { + if (gs_app_get_size_installed (app) > 0 && + gs_app_get_size_download (app) > 0) + return TRUE; + } + + /* need runtime */ + if (!gs_plugin_refine_item_metadata (self, app, cancellable, error)) + return FALSE; + + /* calculate the platform size too if the app is not installed */ + if (gs_app_get_state (app) == AS_APP_STATE_AVAILABLE && + gs_flatpak_app_get_ref_kind (app) == FLATPAK_REF_KIND_APP) { + GsApp *app_runtime; + + /* is the app_runtime already installed? */ + app_runtime = gs_app_get_runtime (app); + if (!gs_flatpak_refine_app_state_unlocked (self, + app_runtime, + cancellable, + error)) + return FALSE; + if (gs_app_get_state (app_runtime) == AS_APP_STATE_INSTALLED) { + g_debug ("runtime %s is already installed, so not adding size", + gs_app_get_unique_id (app_runtime)); + } else { + if (!gs_plugin_refine_item_size (self, + app_runtime, + cancellable, + error)) + return FALSE; + } + } + + /* just get the size of the app */ + if (!gs_plugin_refine_item_origin (self, app, + cancellable, error)) + return FALSE; + + /* if the app is installed we use the ref to fetch the installed size + * and ignore the download size as this is faster */ + if (gs_app_is_installed (app)) { + g_autoptr(FlatpakInstalledRef) xref = NULL; + xref = gs_flatpak_get_installed_ref (self, app, cancellable, error); + if (xref == NULL) + return FALSE; + installed_size = flatpak_installed_ref_get_installed_size (xref); + if (installed_size == 0) + installed_size = GS_APP_SIZE_UNKNOWABLE; + } else { + g_autoptr(FlatpakRef) xref = NULL; + g_autoptr(GError) error_local = NULL; + + /* no origin */ + if (gs_app_get_origin (app) == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "no origin set for %s", + gs_app_get_unique_id (app)); + return FALSE; + } + xref = gs_flatpak_create_fake_ref (app, error); + if (xref == NULL) + return FALSE; + ret = flatpak_installation_fetch_remote_size_sync (self->installation, + gs_app_get_origin (app), + xref, + &download_size, + &installed_size, + cancellable, + &error_local); + + if (!ret) { + g_warning ("libflatpak failed to return application " + "size: %s", error_local->message); + g_clear_error (&error_local); + } + } + + gs_app_set_size_installed (app, installed_size); + gs_app_set_size_download (app, download_size); + + return TRUE; +} + +static void +gs_flatpak_refine_appstream_release (XbNode *component, GsApp *app) +{ + const gchar *version; + + /* get first release */ + version = xb_node_query_attr (component, "releases/release", "version", NULL); + if (version == NULL) + return; + switch (gs_app_get_state (app)) { + case AS_APP_STATE_INSTALLED: + case AS_APP_STATE_AVAILABLE: + case AS_APP_STATE_AVAILABLE_LOCAL: + gs_app_set_version (app, version); + break; + case AS_APP_STATE_UPDATABLE: + case AS_APP_STATE_UPDATABLE_LIVE: + gs_app_set_update_version (app, version); + break; + default: + g_debug ("%s is not installed, so ignoring version of %s", + gs_app_get_unique_id (app), version); + break; + } +} + +/* This function is like gs_flatpak_refine_appstream(), but takes gzip + * compressed appstream data as a GBytes and assumes they are already uniquely + * tied to the app (and therefore app ID alone can be used to find the right + * component). + */ +static gboolean +gs_flatpak_refine_appstream_from_bytes (GsFlatpak *self, + GsApp *app, + const char *origin, /* (nullable) */ + FlatpakInstalledRef *installed_ref, /* (nullable) */ + GBytes *appstream_gz, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + const gchar *const *locales = g_get_language_names (); + g_autofree gchar *xpath = NULL; + g_autoptr(XbBuilder) builder = xb_builder_new (); + g_autoptr(XbBuilderSource) source = xb_builder_source_new (); + g_autoptr(XbNode) component_node = NULL; + g_autoptr(XbNode) n = NULL; + g_autoptr(XbSilo) silo = NULL; + g_autoptr(XbBuilderFixup) bundle_fixup = NULL; + g_autoptr(GBytes) appstream = NULL; + g_autoptr(GInputStream) stream_data = NULL; + g_autoptr(GInputStream) stream_gz = NULL; + g_autoptr(GZlibDecompressor) decompressor = NULL; + + /* add current locales */ + for (guint i = 0; locales[i] != NULL; i++) + xb_builder_add_locale (builder, locales[i]); + + /* decompress data */ + decompressor = g_zlib_decompressor_new (G_ZLIB_COMPRESSOR_FORMAT_GZIP); + stream_gz = g_memory_input_stream_new_from_bytes (appstream_gz); + if (stream_gz == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "unable to decompress appstream data"); + return FALSE; + } + stream_data = g_converter_input_stream_new (stream_gz, + G_CONVERTER (decompressor)); + + appstream = g_input_stream_read_bytes (stream_data, + 0x100000, /* 1Mb */ + cancellable, + error); + if (appstream == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + + /* build silo */ + if (!xb_builder_source_load_bytes (source, appstream, + XB_BUILDER_SOURCE_FLAG_NONE, + error)) + return FALSE; + + /* Appdata from flatpak_installed_ref_load_appdata() may be missing the + * <bundle> tag but for this function we know it's the right component. + */ + bundle_fixup = xb_builder_fixup_new ("AddBundle", + gs_flatpak_add_bundle_tag_cb, + gs_flatpak_app_get_ref_display (app), g_free); + xb_builder_fixup_set_max_depth (bundle_fixup, 2); + xb_builder_source_add_fixup (source, bundle_fixup); + + fixup_flatpak_appstream_xml (source, origin); + + /* add metadata */ + if (installed_ref != NULL) { + g_autoptr(XbBuilderNode) info = NULL; + g_autofree char *icon_prefix = NULL; + + info = xb_builder_node_insert (NULL, "info", NULL); + xb_builder_node_insert_text (info, "scope", as_app_scope_to_string (self->scope), NULL); + icon_prefix = g_build_filename (flatpak_installed_ref_get_deploy_dir (installed_ref), + "files", "share", "app-info", "icons", "flatpak", NULL); + xb_builder_node_insert_text (info, "icon-prefix", icon_prefix, NULL); + xb_builder_source_set_info (source, info); + } + + xb_builder_import_source (builder, source); + silo = xb_builder_compile (builder, + XB_BUILDER_COMPILE_FLAG_SINGLE_LANG, + cancellable, + error); + if (silo == NULL) + return FALSE; + if (g_getenv ("GS_XMLB_VERBOSE") != NULL) { + g_autofree gchar *xml = NULL; + xml = xb_silo_export (silo, + XB_NODE_EXPORT_FLAG_FORMAT_INDENT | + XB_NODE_EXPORT_FLAG_FORMAT_MULTILINE, + NULL); + g_debug ("showing AppStream data: %s", xml); + } + + /* check for sanity */ + n = xb_silo_query_first (silo, "components/component", NULL); + if (n == NULL) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "no apps found in AppStream data"); + return FALSE; + } + + /* find app */ + xpath = g_strdup_printf ("components/component/id[text()='%s']/..", + gs_flatpak_app_get_ref_name (app)); + component_node = xb_silo_query_first (silo, xpath, NULL); + if (component_node == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "application %s not found", + gs_flatpak_app_get_ref_name (app)); + return FALSE; + } + + /* copy details from AppStream to app */ + if (!gs_appstream_refine_app (self->plugin, app, silo, component_node, flags, error)) + return FALSE; + + /* use the default release as the version number */ + gs_flatpak_refine_appstream_release (component_node, app); + + /* save the silo so it can be used for searches */ + { + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->app_silos_mutex); + g_hash_table_replace (self->app_silos, + gs_flatpak_app_get_ref_display (app), + g_steal_pointer (&silo)); + } + + return TRUE; +} + +static gboolean +gs_flatpak_refine_appstream (GsFlatpak *self, + GsApp *app, + XbSilo *silo, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + const gchar *origin = gs_app_get_origin (app); + const gchar *source = gs_app_get_source_default (app); + g_autofree gchar *source_safe = NULL; + g_autofree gchar *xpath = NULL; + g_autoptr(GError) error_local = NULL; + g_autoptr(XbNode) component = NULL; + + if (origin == NULL || source == NULL || gs_flatpak_app_get_ref_name (app) == NULL) + return TRUE; + + /* find using source and origin */ + source_safe = xb_string_escape (source); + xpath = g_strdup_printf ("components[@origin='%s']/component/bundle[@type='flatpak'][text()='%s']/..", + origin, source_safe); + component = xb_silo_query_first (silo, xpath, &error_local); + if (component == NULL) { + g_autoptr(FlatpakInstalledRef) installed_ref = NULL; + g_autoptr(GBytes) appstream_gz = NULL; + + g_debug ("no match for %s: %s", xpath, error_local->message); + /* For apps installed from .flatpak bundles there may not be any remote + * appstream data in @silo for it, so use the appstream data from + * within the app. + */ + installed_ref = flatpak_installation_get_installed_ref (self->installation, + gs_flatpak_app_get_ref_kind (app), + gs_flatpak_app_get_ref_name (app), + gs_flatpak_app_get_ref_arch (app), + gs_app_get_branch (app), + NULL, NULL); + if (installed_ref == NULL) + return TRUE; /* the app may not be installed */ + +#if FLATPAK_CHECK_VERSION(1,1,2) + appstream_gz = flatpak_installed_ref_load_appdata (installed_ref, NULL, NULL); +#endif + if (appstream_gz == NULL) + return TRUE; + + g_debug ("using installed appdata for %s", gs_flatpak_app_get_ref_name (app)); + return gs_flatpak_refine_appstream_from_bytes (self, + app, + flatpak_installed_ref_get_origin (installed_ref), + installed_ref, + appstream_gz, + flags, + cancellable, error); + } + + if (!gs_appstream_refine_app (self->plugin, app, silo, component, flags, error)) + return FALSE; + + /* use the default release as the version number */ + gs_flatpak_refine_appstream_release (component, app); + return TRUE; +} + +/* the _unlocked() version doesn't call gs_flatpak_rescan_appstream_store, + * in order to avoid taking the writer lock on self->silo_lock */ +static gboolean +gs_flatpak_refine_app_unlocked (GsFlatpak *self, + GsApp *app, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + AsAppState old_state = gs_app_get_state (app); + g_autoptr(GRWLockReaderLocker) locker = NULL; + + /* not us */ + if (gs_app_get_bundle_kind (app) != AS_BUNDLE_KIND_FLATPAK) + return TRUE; + + locker = g_rw_lock_reader_locker_new (&self->silo_lock); + + /* always do AppStream properties */ + if (!gs_flatpak_refine_appstream (self, app, self->silo, flags, cancellable, error)) + return FALSE; + + /* AppStream sets the source to appname/arch/branch */ + if (!gs_refine_item_metadata (self, app, cancellable, error)) { + g_prefix_error (error, "failed to get metadata: "); + return FALSE; + } + + /* check the installed state */ + if (!gs_flatpak_refine_app_state_unlocked (self, app, cancellable, error)) { + g_prefix_error (error, "failed to get state: "); + return FALSE; + } + + /* scope is fast, do unconditionally */ + if (gs_app_get_state (app) != AS_APP_STATE_AVAILABLE_LOCAL) + gs_plugin_refine_item_scope (self, app); + + /* if the state was changed, perhaps set the version from the release */ + if (old_state != gs_app_get_state (app)) { + if (!gs_flatpak_refine_appstream (self, app, self->silo, flags, cancellable, error)) + return FALSE; + } + + /* version fallback */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION) { + if (gs_app_get_version (app) == NULL) { + const gchar *branch; + branch = gs_app_get_branch (app); + gs_app_set_version (app, branch); + } + } + + /* size */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE) { + g_autoptr(GError) error_local = NULL; + if (!gs_plugin_refine_item_size (self, app, + cancellable, &error_local)) { + if (!gs_plugin_get_network_available (self->plugin) && + g_error_matches (error_local, GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NO_NETWORK)) { + g_debug ("failed to get size while " + "refining app %s: %s", + gs_app_get_unique_id (app), + error_local->message); + } else { + g_prefix_error (&error_local, "failed to get size: "); + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + } + } + + /* origin-hostname */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME) { + if (!gs_plugin_refine_item_origin_hostname (self, app, + cancellable, + error)) { + g_prefix_error (error, "failed to get origin-hostname: "); + return FALSE; + } + } + + /* permissions */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME || + flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS) { + g_autoptr(GError) error_local = NULL; + if (!gs_plugin_refine_item_metadata (self, app, + cancellable, &error_local)) { + if (!gs_plugin_get_network_available (self->plugin) && + g_error_matches (error_local, GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NO_NETWORK)) { + g_debug ("failed to get permissions while " + "refining app %s: %s", + gs_app_get_unique_id (app), + error_local->message); + } else { + g_prefix_error (&error_local, "failed to get permissions: "); + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + } + } + + return TRUE; +} + +gboolean +gs_flatpak_refine_app (GsFlatpak *self, + GsApp *app, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + /* ensure valid */ + if (!gs_flatpak_rescan_appstream_store (self, cancellable, error)) + return FALSE; + + return gs_flatpak_refine_app_unlocked (self, app, flags, cancellable, error); +} + +gboolean +gs_flatpak_refine_wildcard (GsFlatpak *self, GsApp *app, + GsAppList *list, GsPluginRefineFlags refine_flags, + GCancellable *cancellable, GError **error) +{ + const gchar *id; + g_autofree gchar *xpath = NULL; + g_autoptr(GError) error_local = NULL; + g_autoptr(GPtrArray) components = NULL; + g_autoptr(GRWLockReaderLocker) locker = NULL; + + /* not enough info to find */ + id = gs_app_get_id (app); + if (id == NULL) + return TRUE; + + /* ensure valid */ + if (!gs_flatpak_rescan_appstream_store (self, cancellable, error)) + return FALSE; + + locker = g_rw_lock_reader_locker_new (&self->silo_lock); + + /* find all apps when matching any prefixes */ + xpath = g_strdup_printf ("components/component/id[text()='%s']/..", id); + components = xb_silo_query (self->silo, xpath, 0, &error_local); + if (components == NULL) { + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + return TRUE; + 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 = gs_appstream_create_app (self->plugin, self->silo, component, error); + if (new == NULL) + return FALSE; + gs_flatpak_claim_app (self, new); + if (!gs_flatpak_refine_app_unlocked (self, new, refine_flags, cancellable, error)) + return FALSE; + gs_app_subsume_metadata (new, app); + gs_app_list_add (list, new); + } + + /* success */ + return TRUE; +} + +gboolean +gs_flatpak_launch (GsFlatpak *self, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + /* launch the app */ + if (!flatpak_installation_launch (self->installation, + gs_flatpak_app_get_ref_name (app), + gs_flatpak_app_get_ref_arch (app), + gs_app_get_branch (app), + NULL, + cancellable, + error)) { + gs_flatpak_error_convert (error); + return FALSE; + } + return TRUE; +} + +gboolean +gs_flatpak_app_remove_source (GsFlatpak *self, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(FlatpakRemote) xremote = NULL; + + /* find the remote */ + xremote = flatpak_installation_get_remote_by_name (self->installation, + gs_app_get_id (app), + cancellable, error); + if (xremote == NULL) { + gs_flatpak_error_convert (error); + g_prefix_error (error, + "flatpak source %s not found: ", + gs_app_get_id (app)); + return FALSE; + } + + /* remove */ + gs_app_set_state (app, AS_APP_STATE_REMOVING); + if (!flatpak_installation_remove_remote (self->installation, + gs_app_get_id (app), + cancellable, + error)) { + gs_flatpak_error_convert (error); + gs_app_set_state_recover (app); + return FALSE; + } + + /* invalidate cache */ + g_rw_lock_reader_lock (&self->silo_lock); + if (self->silo != NULL) + xb_silo_invalidate (self->silo); + g_rw_lock_reader_unlock (&self->silo_lock); + + gs_app_set_state (app, AS_APP_STATE_AVAILABLE); + return TRUE; +} + +GsApp * +gs_flatpak_file_to_app_bundle (GsFlatpak *self, + GFile *file, + GCancellable *cancellable, + GError **error) +{ + gint size; + g_autoptr(GBytes) appstream_gz = NULL; + g_autoptr(GBytes) icon_data = NULL; + g_autoptr(GBytes) metadata = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(FlatpakBundleRef) xref_bundle = NULL; + g_autoptr(FlatpakInstalledRef) installed_ref = NULL; + const char *origin = NULL; + + /* load bundle */ + xref_bundle = flatpak_bundle_ref_new (file, error); + if (xref_bundle == NULL) { + gs_flatpak_error_convert (error); + g_prefix_error (error, "error loading bundle: "); + return NULL; + } + + /* get the origin if it's already installed */ + installed_ref = flatpak_installation_get_installed_ref (self->installation, + flatpak_ref_get_kind (FLATPAK_REF (xref_bundle)), + flatpak_ref_get_name (FLATPAK_REF (xref_bundle)), + flatpak_ref_get_arch (FLATPAK_REF (xref_bundle)), + flatpak_ref_get_branch (FLATPAK_REF (xref_bundle)), + NULL, NULL); + if (installed_ref != NULL) + origin = flatpak_installed_ref_get_origin (installed_ref); + + /* load metadata */ + app = gs_flatpak_create_app (self, origin, FLATPAK_REF (xref_bundle)); + if (gs_app_get_state (app) == AS_APP_STATE_INSTALLED) { + if (gs_flatpak_app_get_ref_name (app) == NULL) + gs_flatpak_set_metadata (self, app, FLATPAK_REF (xref_bundle)); + return g_steal_pointer (&app); + } + gs_flatpak_app_set_file_kind (app, GS_FLATPAK_APP_FILE_KIND_BUNDLE); + gs_app_set_state (app, AS_APP_STATE_AVAILABLE_LOCAL); + gs_app_set_size_installed (app, flatpak_bundle_ref_get_installed_size (xref_bundle)); + gs_flatpak_set_metadata (self, app, FLATPAK_REF (xref_bundle)); + metadata = flatpak_bundle_ref_get_metadata (xref_bundle); + if (!gs_flatpak_set_app_metadata (self, app, + g_bytes_get_data (metadata, NULL), + g_bytes_get_size (metadata), + error)) + return NULL; + + /* load AppStream */ + appstream_gz = flatpak_bundle_ref_get_appstream (xref_bundle); + if (appstream_gz != NULL) { + if (!gs_flatpak_refine_appstream_from_bytes (self, app, origin, installed_ref, + appstream_gz, + GS_PLUGIN_REFINE_FLAGS_DEFAULT, + cancellable, error)) + return NULL; + } else { + g_warning ("no appstream metadata in file"); + gs_app_set_name (app, GS_APP_QUALITY_LOWEST, + gs_flatpak_app_get_ref_name (app)); + gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, + "A flatpak application"); + gs_app_set_description (app, GS_APP_QUALITY_LOWEST, ""); + } + + /* load icon */ + size = 64 * (gint) gs_plugin_get_scale (self->plugin); + icon_data = flatpak_bundle_ref_get_icon (xref_bundle, size); + if (icon_data == NULL) + icon_data = flatpak_bundle_ref_get_icon (xref_bundle, 64); + if (icon_data != NULL) { + g_autoptr(GInputStream) stream_icon = NULL; + g_autoptr(GdkPixbuf) pixbuf = NULL; + stream_icon = g_memory_input_stream_new_from_bytes (icon_data); + pixbuf = gdk_pixbuf_new_from_stream (stream_icon, cancellable, error); + if (pixbuf == NULL) { + gs_utils_error_convert_gdk_pixbuf (error); + return NULL; + } + gs_app_set_pixbuf (app, pixbuf); + } else { + g_autoptr(AsIcon) icon = NULL; + icon = as_icon_new (); + as_icon_set_kind (icon, AS_ICON_KIND_STOCK); + as_icon_set_name (icon, "application-x-executable"); + gs_app_add_icon (app, icon); + } + + /* not quite true: this just means we can update this specific app */ + if (flatpak_bundle_ref_get_origin (xref_bundle)) + gs_app_add_quirk (app, GS_APP_QUIRK_HAS_SOURCE); + + /* success */ + return g_steal_pointer (&app); +} + +GsApp * +gs_flatpak_file_to_app_ref (GsFlatpak *self, + GFile *file, + GCancellable *cancellable, + GError **error) +{ + GsApp *runtime; + const gchar *const *locales = g_get_language_names (); + const gchar *remote_name; + gsize len = 0; + g_autofree gchar *contents = NULL; + g_autoptr(FlatpakRemoteRef) xref = NULL; + g_autoptr(FlatpakRemote) xremote = NULL; + g_autoptr(GBytes) ref_file_data = NULL; + g_autoptr(GError) error_local = NULL; + g_autoptr(GKeyFile) kf = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(XbBuilder) builder = xb_builder_new (); + g_autoptr(XbSilo) silo = NULL; + g_autofree gchar *origin_url = NULL; + g_autofree gchar *ref_comment = NULL; + g_autofree gchar *ref_description = NULL; + g_autofree gchar *ref_homepage = NULL; + g_autofree gchar *ref_icon = NULL; + g_autofree gchar *ref_title = NULL; + g_autofree gchar *ref_name = NULL; + + /* add current locales */ + for (guint i = 0; locales[i] != NULL; i++) + xb_builder_add_locale (builder, locales[i]); + + /* get file data */ + if (!g_file_load_contents (file, + cancellable, + &contents, + &len, + NULL, + error)) { + gs_utils_error_convert_gio (error); + return NULL; + } + + /* load the file */ + kf = g_key_file_new (); + if (!g_key_file_load_from_data (kf, contents, len, G_KEY_FILE_NONE, error)) { + gs_utils_error_convert_gio (error); + return NULL; + } + + /* check version */ + if (g_key_file_has_key (kf, "Flatpak Ref", "Version", NULL)) { + guint64 ver = g_key_file_get_uint64 (kf, "Flatpak Ref", "Version", NULL); + if (ver != 1) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "unsupported version %" G_GUINT64_FORMAT, ver); + return NULL; + } + } + + /* get name */ + ref_name = g_key_file_get_string (kf, "Flatpak Ref", "Name", error); + if (ref_name == NULL) { + gs_utils_error_convert_gio (error); + return NULL; + } + + /* install the remote, but not the app */ + ref_file_data = g_bytes_new (contents, len); + xref = flatpak_installation_install_ref_file (self->installation, + ref_file_data, + cancellable, + error); + if (xref == NULL) { + gs_flatpak_error_convert (error); + return NULL; + } + + /* load metadata */ + app = gs_flatpak_create_app (self, NULL /* origin */, FLATPAK_REF (xref)); + if (gs_app_get_state (app) == AS_APP_STATE_INSTALLED) { + if (gs_flatpak_app_get_ref_name (app) == NULL) + gs_flatpak_set_metadata (self, app, FLATPAK_REF (xref)); + return g_steal_pointer (&app); + } + gs_app_add_quirk (app, GS_APP_QUIRK_HAS_SOURCE); + gs_flatpak_app_set_file_kind (app, GS_FLATPAK_APP_FILE_KIND_REF); + gs_app_set_state (app, AS_APP_STATE_AVAILABLE_LOCAL); + gs_flatpak_set_metadata (self, app, FLATPAK_REF (xref)); + + /* use the data from the flatpakref file as a fallback */ + ref_title = g_key_file_get_string (kf, "Flatpak Ref", "Title", NULL); + if (ref_title != NULL) + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, ref_title); + ref_comment = g_key_file_get_string (kf, "Flatpak Ref", "Comment", NULL); + if (ref_comment != NULL) + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, ref_comment); + ref_description = g_key_file_get_string (kf, "Flatpak Ref", "Description", NULL); + if (ref_description != NULL) + gs_app_set_description (app, GS_APP_QUALITY_NORMAL, ref_description); + ref_homepage = g_key_file_get_string (kf, "Flatpak Ref", "Homepage", NULL); + if (ref_homepage != NULL) + gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, ref_homepage); + ref_icon = g_key_file_get_string (kf, "Flatpak Ref", "Icon", NULL); + if (ref_icon != NULL) { + g_autoptr(AsIcon) ic = as_icon_new (); + as_icon_set_kind (ic, AS_ICON_KIND_REMOTE); + as_icon_set_url (ic, ref_icon); + gs_app_add_icon (app, ic); + } + + /* set the origin data */ + remote_name = flatpak_remote_ref_get_remote_name (xref); + g_debug ("auto-created remote name: %s", remote_name); + xremote = flatpak_installation_get_remote_by_name (self->installation, + remote_name, + cancellable, + error); + if (xremote == NULL) { + gs_flatpak_error_convert (error); + return NULL; + } + origin_url = flatpak_remote_get_url (xremote); + if (origin_url == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "no URL for remote %s", + flatpak_remote_get_name (xremote)); + return NULL; + } + gs_app_set_origin (app, remote_name); + gs_app_set_origin_hostname (app, origin_url); + + /* get the new appstream data (nonfatal for failure) */ + if (!gs_flatpak_refresh_appstream_remote (self, remote_name, + cancellable, &error_local)) { + g_autoptr(GsPluginEvent) event = gs_plugin_event_new (); + gs_flatpak_error_convert (&error_local); + gs_plugin_event_set_app (event, app); + gs_plugin_event_set_error (event, error_local); + gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING); + gs_plugin_report_event (self->plugin, event); + g_clear_error (&error_local); + } + + /* get this now, as it's not going to be available at install time */ + if (!gs_plugin_refine_item_metadata (self, app, cancellable, error)) + return NULL; + + /* the new runtime is available from the RuntimeRepo */ + runtime = gs_app_get_runtime (app); + if (runtime != NULL && gs_app_get_state (runtime) == AS_APP_STATE_UNKNOWN) { + g_autofree gchar *uri = NULL; + uri = g_key_file_get_string (kf, "Flatpak Ref", "RuntimeRepo", NULL); + gs_flatpak_app_set_runtime_url (runtime, uri); + } + + /* parse it */ + if (!gs_flatpak_add_apps_from_xremote (self, builder, xremote, cancellable, error)) + return NULL; + + /* build silo */ + silo = xb_builder_compile (builder, + XB_BUILDER_COMPILE_FLAG_SINGLE_LANG, + cancellable, + error); + if (silo == NULL) + return NULL; + if (g_getenv ("GS_XMLB_VERBOSE") != NULL) { + g_autofree gchar *xml = NULL; + xml = xb_silo_export (silo, + XB_NODE_EXPORT_FLAG_FORMAT_INDENT | + XB_NODE_EXPORT_FLAG_FORMAT_MULTILINE, + NULL); + g_debug ("showing AppStream data: %s", xml); + } + + /* get extra AppStream data if available */ + if (!gs_flatpak_refine_appstream (self, app, silo, + G_MAXUINT64, + cancellable, + error)) + return NULL; + + /* success */ + return g_steal_pointer (&app); +} + +gboolean +gs_flatpak_search (GsFlatpak *self, + const gchar * const *values, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GsAppList) list_tmp = gs_app_list_new (); + g_autoptr(GRWLockReaderLocker) locker = NULL; + g_autoptr(GMutexLocker) app_silo_locker = NULL; + g_autoptr(GPtrArray) silos_to_remove = g_ptr_array_new (); + GHashTableIter iter; + gpointer key, value; + + if (!gs_flatpak_rescan_appstream_store (self, cancellable, error)) + return FALSE; + + locker = g_rw_lock_reader_locker_new (&self->silo_lock); + if (!gs_appstream_search (self->plugin, self->silo, values, list_tmp, + cancellable, error)) + return FALSE; + + gs_flatpak_claim_app_list (self, list_tmp); + gs_app_list_add_list (list, list_tmp); + + /* Also search silos from installed apps which were missing from self->silo */ + app_silo_locker = g_mutex_locker_new (&self->app_silos_mutex); + g_hash_table_iter_init (&iter, self->app_silos); + while (g_hash_table_iter_next (&iter, &key, &value)) { + g_autoptr(XbSilo) app_silo = g_object_ref (value); + g_autoptr(GsAppList) app_list_tmp = gs_app_list_new (); + const char *app_ref = (char *)key; + g_autoptr(FlatpakInstalledRef) installed_ref = NULL; + g_auto(GStrv) split = NULL; + FlatpakRefKind kind; + + /* Ignore any silos of apps that have since been removed. + * FIXME: can we use self->installed_refs here? */ + split = g_strsplit (app_ref, "/", -1); + g_assert (g_strv_length (split) == 4); + if (g_strcmp0 (split[0], "app") == 0) + kind = FLATPAK_REF_KIND_APP; + else + kind = FLATPAK_REF_KIND_RUNTIME; + installed_ref = flatpak_installation_get_installed_ref (self->installation, + kind, + split[1], + split[2], + split[3], + NULL, NULL); + if (installed_ref == NULL) { + g_ptr_array_add (silos_to_remove, app_ref); + continue; + } + + if (!gs_appstream_search (self->plugin, app_silo, values, app_list_tmp, + cancellable, error)) + return FALSE; + + gs_flatpak_claim_app_list (self, app_list_tmp); + gs_app_list_add_list (list, app_list_tmp); + } + + for (guint i = 0; i < silos_to_remove->len; i++) { + const char *silo = g_ptr_array_index (silos_to_remove, i); + g_hash_table_remove (self->app_silos, silo); + } + + return TRUE; +} + +gboolean +gs_flatpak_add_category_apps (GsFlatpak *self, + GsCategory *category, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GRWLockReaderLocker) locker = NULL; + locker = g_rw_lock_reader_locker_new (&self->silo_lock); + return gs_appstream_add_category_apps (self->plugin, self->silo, + category, list, + cancellable, error); +} + +gboolean +gs_flatpak_add_categories (GsFlatpak *self, + GPtrArray *list, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GRWLockReaderLocker) locker = NULL; + + if (!gs_flatpak_rescan_appstream_store (self, cancellable, error)) + return FALSE; + + locker = g_rw_lock_reader_locker_new (&self->silo_lock); + return gs_appstream_add_categories (self->plugin, self->silo, + list, cancellable, error); +} + +gboolean +gs_flatpak_add_popular (GsFlatpak *self, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GsAppList) list_tmp = gs_app_list_new (); + g_autoptr(GRWLockReaderLocker) locker = NULL; + + if (!gs_flatpak_rescan_appstream_store (self, cancellable, error)) + return FALSE; + + locker = g_rw_lock_reader_locker_new (&self->silo_lock); + if (!gs_appstream_add_popular (self->plugin, self->silo, list_tmp, + cancellable, error)) + return FALSE; + + gs_app_list_add_list (list, list_tmp); + + return TRUE; +} + +gboolean +gs_flatpak_add_featured (GsFlatpak *self, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GsAppList) list_tmp = gs_app_list_new (); + g_autoptr(GRWLockReaderLocker) locker = NULL; + + if (!gs_flatpak_rescan_appstream_store (self, cancellable, error)) + return FALSE; + + locker = g_rw_lock_reader_locker_new (&self->silo_lock); + if (!gs_appstream_add_featured (self->plugin, self->silo, list_tmp, + cancellable, error)) + return FALSE; + + gs_app_list_add_list (list, list_tmp); + + return TRUE; +} + +gboolean +gs_flatpak_add_alternates (GsFlatpak *self, + GsApp *app, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GsAppList) list_tmp = gs_app_list_new (); + g_autoptr(GRWLockReaderLocker) locker = NULL; + + if (!gs_flatpak_rescan_appstream_store (self, cancellable, error)) + return FALSE; + + locker = g_rw_lock_reader_locker_new (&self->silo_lock); + if (!gs_appstream_add_alternates (self->plugin, self->silo, app, list_tmp, + cancellable, error)) + return FALSE; + + gs_app_list_add_list (list, list_tmp); + + return TRUE; +} + +gboolean +gs_flatpak_add_recent (GsFlatpak *self, + GsAppList *list, + guint64 age, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GsAppList) list_tmp = gs_app_list_new (); + g_autoptr(GRWLockReaderLocker) locker = NULL; + + if (!gs_flatpak_rescan_appstream_store (self, cancellable, error)) + return FALSE; + + locker = g_rw_lock_reader_locker_new (&self->silo_lock); + if (!gs_appstream_add_recent (self->plugin, self->silo, list_tmp, age, + cancellable, error)) + return FALSE; + + gs_flatpak_claim_app_list (self, list_tmp); + gs_app_list_add_list (list, list_tmp); + + return TRUE; +} + +const gchar * +gs_flatpak_get_id (GsFlatpak *self) +{ + if (self->id == NULL) { + GString *str = g_string_new ("flatpak"); + g_string_append_printf (str, "-%s", + as_app_scope_to_string (self->scope)); + if (flatpak_installation_get_id (self->installation) != NULL) { + g_string_append_printf (str, "-%s", + flatpak_installation_get_id (self->installation)); + } + if (self->flags & GS_FLATPAK_FLAG_IS_TEMPORARY) + g_string_append (str, "-temp"); + self->id = g_string_free (str, FALSE); + } + return self->id; +} + +AsAppScope +gs_flatpak_get_scope (GsFlatpak *self) +{ + return self->scope; +} + +FlatpakInstallation * +gs_flatpak_get_installation (GsFlatpak *self) +{ + return self->installation; +} + +static void +gs_flatpak_finalize (GObject *object) +{ + GsFlatpak *self; + g_return_if_fail (GS_IS_FLATPAK (object)); + self = GS_FLATPAK (object); + + if (self->changed_id > 0) { + g_signal_handler_disconnect (self->monitor, self->changed_id); + self->changed_id = 0; + } + if (self->silo != NULL) + g_object_unref (self->silo); + + g_free (self->id); + g_object_unref (self->installation); + g_clear_pointer (&self->installed_refs, g_ptr_array_unref); + g_mutex_clear (&self->installed_refs_mutex); + g_object_unref (self->plugin); + g_hash_table_unref (self->broken_remotes); + g_mutex_clear (&self->broken_remotes_mutex); + g_rw_lock_clear (&self->silo_lock); + g_hash_table_unref (self->app_silos); + g_mutex_clear (&self->app_silos_mutex); + + G_OBJECT_CLASS (gs_flatpak_parent_class)->finalize (object); +} + +static void +gs_flatpak_class_init (GsFlatpakClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = gs_flatpak_finalize; +} + +static void +gs_flatpak_init (GsFlatpak *self) +{ + /* XbSilo needs external locking as we destroy the silo and build a new + * one when something changes */ + g_rw_lock_init (&self->silo_lock); + + g_mutex_init (&self->installed_refs_mutex); + self->installed_refs = NULL; + g_mutex_init (&self->broken_remotes_mutex); + self->broken_remotes = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, NULL); + self->app_silos = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref); + g_mutex_init (&self->app_silos_mutex); +} + +GsFlatpak * +gs_flatpak_new (GsPlugin *plugin, FlatpakInstallation *installation, GsFlatpakFlags flags) +{ + GsFlatpak *self; + self = g_object_new (GS_TYPE_FLATPAK, NULL); + self->installation = g_object_ref (installation); + self->scope = flatpak_installation_get_is_user (installation) + ? AS_APP_SCOPE_USER : AS_APP_SCOPE_SYSTEM; + self->plugin = g_object_ref (plugin); + self->flags = flags; + return GS_FLATPAK (self); +} diff --git a/plugins/flatpak/gs-flatpak.h b/plugins/flatpak/gs-flatpak.h new file mode 100644 index 0000000..e5af289 --- /dev/null +++ b/plugins/flatpak/gs-flatpak.h @@ -0,0 +1,128 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Joaquim Rocha <jrocha@endlessm.com> + * Copyright (C) 2016-2018 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gnome-software.h> +#include <flatpak.h> + +G_BEGIN_DECLS + +#define GS_TYPE_FLATPAK (gs_flatpak_get_type ()) + +G_DECLARE_FINAL_TYPE (GsFlatpak, gs_flatpak, GS, FLATPAK, GObject) + +typedef enum { + GS_FLATPAK_FLAG_NONE = 0, + GS_FLATPAK_FLAG_IS_TEMPORARY = 1 << 0, + /*< private >*/ + GS_FLATPAK_FLAG_LAST +} GsFlatpakFlags; + +GsFlatpak *gs_flatpak_new (GsPlugin *plugin, + FlatpakInstallation *installation, + GsFlatpakFlags flags); +FlatpakInstallation *gs_flatpak_get_installation (GsFlatpak *self); + +GsApp *gs_flatpak_ref_to_app (GsFlatpak *self, const gchar *ref, GCancellable *cancellable, GError **error); + +AsAppScope gs_flatpak_get_scope (GsFlatpak *self); +const gchar *gs_flatpak_get_id (GsFlatpak *self); +gboolean gs_flatpak_setup (GsFlatpak *self, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_add_installed (GsFlatpak *self, + GsAppList *list, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_add_sources (GsFlatpak *self, + GsAppList *list, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_add_updates (GsFlatpak *self, + GsAppList *list, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_refresh (GsFlatpak *self, + guint cache_age, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_refine_app (GsFlatpak *self, + GsApp *app, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_refine_app_state (GsFlatpak *self, + GsApp *app, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_refine_wildcard (GsFlatpak *self, + GsApp *app, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_launch (GsFlatpak *self, + GsApp *app, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_app_remove_source (GsFlatpak *self, + GsApp *app, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_app_install_source (GsFlatpak *self, + GsApp *app, + GCancellable *cancellable, + GError **error); +GsApp *gs_flatpak_file_to_app_ref (GsFlatpak *self, + GFile *file, + GCancellable *cancellable, + GError **error); +GsApp *gs_flatpak_file_to_app_bundle (GsFlatpak *self, + GFile *file, + GCancellable *cancellable, + GError **error); +GsApp *gs_flatpak_find_source_by_url (GsFlatpak *self, + const gchar *name, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_search (GsFlatpak *self, + const gchar * const *values, + GsAppList *list, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_add_categories (GsFlatpak *self, + GPtrArray *list, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_add_category_apps (GsFlatpak *self, + GsCategory *category, + GsAppList *list, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_add_popular (GsFlatpak *self, + GsAppList *list, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_add_featured (GsFlatpak *self, + GsAppList *list, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_add_alternates (GsFlatpak *self, + GsApp *app, + GsAppList *list, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_add_recent (GsFlatpak *self, + GsAppList *list, + guint64 age, + GCancellable *cancellable, + GError **error); + +G_END_DECLS diff --git a/plugins/flatpak/gs-plugin-flatpak.c b/plugins/flatpak/gs-plugin-flatpak.c new file mode 100644 index 0000000..5b7a549 --- /dev/null +++ b/plugins/flatpak/gs-plugin-flatpak.c @@ -0,0 +1,1320 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Joaquim Rocha <jrocha@endlessm.com> + * Copyright (C) 2016-2018 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2017-2020 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/* Notes: + * + * All GsApp's created have management-plugin set to flatpak + * Some GsApp's created have have flatpak::kind of app or runtime + * The GsApp:origin is the remote name, e.g. test-repo + */ + +#include <config.h> + +#include <flatpak.h> +#include <gnome-software.h> + +#include "gs-appstream.h" +#include "gs-flatpak-app.h" +#include "gs-flatpak.h" +#include "gs-flatpak-transaction.h" +#include "gs-flatpak-utils.h" +#include "gs-metered.h" + +struct GsPluginData { + GPtrArray *flatpaks; /* of GsFlatpak */ + gboolean has_system_helper; + const gchar *destdir_for_tests; +}; + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData)); + const gchar *action_id = "org.freedesktop.Flatpak.appstream-update"; + g_autoptr(GError) error_local = NULL; + g_autoptr(GPermission) permission = NULL; + + priv->flatpaks = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + + /* getting app properties from appstream is quicker */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "appstream"); + + /* like appstream, we need the icon plugin to load cached icons into pixbufs */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_BEFORE, "icons"); + + /* prioritize over packages */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_BETTER_THAN, "packagekit"); + + /* set name of MetaInfo file */ + gs_plugin_set_appstream_id (plugin, "org.gnome.Software.Plugin.Flatpak"); + + /* if we can't update the AppStream database system-wide don't even + * pull the data as we can't do anything with it */ + permission = gs_utils_get_permission (action_id, NULL, &error_local); + if (permission == NULL) { + g_debug ("no permission for %s: %s", action_id, error_local->message); + g_clear_error (&error_local); + } else { + priv->has_system_helper = g_permission_get_allowed (permission) || + g_permission_get_can_acquire (permission); + } + + /* used for self tests */ + priv->destdir_for_tests = g_getenv ("GS_SELF_TEST_FLATPAK_DATADIR"); +} + +static gboolean +_as_app_scope_is_compatible (AsAppScope scope1, AsAppScope scope2) +{ + if (scope1 == AS_APP_SCOPE_UNKNOWN) + return TRUE; + if (scope2 == AS_APP_SCOPE_UNKNOWN) + return TRUE; + return scope1 == scope2; +} + +void +gs_plugin_destroy (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_ptr_array_unref (priv->flatpaks); +} + +void +gs_plugin_adopt_app (GsPlugin *plugin, GsApp *app) +{ + if (gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_FLATPAK) + gs_app_set_management_plugin (app, gs_plugin_get_name (plugin)); +} + +static gboolean +gs_plugin_flatpak_add_installation (GsPlugin *plugin, + FlatpakInstallation *installation, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GsFlatpak) flatpak = NULL; + + /* create and set up */ + flatpak = gs_flatpak_new (plugin, installation, GS_FLATPAK_FLAG_NONE); + if (!gs_flatpak_setup (flatpak, cancellable, error)) + return FALSE; + g_debug ("successfully set up %s", gs_flatpak_get_id (flatpak)); + + /* add objects that set up correctly */ + g_ptr_array_add (priv->flatpaks, g_steal_pointer (&flatpak)); + return TRUE; +} + +gboolean +gs_plugin_setup (GsPlugin *plugin, GCancellable *cancellable, GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + + /* clear in case we're called from resetup in the self tests */ + g_ptr_array_set_size (priv->flatpaks, 0); + + /* we use a permissions helper to elevate privs */ + if (priv->has_system_helper && priv->destdir_for_tests == NULL) { + g_autoptr(GPtrArray) installations = NULL; + installations = flatpak_get_system_installations (cancellable, error); + if (installations == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + for (guint i = 0; i < installations->len; i++) { + FlatpakInstallation *installation = g_ptr_array_index (installations, i); + if (!gs_plugin_flatpak_add_installation (plugin, installation, + cancellable, error)) { + return FALSE; + } + } + } + + /* in gs-self-test */ + if (priv->destdir_for_tests != NULL) { + g_autofree gchar *full_path = g_build_filename (priv->destdir_for_tests, + "flatpak", + NULL); + g_autoptr(GFile) file = g_file_new_for_path (full_path); + g_autoptr(FlatpakInstallation) installation = NULL; + g_debug ("using custom flatpak path %s", full_path); + installation = flatpak_installation_new_for_path (file, TRUE, + cancellable, + error); + if (installation == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + if (!gs_plugin_flatpak_add_installation (plugin, installation, + cancellable, error)) { + return FALSE; + } + } + + /* per-user installations always available when not in self tests */ + if (priv->destdir_for_tests == NULL) { + g_autoptr(FlatpakInstallation) installation = NULL; + installation = flatpak_installation_new_user (cancellable, error); + if (installation == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + if (!gs_plugin_flatpak_add_installation (plugin, installation, + cancellable, error)) { + return FALSE; + } + } + + return TRUE; +} + +gboolean +gs_plugin_add_installed (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + for (guint i = 0; i < priv->flatpaks->len; i++) { + GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i); + if (!gs_flatpak_add_installed (flatpak, list, cancellable, error)) + return FALSE; + } + return TRUE; +} + +gboolean +gs_plugin_add_sources (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + for (guint i = 0; i < priv->flatpaks->len; i++) { + GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i); + if (!gs_flatpak_add_sources (flatpak, list, cancellable, error)) + return FALSE; + } + return TRUE; +} + +gboolean +gs_plugin_add_updates (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + for (guint i = 0; i < priv->flatpaks->len; i++) { + GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i); + if (!gs_flatpak_add_updates (flatpak, list, cancellable, error)) + return FALSE; + } + return TRUE; +} + +gboolean +gs_plugin_refresh (GsPlugin *plugin, + guint cache_age, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + for (guint i = 0; i < priv->flatpaks->len; i++) { + GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i); + if (!gs_flatpak_refresh (flatpak, cache_age, cancellable, error)) + return FALSE; + } + return TRUE; +} + +static GsFlatpak * +gs_plugin_flatpak_get_handler (GsPlugin *plugin, GsApp *app) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + const gchar *object_id; + + /* only process this app if was created by this plugin */ + if (g_strcmp0 (gs_app_get_management_plugin (app), + gs_plugin_get_name (plugin)) != 0) { + return NULL; + } + + /* specified an explicit name */ + object_id = gs_flatpak_app_get_object_id (app); + if (object_id != NULL) { + for (guint i = 0; i < priv->flatpaks->len; i++) { + GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i); + if (g_strcmp0 (gs_flatpak_get_id (flatpak), object_id) == 0) + return flatpak; + } + } + + /* find a scope that matches */ + for (guint i = 0; i < priv->flatpaks->len; i++) { + GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i); + if (_as_app_scope_is_compatible (gs_flatpak_get_scope (flatpak), + gs_app_get_scope (app))) + return flatpak; + } + return NULL; +} + +static gboolean +gs_plugin_flatpak_refine_app (GsPlugin *plugin, + GsApp *app, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + GsFlatpak *flatpak = NULL; + + /* not us */ + if (gs_app_get_bundle_kind (app) != AS_BUNDLE_KIND_FLATPAK) { + g_debug ("%s not a package, ignoring", gs_app_get_unique_id (app)); + return TRUE; + } + + /* we have to look for the app in all GsFlatpak stores */ + if (gs_app_get_scope (app) == AS_APP_SCOPE_UNKNOWN) { + for (guint i = 0; i < priv->flatpaks->len; i++) { + GsFlatpak *flatpak_tmp = g_ptr_array_index (priv->flatpaks, i); + g_autoptr(GError) error_local = NULL; + if (gs_flatpak_refine_app_state (flatpak_tmp, app, + cancellable, &error_local)) { + flatpak = flatpak_tmp; + break; + } else { + g_debug ("%s", error_local->message); + } + } + } else { + flatpak = gs_plugin_flatpak_get_handler (plugin, app); + } + if (flatpak == NULL) + return TRUE; + return gs_flatpak_refine_app (flatpak, app, flags, cancellable, error); +} + + +static gboolean +refine_app (GsPlugin *plugin, + GsApp *app, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + /* only process this app if was created by this plugin */ + if (g_strcmp0 (gs_app_get_management_plugin (app), + gs_plugin_get_name (plugin)) != 0) { + return TRUE; + } + + /* get the runtime first */ + if (!gs_plugin_flatpak_refine_app (plugin, app, flags, cancellable, error)) + return FALSE; + + /* the runtime might be installed in a different scope */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME) { + GsApp *runtime = gs_app_get_runtime (app); + if (runtime != NULL) { + if (!gs_plugin_flatpak_refine_app (plugin, app, + flags, + cancellable, + error)) { + return FALSE; + } + } + } + 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; +} + +gboolean +gs_plugin_refine_wildcard (GsPlugin *plugin, + GsApp *app, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + for (guint i = 0; i < priv->flatpaks->len; i++) { + GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i); + if (!gs_flatpak_refine_wildcard (flatpak, app, list, flags, + cancellable, error)) { + return FALSE; + } + } + return TRUE; +} + +gboolean +gs_plugin_launch (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsFlatpak *flatpak = gs_plugin_flatpak_get_handler (plugin, app); + if (flatpak == NULL) + return TRUE; + return gs_flatpak_launch (flatpak, app, cancellable, error); +} + +/* ref full */ +static GsApp * +gs_plugin_flatpak_find_app_by_ref (GsPlugin *plugin, const gchar *ref, + GCancellable *cancellable, GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + + g_debug ("finding ref %s", ref); + for (guint i = 0; i < priv->flatpaks->len; i++) { + GsFlatpak *flatpak_tmp = g_ptr_array_index (priv->flatpaks, i); + g_autoptr(GsApp) app = NULL; + g_autoptr(GError) error_local = NULL; + + app = gs_flatpak_ref_to_app (flatpak_tmp, ref, cancellable, &error_local); + if (app == NULL) { + g_debug ("%s", error_local->message); + continue; + } + g_debug ("found ref=%s->%s", ref, gs_app_get_unique_id (app)); + return g_steal_pointer (&app); + } + return NULL; +} + +/* ref full */ +static GsApp * +_ref_to_app (FlatpakTransaction *transaction, const gchar *ref, GsPlugin *plugin) +{ + g_return_val_if_fail (GS_IS_FLATPAK_TRANSACTION (transaction), NULL); + g_return_val_if_fail (ref != NULL, NULL); + g_return_val_if_fail (GS_IS_PLUGIN (plugin), NULL); + + /* search through each GsFlatpak */ + return gs_plugin_flatpak_find_app_by_ref (plugin, ref, NULL, NULL); +} + +/* + * Returns: (transfer full) (element-type GsFlatpak GsAppList): + * a map from GsFlatpak to non-empty lists of apps from @list associated + * with that installation. + */ +static GHashTable * +_group_apps_by_installation (GsPlugin *plugin, + GsAppList *list) +{ + g_autoptr(GHashTable) applist_by_flatpaks = NULL; + + /* list of apps to be handled by each flatpak installation */ + applist_by_flatpaks = g_hash_table_new_full (g_direct_hash, g_direct_equal, + (GDestroyNotify) g_object_unref, + (GDestroyNotify) g_object_unref); + + /* put each app into the correct per-GsFlatpak list */ + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + GsFlatpak *flatpak = gs_plugin_flatpak_get_handler (plugin, app); + if (flatpak != NULL) { + GsAppList *list_tmp = g_hash_table_lookup (applist_by_flatpaks, flatpak); + if (list_tmp == NULL) { + list_tmp = gs_app_list_new (); + g_hash_table_insert (applist_by_flatpaks, + g_object_ref (flatpak), + list_tmp); + } + gs_app_list_add (list_tmp, app); + } + } + + return g_steal_pointer (&applist_by_flatpaks); +} + +#if FLATPAK_CHECK_VERSION(1,6,0) +typedef struct { + FlatpakTransaction *transaction; + guint id; +} BasicAuthData; + +static void +basic_auth_data_free (BasicAuthData *data) +{ + g_object_unref (data->transaction); + g_slice_free (BasicAuthData, data); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(BasicAuthData, basic_auth_data_free) + +static void +_basic_auth_cb (const gchar *user, const gchar *password, gpointer user_data) +{ + g_autoptr(BasicAuthData) data = user_data; + + g_debug ("Submitting basic auth data"); + + /* NULL user aborts the basic auth request */ + flatpak_transaction_complete_basic_auth (data->transaction, data->id, user, password, NULL /* options */); +} + +static gboolean +_basic_auth_start (FlatpakTransaction *transaction, + const char *remote, + const char *realm, + GVariant *options, + guint id, + GsPlugin *plugin) +{ + BasicAuthData *data; + + if (!gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)) + return FALSE; + + data = g_slice_new0 (BasicAuthData); + data->transaction = g_object_ref (transaction); + data->id = id; + + g_debug ("Login required remote %s (realm %s)\n", remote, realm); + gs_plugin_basic_auth_start (plugin, remote, realm, G_CALLBACK (_basic_auth_cb), data); + return TRUE; +} + +static gboolean +_webflow_start (FlatpakTransaction *transaction, + const char *remote, + const char *url, + GVariant *options, + guint id, + GsPlugin *plugin) +{ + const char *browser; + g_autoptr(GError) error_local = NULL; + + if (!gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)) + return FALSE; + + g_debug ("Authentication required for remote '%s'", remote); + + /* Allow hard overrides with $BROWSER */ + browser = g_getenv ("BROWSER"); + if (browser != NULL) { + const char *args[3] = { NULL, url, NULL }; + args[0] = browser; + if (!g_spawn_async (NULL, (char **)args, NULL, G_SPAWN_SEARCH_PATH, + NULL, NULL, NULL, &error_local)) { + g_autoptr(GsPluginEvent) event = NULL; + + g_warning ("Failed to start browser %s: %s", browser, error_local->message); + + event = gs_plugin_event_new (); + gs_flatpak_error_convert (&error_local); + gs_plugin_event_set_error (event, error_local); + gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING); + gs_plugin_report_event (plugin, event); + + return FALSE; + } + } else { + if (!g_app_info_launch_default_for_uri (url, NULL, &error_local)) { + g_autoptr(GsPluginEvent) event = NULL; + + g_warning ("Failed to show url: %s", error_local->message); + + event = gs_plugin_event_new (); + gs_flatpak_error_convert (&error_local); + gs_plugin_event_set_error (event, error_local); + gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING); + gs_plugin_report_event (plugin, event); + + return FALSE; + } + } + + g_debug ("Waiting for browser..."); + + return TRUE; +} + +static void +_webflow_done (FlatpakTransaction *transaction, + GVariant *options, + guint id, + GsPlugin *plugin) +{ + g_debug ("Browser done"); +} +#endif + +static FlatpakTransaction * +_build_transaction (GsPlugin *plugin, GsFlatpak *flatpak, + GCancellable *cancellable, GError **error) +{ + FlatpakInstallation *installation; +#if !FLATPAK_CHECK_VERSION(1, 7, 3) + g_autoptr(GFile) installation_path = NULL; +#endif /* flatpak < 1.7.3 */ + g_autoptr(FlatpakInstallation) installation_clone = NULL; + g_autoptr(FlatpakTransaction) transaction = NULL; + + installation = gs_flatpak_get_installation (flatpak); + +#if !FLATPAK_CHECK_VERSION(1, 7, 3) + /* Operate on a copy of the installation so we can set the interactive + * flag for the duration of this transaction. */ + installation_path = flatpak_installation_get_path (installation); + installation_clone = flatpak_installation_new_for_path (installation_path, + flatpak_installation_get_is_user (installation), + cancellable, error); + if (installation_clone == NULL) + return NULL; + + /* Let flatpak know if it is a background operation */ + flatpak_installation_set_no_interaction (installation_clone, + !gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)); +#else /* if flatpak ≥ 1.7.3 */ + installation_clone = g_object_ref (installation); +#endif /* flatpak ≥ 1.7.3 */ + + /* create transaction */ + transaction = gs_flatpak_transaction_new (installation_clone, cancellable, error); + if (transaction == NULL) { + g_prefix_error (error, "failed to build transaction: "); + gs_flatpak_error_convert (error); + return NULL; + } + +#if FLATPAK_CHECK_VERSION(1, 7, 3) + /* Let flatpak know if it is a background operation */ + flatpak_transaction_set_no_interaction (transaction, + !gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)); +#endif /* flatpak ≥ 1.7.3 */ + + /* connect up signals */ + g_signal_connect (transaction, "ref-to-app", + G_CALLBACK (_ref_to_app), plugin); +#if FLATPAK_CHECK_VERSION(1,6,0) + g_signal_connect (transaction, "basic-auth-start", + G_CALLBACK (_basic_auth_start), plugin); + g_signal_connect (transaction, "webflow-start", + G_CALLBACK (_webflow_start), plugin); + g_signal_connect (transaction, "webflow-done", + G_CALLBACK (_webflow_done), plugin); +#endif + + /* use system installations as dependency sources for user installations */ + flatpak_transaction_add_default_dependency_sources (transaction); + + return g_steal_pointer (&transaction); +} + +gboolean +gs_plugin_download (GsPlugin *plugin, GsAppList *list, + GCancellable *cancellable, GError **error) +{ + g_autoptr(GHashTable) applist_by_flatpaks = NULL; + GHashTableIter iter; + gpointer key, value; + + /* build and run transaction for each flatpak installation */ + applist_by_flatpaks = _group_apps_by_installation (plugin, list); + g_hash_table_iter_init (&iter, applist_by_flatpaks); + while (g_hash_table_iter_next (&iter, &key, &value)) { + GsFlatpak *flatpak = GS_FLATPAK (key); + GsAppList *list_tmp = GS_APP_LIST (value); + g_autoptr(FlatpakTransaction) transaction = NULL; + + g_assert (GS_IS_FLATPAK (flatpak)); + g_assert (list_tmp != NULL); + g_assert (gs_app_list_length (list_tmp) > 0); + + if (!gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)) { + g_autoptr(GError) error_local = NULL; + + if (!gs_metered_block_app_list_on_download_scheduler (list_tmp, cancellable, &error_local)) { + g_warning ("Failed to block on download scheduler: %s", + error_local->message); + g_clear_error (&error_local); + } + } + + /* build and run non-deployed transaction */ + transaction = _build_transaction (plugin, flatpak, cancellable, error); + if (transaction == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } +#if !FLATPAK_CHECK_VERSION(1,5,1) + gs_flatpak_transaction_set_no_deploy (transaction, TRUE); +#else + flatpak_transaction_set_no_deploy (transaction, TRUE); +#endif + for (guint i = 0; i < gs_app_list_length (list_tmp); i++) { + GsApp *app = gs_app_list_index (list_tmp, i); + g_autofree gchar *ref = NULL; + + ref = gs_flatpak_app_get_ref_display (app); + if (!flatpak_transaction_add_update (transaction, ref, NULL, NULL, error)) { + gs_flatpak_error_convert (error); + return FALSE; + } + } + if (!gs_flatpak_transaction_run (transaction, cancellable, error)) { + gs_flatpak_error_convert (error); + return FALSE; + } + + /* Traverse over the GsAppList again and set that the update has been already downloaded + * for the apps. */ + for (guint i = 0; i < gs_app_list_length (list_tmp); i++) { + GsApp *app = gs_app_list_index (list_tmp, i); + gs_app_set_is_update_downloaded (app, TRUE); + } + } + + return TRUE; +} + +gboolean +gs_plugin_app_remove (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsFlatpak *flatpak; + g_autoptr(FlatpakTransaction) transaction = NULL; + g_autofree gchar *ref = NULL; + + /* not supported */ + flatpak = gs_plugin_flatpak_get_handler (plugin, app); + if (flatpak == NULL) + return TRUE; + + /* is a source */ + if (gs_app_get_kind (app) == AS_APP_KIND_SOURCE) + return gs_flatpak_app_remove_source (flatpak, app, cancellable, error); + + /* build and run transaction */ + transaction = _build_transaction (plugin, flatpak, cancellable, error); + if (transaction == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + + /* add to the transaction cache for quick look up -- other unrelated + * refs will be matched using gs_plugin_flatpak_find_app_by_ref() */ + gs_flatpak_transaction_add_app (transaction, app); + + ref = gs_flatpak_app_get_ref_display (app); + if (!flatpak_transaction_add_uninstall (transaction, ref, error)) { + gs_flatpak_error_convert (error); + return FALSE; + } + + /* run transaction */ + gs_app_set_state (app, AS_APP_STATE_REMOVING); + if (!gs_flatpak_transaction_run (transaction, cancellable, error)) { + gs_flatpak_error_convert (error); + gs_app_set_state_recover (app); + return FALSE; + } + + /* get any new state */ + if (!gs_flatpak_refresh (flatpak, G_MAXUINT, cancellable, error)) { + gs_flatpak_error_convert (error); + return FALSE; + } + if (!gs_flatpak_refine_app (flatpak, app, + GS_PLUGIN_REFINE_FLAGS_DEFAULT, + cancellable, error)) { + g_prefix_error (error, "failed to run refine for %s: ", ref); + gs_flatpak_error_convert (error); + return FALSE; + } + return TRUE; +} + +static gboolean +app_has_local_source (GsApp *app) +{ + const gchar *url = gs_app_get_origin_hostname (app); + return url != NULL && g_str_has_prefix (url, "file://"); +} + +gboolean +gs_plugin_app_install (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + GsFlatpak *flatpak; + g_autoptr(FlatpakTransaction) transaction = NULL; + + /* queue for install if installation needs the network */ + if (!app_has_local_source (app) && + !gs_plugin_get_network_available (plugin)) { + gs_app_set_state (app, AS_APP_STATE_QUEUED_FOR_INSTALL); + return TRUE; + } + + /* set the app scope */ + if (gs_app_get_scope (app) == AS_APP_SCOPE_UNKNOWN) { + g_autoptr(GSettings) settings = g_settings_new ("org.gnome.software"); + + /* get the new GsFlatpak for handling of local files */ + gs_app_set_scope (app, g_settings_get_boolean (settings, "install-bundles-system-wide") ? + AS_APP_SCOPE_SYSTEM : AS_APP_SCOPE_USER); + if (!priv->has_system_helper) { + g_info ("no flatpak system helper is available, using user"); + gs_app_set_scope (app, AS_APP_SCOPE_USER); + } + if (priv->destdir_for_tests != NULL) { + g_debug ("in self tests, using user"); + gs_app_set_scope (app, AS_APP_SCOPE_USER); + } + } + + /* not supported */ + flatpak = gs_plugin_flatpak_get_handler (plugin, app); + if (flatpak == NULL) + return TRUE; + + /* is a source */ + if (gs_app_get_kind (app) == AS_APP_KIND_SOURCE) + return gs_flatpak_app_install_source (flatpak, app, cancellable, error); + + if (!gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)) { + g_autoptr(GError) error_local = NULL; + + /* FIXME: Add additional details here, especially the download + * size bounds (using `size-minimum` and `size-maximum`, both + * type `t`). */ + if (!gs_metered_block_app_on_download_scheduler (app, cancellable, &error_local)) { + g_warning ("Failed to block on download scheduler: %s", + error_local->message); + g_clear_error (&error_local); + } + } + + /* build */ + transaction = _build_transaction (plugin, flatpak, cancellable, error); + if (transaction == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + + /* add to the transaction cache for quick look up -- other unrelated + * refs will be matched using gs_plugin_flatpak_find_app_by_ref() */ + gs_flatpak_transaction_add_app (transaction, app); + + /* add flatpakref */ + if (gs_flatpak_app_get_file_kind (app) == GS_FLATPAK_APP_FILE_KIND_REF) { + GFile *file = gs_app_get_local_file (app); + g_autoptr(GBytes) blob = NULL; + if (file == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "no local file set for bundle %s", + gs_app_get_unique_id (app)); + return FALSE; + } + blob = g_file_load_bytes (file, cancellable, NULL, error); + if (blob == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + if (!flatpak_transaction_add_install_flatpakref (transaction, blob, error)) { + gs_flatpak_error_convert (error); + return FALSE; + } + + /* add bundle */ + } else if (gs_flatpak_app_get_file_kind (app) == GS_FLATPAK_APP_FILE_KIND_BUNDLE) { + GFile *file = gs_app_get_local_file (app); + if (file == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "no local file set for bundle %s", + gs_app_get_unique_id (app)); + return FALSE; + } + if (!flatpak_transaction_add_install_bundle (transaction, file, + NULL, error)) { + gs_flatpak_error_convert (error); + return FALSE; + } + + /* add normal ref */ + } else { + g_autofree gchar *ref = gs_flatpak_app_get_ref_display (app); + if (!flatpak_transaction_add_install (transaction, + gs_app_get_origin (app), + ref, NULL, error)) { + gs_flatpak_error_convert (error); + return FALSE; + } + } + + /* run transaction */ + gs_app_set_state (app, AS_APP_STATE_INSTALLING); + if (!gs_flatpak_transaction_run (transaction, cancellable, error)) { + gs_flatpak_error_convert (error); + gs_app_set_state_recover (app); + return FALSE; + } + + /* get any new state */ + if (!gs_flatpak_refresh (flatpak, G_MAXUINT, cancellable, error)) { + gs_flatpak_error_convert (error); + return FALSE; + } + if (!gs_flatpak_refine_app (flatpak, app, + GS_PLUGIN_REFINE_FLAGS_DEFAULT, + cancellable, error)) { + g_prefix_error (error, "failed to run refine for %s: ", + gs_app_get_unique_id (app)); + gs_flatpak_error_convert (error); + return FALSE; + } + return TRUE; +} + +static gboolean +gs_plugin_flatpak_update (GsPlugin *plugin, + GsFlatpak *flatpak, + GsAppList *list_tmp, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(FlatpakTransaction) transaction = NULL; + gboolean is_update_downloaded = TRUE; + + /* build and run transaction */ + transaction = _build_transaction (plugin, flatpak, cancellable, error); + if (transaction == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + + for (guint i = 0; i < gs_app_list_length (list_tmp); i++) { + GsApp *app = gs_app_list_index (list_tmp, i); + g_autofree gchar *ref = NULL; + + ref = gs_flatpak_app_get_ref_display (app); + if (!flatpak_transaction_add_update (transaction, ref, NULL, NULL, error)) { + gs_flatpak_error_convert (error); + return FALSE; + } + + /* add to the transaction cache for quick look up -- other unrelated + * refs will be matched using gs_plugin_flatpak_find_app_by_ref() */ + gs_flatpak_transaction_add_app (transaction, app); + } + + /* run transaction */ + for (guint i = 0; i < gs_app_list_length (list_tmp); i++) { + GsApp *app = gs_app_list_index (list_tmp, i); + gs_app_set_state (app, AS_APP_STATE_INSTALLING); + + /* If all apps' update are previously downloaded and available locally, + * FlatpakTransaction should run with no-pull flag. This is the case + * for apps' autoupdates. */ + is_update_downloaded &= gs_app_get_is_update_downloaded (app); + } + + if (is_update_downloaded) + flatpak_transaction_set_no_pull (transaction, TRUE); + + if (!gs_flatpak_transaction_run (transaction, cancellable, error)) { + for (guint i = 0; i < gs_app_list_length (list_tmp); i++) { + GsApp *app = gs_app_list_index (list_tmp, i); + gs_app_set_state_recover (app); + } + gs_flatpak_error_convert (error); + return FALSE; + } + gs_plugin_updates_changed (plugin); + + /* get any new state */ + if (!gs_flatpak_refresh (flatpak, G_MAXUINT, cancellable, error)) { + gs_flatpak_error_convert (error); + return FALSE; + } + for (guint i = 0; i < gs_app_list_length (list_tmp); i++) { + GsApp *app = gs_app_list_index (list_tmp, i); + g_autofree gchar *ref = NULL; + + ref = gs_flatpak_app_get_ref_display (app); + if (!gs_flatpak_refine_app (flatpak, app, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME, + cancellable, error)) { + g_prefix_error (error, "failed to run refine for %s: ", ref); + gs_flatpak_error_convert (error); + return FALSE; + } + } + return TRUE; +} + +gboolean +gs_plugin_update (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GHashTable) applist_by_flatpaks = NULL; + GHashTableIter iter; + gpointer key, value; + + /* build and run transaction for each flatpak installation */ + applist_by_flatpaks = _group_apps_by_installation (plugin, list); + g_hash_table_iter_init (&iter, applist_by_flatpaks); + while (g_hash_table_iter_next (&iter, &key, &value)) { + GsFlatpak *flatpak = GS_FLATPAK (key); + GsAppList *list_tmp = GS_APP_LIST (value); + + g_assert (GS_IS_FLATPAK (flatpak)); + g_assert (list_tmp != NULL); + g_assert (gs_app_list_length (list_tmp) > 0); + + if (!gs_plugin_flatpak_update (plugin, flatpak, list_tmp, cancellable, error)) + return FALSE; + } + return TRUE; +} + +static GsApp * +gs_plugin_flatpak_file_to_app_repo (GsPlugin *plugin, + GFile *file, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GsApp) app = NULL; + + /* parse the repo file */ + app = gs_flatpak_app_new_from_repo_file (file, cancellable, error); + if (app == NULL) + return NULL; + + /* already exists */ + for (guint i = 0; i < priv->flatpaks->len; i++) { + GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i); + g_autoptr(GError) error_local = NULL; + g_autoptr(GsApp) app_tmp = NULL; + app_tmp = gs_flatpak_find_source_by_url (flatpak, + gs_flatpak_app_get_repo_url (app), + cancellable, &error_local); + if (app_tmp == NULL) { + g_debug ("%s", error_local->message); + continue; + } + return g_steal_pointer (&app_tmp); + } + + /* this is new */ + gs_app_set_management_plugin (app, gs_plugin_get_name (plugin)); + return g_steal_pointer (&app); +} + +static GsFlatpak * +gs_plugin_flatpak_create_temporary (GsPlugin *plugin, GCancellable *cancellable, GError **error) +{ + g_autofree gchar *installation_path = NULL; + g_autoptr(FlatpakInstallation) installation = NULL; + g_autoptr(GFile) installation_file = NULL; + + /* create new per-user installation in a cache dir */ + installation_path = gs_utils_get_cache_filename ("flatpak", + "installation-tmp", + GS_UTILS_CACHE_FLAG_WRITEABLE | + GS_UTILS_CACHE_FLAG_ENSURE_EMPTY, + error); + if (installation_path == NULL) + return NULL; + installation_file = g_file_new_for_path (installation_path); + installation = flatpak_installation_new_for_path (installation_file, + TRUE, /* user */ + cancellable, + error); + if (installation == NULL) { + gs_flatpak_error_convert (error); + return NULL; + } + return gs_flatpak_new (plugin, installation, GS_FLATPAK_FLAG_IS_TEMPORARY); +} + +static GsApp * +gs_plugin_flatpak_file_to_app_bundle (GsPlugin *plugin, + GFile *file, + GCancellable *cancellable, + GError **error) +{ + g_autofree gchar *ref = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsApp) app_tmp = NULL; + g_autoptr(GsFlatpak) flatpak_tmp = NULL; + + /* only use the temporary GsFlatpak to avoid the auth dialog */ + flatpak_tmp = gs_plugin_flatpak_create_temporary (plugin, cancellable, error); + if (flatpak_tmp == NULL) + return NULL; + + /* add object */ + app = gs_flatpak_file_to_app_bundle (flatpak_tmp, file, cancellable, error); + if (app == NULL) + return NULL; + + /* is this already installed or available in a configured remote */ + ref = gs_flatpak_app_get_ref_display (app); + app_tmp = gs_plugin_flatpak_find_app_by_ref (plugin, ref, cancellable, NULL); + if (app_tmp != NULL) + return g_steal_pointer (&app_tmp); + + /* force this to be 'any' scope for installation */ + gs_app_set_scope (app, AS_APP_SCOPE_UNKNOWN); + + /* this is new */ + return g_steal_pointer (&app); +} + +static GsApp * +gs_plugin_flatpak_file_to_app_ref (GsPlugin *plugin, + GFile *file, + GCancellable *cancellable, + GError **error) +{ + GsApp *runtime; + g_autofree gchar *ref = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsApp) app_tmp = NULL; + g_autoptr(GsFlatpak) flatpak_tmp = NULL; + + /* only use the temporary GsFlatpak to avoid the auth dialog */ + flatpak_tmp = gs_plugin_flatpak_create_temporary (plugin, cancellable, error); + if (flatpak_tmp == NULL) + return NULL; + + /* add object */ + app = gs_flatpak_file_to_app_ref (flatpak_tmp, file, cancellable, error); + if (app == NULL) + return NULL; + + /* is this already installed or available in a configured remote */ + ref = gs_flatpak_app_get_ref_display (app); + app_tmp = gs_plugin_flatpak_find_app_by_ref (plugin, ref, cancellable, NULL); + if (app_tmp != NULL) + return g_steal_pointer (&app_tmp); + + /* force this to be 'any' scope for installation */ + gs_app_set_scope (app, AS_APP_SCOPE_UNKNOWN); + + /* do we have a system runtime available */ + runtime = gs_app_get_runtime (app); + if (runtime != NULL) { + g_autoptr(GsApp) runtime_tmp = NULL; + g_autofree gchar *runtime_ref = gs_flatpak_app_get_ref_display (runtime); + runtime_tmp = gs_plugin_flatpak_find_app_by_ref (plugin, + runtime_ref, + cancellable, + NULL); + if (runtime_tmp != NULL) { + gs_app_set_runtime (app, runtime_tmp); + } else { + /* the new runtime is available from the RuntimeRepo */ + if (gs_flatpak_app_get_runtime_url (runtime) != NULL) + gs_app_set_state (runtime, AS_APP_STATE_AVAILABLE_LOCAL); + } + } + + /* this is new */ + return g_steal_pointer (&app); +} + +gboolean +gs_plugin_file_to_app (GsPlugin *plugin, + GsAppList *list, + GFile *file, + GCancellable *cancellable, + GError **error) +{ + g_autofree gchar *content_type = NULL; + g_autoptr(GsApp) app = NULL; + const gchar *mimetypes_bundle[] = { + "application/vnd.flatpak", + NULL }; + const gchar *mimetypes_repo[] = { + "application/vnd.flatpak.repo", + NULL }; + const gchar *mimetypes_ref[] = { + "application/vnd.flatpak.ref", + NULL }; + + /* does this match any of the mimetypes we support */ + content_type = gs_utils_get_content_type (file, cancellable, error); + if (content_type == NULL) + return FALSE; + if (g_strv_contains (mimetypes_bundle, content_type)) { + app = gs_plugin_flatpak_file_to_app_bundle (plugin, file, + cancellable, error); + if (app == NULL) + return FALSE; + } else if (g_strv_contains (mimetypes_repo, content_type)) { + app = gs_plugin_flatpak_file_to_app_repo (plugin, file, + cancellable, error); + if (app == NULL) + return FALSE; + } else if (g_strv_contains (mimetypes_ref, content_type)) { + app = gs_plugin_flatpak_file_to_app_ref (plugin, file, + cancellable, error); + if (app == NULL) + return FALSE; + } + if (app != NULL) + gs_app_list_add (list, app); + return TRUE; +} + +gboolean +gs_plugin_add_search (GsPlugin *plugin, + gchar **values, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + for (guint i = 0; i < priv->flatpaks->len; i++) { + GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i); + if (!gs_flatpak_search (flatpak, (const gchar * const *) values, list, + cancellable, error)) { + return FALSE; + } + } + return TRUE; +} + +gboolean +gs_plugin_add_categories (GsPlugin *plugin, + GPtrArray *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + for (guint i = 0; i < priv->flatpaks->len; i++) { + GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i); + if (!gs_flatpak_add_categories (flatpak, list, cancellable, error)) + return FALSE; + } + 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); + for (guint i = 0; i < priv->flatpaks->len; i++) { + GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i); + if (!gs_flatpak_add_category_apps (flatpak, + category, + list, + cancellable, + error)) { + return FALSE; + } + } + return TRUE; +} + +gboolean +gs_plugin_add_popular (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + for (guint i = 0; i < priv->flatpaks->len; i++) { + GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i); + if (!gs_flatpak_add_popular (flatpak, list, cancellable, error)) + return FALSE; + } + return TRUE; +} + +gboolean +gs_plugin_add_alternates (GsPlugin *plugin, + GsApp *app, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + for (guint i = 0; i < priv->flatpaks->len; i++) { + GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i); + if (!gs_flatpak_add_alternates (flatpak, app, list, cancellable, error)) + return FALSE; + } + return TRUE; +} + +gboolean +gs_plugin_add_featured (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + for (guint i = 0; i < priv->flatpaks->len; i++) { + GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i); + if (!gs_flatpak_add_featured (flatpak, list, cancellable, error)) + return FALSE; + } + return TRUE; +} + +gboolean +gs_plugin_add_recent (GsPlugin *plugin, + GsAppList *list, + guint64 age, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + for (guint i = 0; i < priv->flatpaks->len; i++) { + GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i); + if (!gs_flatpak_add_recent (flatpak, list, age, cancellable, error)) + return FALSE; + } + return TRUE; +} diff --git a/plugins/flatpak/gs-self-test.c b/plugins/flatpak/gs-self-test.c new file mode 100644 index 0000000..ae0acda --- /dev/null +++ b/plugins/flatpak/gs-self-test.c @@ -0,0 +1,1936 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2018 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2017 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gstdio.h> + +#include "gnome-software-private.h" + +#include "gs-flatpak-app.h" + +#include "gs-test.h" + +static gboolean +gs_flatpak_test_write_repo_file (const gchar *fn, const gchar *testdir, GFile **file_out, GError **error) +{ + g_autofree gchar *testdir_repourl = NULL; + g_autoptr(GString) str = g_string_new (NULL); + g_autofree gchar *path = NULL; + + /* create file */ + testdir_repourl = g_strdup_printf ("file://%s/repo", testdir); + g_string_append (str, "[Flatpak Repo]\n"); + g_string_append (str, "Title=foo-bar\n"); + g_string_append (str, "Comment=Longer one line comment\n"); + g_string_append (str, "Description=Longer multiline comment that " + "does into detail.\n"); + g_string_append (str, "DefaultBranch=master\n"); + g_string_append_printf (str, "Url=%s\n", testdir_repourl); + g_string_append (str, "Homepage=http://foo.bar\n"); + + path = g_build_filename (g_getenv ("GS_SELF_TEST_FLATPAK_DATADIR"), fn, NULL); + *file_out = g_file_new_for_path (path); + + return g_file_set_contents (path, str->str, -1, error); +} + +static gboolean +gs_flatpak_test_write_ref_file (const gchar *filename, const gchar *url, const gchar *runtimerepo, GFile **file_out, GError **error) +{ + g_autoptr(GString) str = g_string_new (NULL); + g_autofree gchar *path = NULL; + + g_return_val_if_fail (filename != NULL, FALSE); + g_return_val_if_fail (url != NULL, FALSE); + g_return_val_if_fail (runtimerepo != NULL, FALSE); + + g_string_append (str, "[Flatpak Ref]\n"); + g_string_append (str, "Title=Chiron\n"); + g_string_append (str, "Name=org.test.Chiron\n"); + g_string_append (str, "Branch=master\n"); + g_string_append_printf (str, "Url=%s\n", url); + g_string_append (str, "IsRuntime=False\n"); + g_string_append (str, "Comment=Single line synopsis\n"); + g_string_append (str, "Description=A Testing Application\n"); + g_string_append (str, "Icon=https://getfedora.org/static/images/fedora-logotext.png\n"); + g_string_append_printf (str, "RuntimeRepo=%s\n", runtimerepo); + + path = g_build_filename (g_getenv ("GS_SELF_TEST_FLATPAK_DATADIR"), filename, NULL); + *file_out = g_file_new_for_path (path); + + return g_file_set_contents (path, str->str, -1, error); +} + +/* create duplicate file as if downloaded in firefox */ +static void +gs_plugins_flatpak_repo_non_ascii_func (GsPluginLoader *plugin_loader) +{ + const gchar *fn = "example (1)….flatpakrepo"; + gboolean ret; + g_autofree gchar *testdir = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* get a resolvable */ + testdir = gs_test_get_filename (TESTDATADIR, "app-with-runtime"); + if (testdir == NULL) + return; + + ret = gs_flatpak_test_write_repo_file (fn, testdir, &file, &error); + g_assert_no_error (error); + g_assert_true (ret); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP, + "file", file, + NULL); + app = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (app != NULL); + g_assert_cmpstr (gs_app_get_unique_id (app), ==, "*/*/*/source/example__1____/master"); +} + +static void +gs_plugins_flatpak_repo_func (GsPluginLoader *plugin_loader) +{ + const gchar *group_name = "remote \"example\""; + const gchar *root = NULL; + const gchar *fn = "example.flatpakrepo"; + gboolean ret; + g_autofree gchar *config_fn = NULL; + g_autofree gchar *remote_url = NULL; + g_autofree gchar *testdir = NULL; + g_autofree gchar *testdir_repourl = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GKeyFile) kf = NULL; + g_autoptr(GsApp) app2 = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* no flatpak, abort */ + if (!gs_plugin_loader_get_enabled (plugin_loader, "flatpak")) + return; + + /* get a resolvable */ + testdir = gs_test_get_filename (TESTDATADIR, "app-with-runtime"); + if (testdir == NULL) + return; + testdir_repourl = g_strdup_printf ("file://%s/repo", testdir); + + /* create file */ + ret = gs_flatpak_test_write_repo_file (fn, testdir, &file, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* load local file */ + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP, + "file", file, + NULL); + app = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (app != NULL); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_APP_KIND_SOURCE); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_AVAILABLE_LOCAL); + g_assert_cmpstr (gs_app_get_id (app), ==, "example"); + g_assert_cmpstr (gs_app_get_management_plugin (app), ==, "flatpak"); + g_assert_cmpstr (gs_app_get_origin_hostname (app), ==, "localhost"); + g_assert_cmpstr (gs_app_get_url (app, AS_URL_KIND_HOMEPAGE), ==, "http://foo.bar"); + g_assert_cmpstr (gs_app_get_name (app), ==, "foo-bar"); + g_assert_cmpstr (gs_app_get_summary (app), ==, "Longer one line comment"); + g_assert_cmpstr (gs_app_get_description (app), ==, + "Longer multiline comment that does into detail."); + g_assert_true (gs_app_get_local_file (app) != NULL); + g_assert_true (gs_app_get_pixbuf (app) != NULL); + + /* now install the remote */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_INSTALLED); + + /* check config file was updated */ + root = g_getenv ("GS_SELF_TEST_FLATPAK_DATADIR"); + config_fn = g_build_filename (root, "flatpak", "repo", "config", NULL); + kf = g_key_file_new (); + ret = g_key_file_load_from_file (kf, config_fn, 0, &error); + g_assert_no_error (error); + g_assert_true (ret); + + g_assert_true (g_key_file_has_group (kf, "core")); + g_assert_true (g_key_file_has_group (kf, group_name)); + g_assert_true (!g_key_file_get_boolean (kf, group_name, "gpg-verify", NULL)); + + /* check the URL was unmangled */ + remote_url = g_key_file_get_string (kf, group_name, "url", &error); + g_assert_no_error (error); + g_assert_cmpstr (remote_url, ==, testdir_repourl); + + /* try again, check state is correct */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP, + "file", file, + NULL); + app2 = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (app2 != NULL); + g_assert_cmpint (gs_app_get_state (app2), ==, AS_APP_STATE_INSTALLED); + + /* remove it */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_AVAILABLE); + g_assert_cmpint (gs_app_get_progress (app), ==, GS_APP_PROGRESS_UNKNOWN); +} + +static void +progress_notify_cb (GObject *obj, GParamSpec *pspec, gpointer user_data) +{ + gboolean *seen_unknown = user_data; + GsApp *app = GS_APP (obj); + + if (gs_app_get_progress (app) == GS_APP_PROGRESS_UNKNOWN) + *seen_unknown = TRUE; +} + +static void +gs_plugins_flatpak_app_with_runtime_func (GsPluginLoader *plugin_loader) +{ + GsApp *app; + GsApp *runtime; + const gchar *root; + gboolean ret; + gint kf_remote_repo_version; + g_autofree gchar *changed_fn = NULL; + g_autofree gchar *config_fn = NULL; + g_autofree gchar *desktop_fn = NULL; + g_autofree gchar *kf_remote_url = NULL; + g_autofree gchar *metadata_fn = NULL; + g_autofree gchar *repodir_fn = NULL; + g_autofree gchar *runtime_fn = NULL; + g_autofree gchar *testdir = NULL; + g_autofree gchar *testdir_repourl = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GKeyFile) kf1 = g_key_file_new (); + g_autoptr(GKeyFile) kf2 = g_key_file_new (); + g_autoptr(GsApp) app_source = NULL; + g_autoptr(GsAppList) list_all = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GsAppList) sources = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + gulong signal_id; + gboolean seen_unknown; + + /* drop all caches */ + gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL); + gs_plugin_loader_setup_again (plugin_loader); + + /* no flatpak, abort */ + if (!gs_plugin_loader_get_enabled (plugin_loader, "flatpak")) + return; + + /* no files to use */ + repodir_fn = gs_test_get_filename (TESTDATADIR, "app-with-runtime/repo"); + if (repodir_fn == NULL || + !g_file_test (repodir_fn, G_FILE_TEST_EXISTS)) { + g_test_skip ("no flatpak test repo"); + return; + } + + /* check changed file exists */ + root = g_getenv ("GS_SELF_TEST_FLATPAK_DATADIR"); + changed_fn = g_build_filename (root, "flatpak", ".changed", NULL); + g_assert_true (g_file_test (changed_fn, G_FILE_TEST_IS_REGULAR)); + + /* check repo is set up */ + config_fn = g_build_filename (root, "flatpak", "repo", "config", NULL); + ret = g_key_file_load_from_file (kf1, config_fn, G_KEY_FILE_NONE, &error); + g_assert_no_error (error); + g_assert_true (ret); + kf_remote_repo_version = g_key_file_get_integer (kf1, "core", "repo_version", &error); + g_assert_no_error (error); + g_assert_cmpint (kf_remote_repo_version, ==, 1); + + /* add a remote */ + app_source = gs_flatpak_app_new ("test"); + testdir = gs_test_get_filename (TESTDATADIR, "app-with-runtime"); + if (testdir == NULL) + return; + testdir_repourl = g_strdup_printf ("file://%s/repo", testdir); + gs_app_set_kind (app_source, AS_APP_KIND_SOURCE); + gs_app_set_management_plugin (app_source, "flatpak"); + gs_app_set_state (app_source, AS_APP_STATE_AVAILABLE); + gs_flatpak_app_set_repo_url (app_source, testdir_repourl); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", app_source, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app_source), ==, AS_APP_STATE_INSTALLED); + + /* check remote was set up */ + ret = g_key_file_load_from_file (kf2, config_fn, G_KEY_FILE_NONE, &error); + g_assert_no_error (error); + g_assert_true (ret); + kf_remote_url = g_key_file_get_string (kf2, "remote \"test\"", "url", &error); + g_assert_no_error (error); + g_assert_cmpstr (kf_remote_url, !=, NULL); + + /* check the source now exists */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_SOURCES, NULL); + sources = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (sources != NULL); + g_assert_cmpint (gs_app_list_length (sources), ==, 1); + app = gs_app_list_index (sources, 0); + g_assert_cmpstr (gs_app_get_id (app), ==, "test"); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_APP_KIND_SOURCE); + + /* refresh the appstream metadata */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFRESH, + "age", (guint64) G_MAXUINT, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* all the apps should have the flatpak keyword */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH, + "search", "flatpak", + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + NULL); + list_all = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (list_all != NULL); + g_assert_cmpint (gs_app_list_length (list_all), ==, 2); + + /* find available application */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH, + "search", "Bingo", + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + NULL); + list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (list != NULL); + + /* make sure there is one entry, the flatpak app */ + g_assert_cmpint (gs_app_list_length (list), ==, 1); + app = gs_app_list_index (list, 0); + g_assert_cmpstr (gs_app_get_id (app), ==, "org.test.Chiron"); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_APP_KIND_DESKTOP); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_AVAILABLE); + g_assert_cmpint ((gint64) gs_app_get_kudos (app), ==, + GS_APP_KUDO_MY_LANGUAGE | + GS_APP_KUDO_HAS_KEYWORDS | + GS_APP_KUDO_HI_DPI_ICON | + GS_APP_KUDO_SANDBOXED_SECURE | + GS_APP_KUDO_SANDBOXED); + g_assert_cmpstr (gs_app_get_origin_hostname (app), ==, "localhost"); + g_assert_cmpstr (gs_app_get_version (app), ==, "1.2.3"); + g_assert_cmpstr (gs_app_get_update_version (app), ==, NULL); + g_assert_cmpstr (gs_app_get_update_details (app), ==, NULL); + g_assert_cmpint (gs_app_get_update_urgency (app), ==, AS_URGENCY_KIND_UNKNOWN); + + /* check runtime */ + runtime = gs_app_get_runtime (app); + g_assert_true (runtime != NULL); + g_assert_cmpstr (gs_app_get_unique_id (runtime), ==, "user/flatpak/test/runtime/org.test.Runtime/master"); + g_assert_cmpint (gs_app_get_state (runtime), ==, AS_APP_STATE_AVAILABLE); + + /* install, also installing runtime */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_INSTALLED); + g_assert_cmpstr (gs_app_get_version (app), ==, "1.2.3"); + g_assert_cmpint (gs_app_get_progress (app), ==, GS_APP_PROGRESS_UNKNOWN); + g_assert_cmpint (gs_app_get_state (runtime), ==, AS_APP_STATE_INSTALLED); + + /* check the application exists in the right places */ + metadata_fn = g_build_filename (root, + "flatpak", + "app", + "org.test.Chiron", + "current", + "active", + "metadata", + NULL); + g_assert_true (g_file_test (metadata_fn, G_FILE_TEST_IS_REGULAR)); + desktop_fn = g_build_filename (root, + "flatpak", + "app", + "org.test.Chiron", + "current", + "active", + "export", + "share", + "applications", + "org.test.Chiron.desktop", + NULL); + g_assert_true (g_file_test (desktop_fn, G_FILE_TEST_IS_REGULAR)); + + /* check the runtime was installed as well */ + runtime_fn = g_build_filename (root, + "flatpak", + "runtime", + "org.test.Runtime", + "x86_64", + "master", + "active", + "files", + "share", + "libtest", + "README", + NULL); + g_assert_true (g_file_test (runtime_fn, G_FILE_TEST_IS_REGULAR)); + + /* remove the application */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_AVAILABLE); + g_assert_cmpint (gs_app_get_state (runtime), ==, AS_APP_STATE_INSTALLED); + g_assert_true (!g_file_test (metadata_fn, G_FILE_TEST_IS_REGULAR)); + g_assert_true (!g_file_test (desktop_fn, G_FILE_TEST_IS_REGULAR)); + + /* install again, to check whether the progress gets initialized; + * since installation happens in another thread, we have to monitor all + * changes to the progress and see if we see the one we want */ + seen_unknown = (gs_app_get_progress (app) == GS_APP_PROGRESS_UNKNOWN); + signal_id = g_signal_connect (app, "notify::progress", + G_CALLBACK (progress_notify_cb), &seen_unknown); + + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + + /* progress should be set to unknown right before installing */ + while (!seen_unknown) + g_main_context_iteration (NULL, TRUE); + g_assert_true (seen_unknown); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_INSTALLED); + g_assert_cmpstr (gs_app_get_version (app), ==, "1.2.3"); + g_assert_cmpint (gs_app_get_progress (app), ==, GS_APP_PROGRESS_UNKNOWN); + g_signal_handler_disconnect (app, signal_id); + + /* remove the application */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_AVAILABLE); + g_assert_cmpint (gs_app_get_state (runtime), ==, AS_APP_STATE_INSTALLED); + g_assert_true (!g_file_test (metadata_fn, G_FILE_TEST_IS_REGULAR)); + g_assert_true (!g_file_test (desktop_fn, G_FILE_TEST_IS_REGULAR)); + + /* remove the remote (fail, as the runtime is still installed) */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", app_source, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_error (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED); + g_assert_true (!ret); + g_clear_error (&error); + g_assert_cmpint (gs_app_get_state (app_source), ==, AS_APP_STATE_INSTALLED); + + /* remove the runtime */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", runtime, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (runtime), ==, AS_APP_STATE_AVAILABLE); + + /* remove the remote */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", app_source, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app_source), ==, AS_APP_STATE_AVAILABLE); +} + +static void +gs_plugins_flatpak_app_missing_runtime_func (GsPluginLoader *plugin_loader) +{ + GsApp *app; + gboolean ret; + g_autofree gchar *repodir_fn = NULL; + g_autofree gchar *testdir = NULL; + g_autofree gchar *testdir_repourl = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsApp) app_source = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* drop all caches */ + gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL); + gs_plugin_loader_setup_again (plugin_loader); + + /* no flatpak, abort */ + if (!gs_plugin_loader_get_enabled (plugin_loader, "flatpak")) + return; + + /* no files to use */ + repodir_fn = gs_test_get_filename (TESTDATADIR, "app-missing-runtime/repo"); + if (repodir_fn == NULL || + !g_file_test (repodir_fn, G_FILE_TEST_EXISTS)) { + g_test_skip ("no flatpak test repo"); + return; + } + + /* add a remote */ + app_source = gs_flatpak_app_new ("test"); + testdir = gs_test_get_filename (TESTDATADIR, "app-missing-runtime"); + if (testdir == NULL) + return; + testdir_repourl = g_strdup_printf ("file://%s/repo", testdir); + gs_app_set_kind (app_source, AS_APP_KIND_SOURCE); + gs_app_set_management_plugin (app_source, "flatpak"); + gs_app_set_state (app_source, AS_APP_STATE_AVAILABLE); + gs_flatpak_app_set_repo_url (app_source, testdir_repourl); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", app_source, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app_source), ==, AS_APP_STATE_INSTALLED); + + /* refresh the appstream metadata */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFRESH, + "age", (guint64) G_MAXUINT, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* find available application */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH, + "search", "Bingo", + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + NULL); + list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (list != NULL); + + /* make sure there is one entry, the flatpak app */ + g_assert_cmpint (gs_app_list_length (list), ==, 1); + app = gs_app_list_index (list, 0); + g_assert_cmpstr (gs_app_get_id (app), ==, "org.test.Chiron"); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_AVAILABLE); + + /* install, also installing runtime */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_error (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NOT_SUPPORTED); + g_assert_true (!ret); + g_clear_error (&error); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_AVAILABLE); + g_assert_cmpint (gs_app_get_progress (app), ==, GS_APP_PROGRESS_UNKNOWN); + + /* remove the remote */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", app_source, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app_source), ==, AS_APP_STATE_AVAILABLE); +} + +static void +update_app_progress_notify_cb (GsApp *app, GParamSpec *pspec, gpointer user_data) +{ + g_debug ("progress now %u%%", gs_app_get_progress (app)); + if (user_data != NULL) { + guint *tmp = (guint *) user_data; + (*tmp)++; + } +} + +static void +update_app_state_notify_cb (GsApp *app, GParamSpec *pspec, gpointer user_data) +{ + AsAppState state = gs_app_get_state (app); + g_debug ("state now %s", as_app_state_to_string (state)); + if (state == AS_APP_STATE_INSTALLING) { + gboolean *tmp = (gboolean *) user_data; + *tmp = TRUE; + } +} + +static gboolean +update_app_action_delay_cb (gpointer user_data) +{ + GMainLoop *loop = (GMainLoop *) user_data; + g_main_loop_quit (loop); + return FALSE; +} + +static void +update_app_action_finish_sync (GObject *source, GAsyncResult *res, gpointer user_data) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source); + gboolean ret; + g_autoptr(GError) error = NULL; + ret = gs_plugin_loader_job_action_finish (plugin_loader, res, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_timeout_add_seconds (5, update_app_action_delay_cb, user_data); +} + +static void +gs_plugins_flatpak_runtime_repo_func (GsPluginLoader *plugin_loader) +{ + GsApp *app_source; + GsApp *runtime; + const gchar *fn_ref = "test.flatpakref"; + const gchar *fn_repo = "test.flatpakrepo"; + gboolean ret; + g_autoptr(GFile) fn_repo_file = NULL; + g_autofree gchar *fn_repourl = NULL; + g_autofree gchar *testdir2 = NULL; + g_autofree gchar *testdir2_repourl = NULL; + g_autofree gchar *testdir = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GMainLoop) loop = g_main_loop_new (NULL, FALSE); + g_autoptr(GsApp) app = NULL; + g_autoptr(GsAppList) sources2 = NULL; + g_autoptr(GsAppList) sources = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* drop all caches */ + gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL); + gs_plugin_loader_setup_again (plugin_loader); + + /* write a flatpakrepo file */ + testdir = gs_test_get_filename (TESTDATADIR, "only-runtime"); + if (testdir == NULL) + return; + ret = gs_flatpak_test_write_repo_file (fn_repo, testdir, &fn_repo_file, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* write a flatpakref file */ + fn_repourl = g_file_get_uri (fn_repo_file); + testdir2 = gs_test_get_filename (TESTDATADIR, "app-missing-runtime"); + if (testdir2 == NULL) + return; + testdir2_repourl = g_strdup_printf ("file://%s/repo", testdir2); + ret = gs_flatpak_test_write_ref_file (fn_ref, testdir2_repourl, fn_repourl, &file, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* convert it to a GsApp */ + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP, + "file", file, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME, + NULL); + app = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (app != NULL); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_APP_KIND_DESKTOP); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_AVAILABLE_LOCAL); + g_assert_cmpstr (gs_app_get_id (app), ==, "org.test.Chiron"); + g_assert_true (as_utils_unique_id_equal (gs_app_get_unique_id (app), + "user/flatpak/*/desktop/org.test.Chiron/master")); + g_assert_true (gs_app_get_local_file (app) != NULL); + + /* get runtime */ + runtime = gs_app_get_runtime (app); + g_assert_cmpstr (gs_app_get_unique_id (runtime), ==, "user/flatpak/*/runtime/org.test.Runtime/master"); + g_assert_cmpint (gs_app_get_state (runtime), ==, AS_APP_STATE_AVAILABLE_LOCAL); + + /* check the number of sources */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_SOURCES, NULL); + sources = gs_plugin_loader_job_process (plugin_loader, plugin_job, + NULL, &error); + g_assert_no_error (error); + g_assert_true (sources != NULL); + g_assert_cmpint (gs_app_list_length (sources), ==, 0); + + /* install, which will install the runtime from the new remote */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", app, + NULL); + gs_plugin_loader_job_process_async (plugin_loader, plugin_job, + NULL, + update_app_action_finish_sync, + loop); + g_main_loop_run (loop); + gs_test_flush_main_context (); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_INSTALLED); + g_assert_cmpint (gs_app_get_state (runtime), ==, AS_APP_STATE_INSTALLED); + + /* check the number of sources */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_SOURCES, NULL); + sources2 = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (sources2 != NULL); + g_assert_cmpint (gs_app_list_length (sources2), ==, 1); + + /* remove the app */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_UNKNOWN); + + /* remove the runtime */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", runtime, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (runtime), ==, AS_APP_STATE_AVAILABLE); + + /* remove the remote */ + app_source = gs_app_list_index (sources2, 0); + g_assert_true (app_source != NULL); + g_assert_cmpstr (gs_app_get_unique_id (app_source), ==, "user/flatpak/*/source/test/*"); + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", app_source, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app_source), ==, AS_APP_STATE_AVAILABLE); +} + +/* same as gs_plugins_flatpak_runtime_repo_func, but this time manually + * installing the flatpakrepo BEFORE the flatpakref is installed */ +static void +gs_plugins_flatpak_runtime_repo_redundant_func (GsPluginLoader *plugin_loader) +{ + GsApp *app_source; + GsApp *runtime; + const gchar *fn_ref = "test.flatpakref"; + const gchar *fn_repo = "test.flatpakrepo"; + gboolean ret; + g_autofree gchar *fn_repourl = NULL; + g_autofree gchar *testdir2 = NULL; + g_autofree gchar *testdir2_repourl = NULL; + g_autofree gchar *testdir = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GFile) file_repo = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsApp) app_src = NULL; + g_autoptr(GsAppList) sources2 = NULL; + g_autoptr(GsAppList) sources = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* drop all caches */ + gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL); + gs_plugin_loader_setup_again (plugin_loader); + + /* write a flatpakrepo file */ + testdir = gs_test_get_filename (TESTDATADIR, "only-runtime"); + if (testdir == NULL) + return; + ret = gs_flatpak_test_write_repo_file (fn_repo, testdir, &file_repo, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* convert it to a GsApp */ + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP, + "file", file_repo, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME, + NULL); + app_src = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (app_src != NULL); + g_assert_cmpint (gs_app_get_kind (app_src), ==, AS_APP_KIND_SOURCE); + g_assert_cmpint (gs_app_get_state (app_src), ==, AS_APP_STATE_AVAILABLE_LOCAL); + g_assert_cmpstr (gs_app_get_id (app_src), ==, "test"); + g_assert_cmpstr (gs_app_get_unique_id (app_src), ==, "*/*/*/source/test/master"); + g_assert_true (gs_app_get_local_file (app_src) != NULL); + + /* install the source manually */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", app_src, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app_src), ==, AS_APP_STATE_INSTALLED); + + /* write a flatpakref file */ + fn_repourl = g_file_get_uri (file_repo); + testdir2 = gs_test_get_filename (TESTDATADIR, "app-missing-runtime"); + if (testdir2 == NULL) + return; + testdir2_repourl = g_strdup_printf ("file://%s/repo", testdir2); + ret = gs_flatpak_test_write_ref_file (fn_ref, testdir2_repourl, fn_repourl, &file, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* convert it to a GsApp */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP, + "file", file, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME, + NULL); + app = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (app != NULL); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_APP_KIND_DESKTOP); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_AVAILABLE_LOCAL); + g_assert_cmpstr (gs_app_get_id (app), ==, "org.test.Chiron"); + g_assert_true (as_utils_unique_id_equal (gs_app_get_unique_id (app), + "user/flatpak/*/desktop/org.test.Chiron/master")); + g_assert_true (gs_app_get_local_file (app) != NULL); + + /* get runtime */ + runtime = gs_app_get_runtime (app); + g_assert_cmpstr (gs_app_get_unique_id (runtime), ==, "user/flatpak/test/runtime/org.test.Runtime/master"); + g_assert_cmpint (gs_app_get_state (runtime), ==, AS_APP_STATE_AVAILABLE); + + /* check the number of sources */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_SOURCES, NULL); + sources = gs_plugin_loader_job_process (plugin_loader, plugin_job, + NULL, &error); + g_assert_no_error (error); + g_assert_true (sources != NULL); + g_assert_cmpint (gs_app_list_length (sources), ==, 1); /* repo */ + + /* install, which will NOT install the runtime from the RuntimeRemote, + * but from the existing test repo */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_INSTALLED); + g_assert_cmpint (gs_app_get_state (runtime), ==, AS_APP_STATE_INSTALLED); + + /* check the number of sources */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_SOURCES, NULL); + sources2 = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (sources2 != NULL); + + /* remove the app */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_UNKNOWN); + + /* remove the runtime */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", runtime, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (runtime), ==, AS_APP_STATE_AVAILABLE); + + /* remove the remote */ + app_source = gs_app_list_index (sources2, 0); + g_assert_true (app_source != NULL); + g_assert_cmpstr (gs_app_get_unique_id (app_source), ==, "user/flatpak/*/source/test/*"); + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", app_source, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app_source), ==, AS_APP_STATE_AVAILABLE); +} + +static void +gs_plugins_flatpak_broken_remote_func (GsPluginLoader *plugin_loader) +{ + gboolean ret; + const gchar *fn = "test.flatpakref"; + const gchar *fn_repo = "test.flatpakrepo"; + g_autoptr(GFile) fn_repo_file = NULL; + g_autofree gchar *fn_repourl = NULL; + g_autofree gchar *testdir2 = NULL; + g_autofree gchar *testdir2_repourl = NULL; + g_autofree gchar *testdir = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsApp) app_source = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* drop all caches */ + gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL); + gs_plugin_loader_setup_again (plugin_loader); + + /* no flatpak, abort */ + if (!gs_plugin_loader_get_enabled (plugin_loader, "flatpak")) + return; + + /* add a remote with only the runtime in */ + app_source = gs_flatpak_app_new ("test"); + testdir = gs_test_get_filename (TESTDATADIR, "only-runtime"); + if (testdir == NULL) + return; + gs_app_set_kind (app_source, AS_APP_KIND_SOURCE); + gs_app_set_management_plugin (app_source, "flatpak"); + gs_app_set_state (app_source, AS_APP_STATE_AVAILABLE); + gs_flatpak_app_set_repo_url (app_source, "file:///wont/work"); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", app_source, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app_source), ==, AS_APP_STATE_INSTALLED); + + /* write a flatpakrepo file (the flatpakref below must have a RuntimeRepo= + * to avoid a warning) */ + testdir2 = gs_test_get_filename (TESTDATADIR, "app-with-runtime"); + if (testdir2 == NULL) + return; + ret = gs_flatpak_test_write_repo_file (fn_repo, testdir2, &fn_repo_file, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* write a flatpakref file */ + fn_repourl = g_file_get_uri (fn_repo_file); + testdir2_repourl = g_strdup_printf ("file://%s/repo", testdir2); + ret = gs_flatpak_test_write_ref_file (fn, testdir2_repourl, fn_repourl, &file, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* convert it to a GsApp */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP, + "file", file, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION, + NULL); + app = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (app != NULL); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_APP_KIND_DESKTOP); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_AVAILABLE_LOCAL); + g_assert_cmpstr (gs_app_get_id (app), ==, "org.test.Chiron"); +#if FLATPAK_CHECK_VERSION(1,1,2) + g_assert_true (as_utils_unique_id_equal (gs_app_get_unique_id (app), + "user/flatpak/chiron-origin/desktop/org.test.Chiron/master")); +#else + g_assert_true (as_utils_unique_id_equal (gs_app_get_unique_id (app), + "user/flatpak/org.test.Chiron-origin/desktop/org.test.Chiron/master")); +#endif + g_assert_cmpstr (gs_app_get_url (app, AS_URL_KIND_HOMEPAGE), ==, "http://127.0.0.1/"); + g_assert_cmpstr (gs_app_get_name (app), ==, "Chiron"); + g_assert_cmpstr (gs_app_get_summary (app), ==, "Single line synopsis"); + g_assert_cmpstr (gs_app_get_description (app), ==, "Long description."); + g_assert_cmpstr (gs_app_get_version (app), ==, "1.2.3"); + g_assert_true (gs_app_get_local_file (app) != NULL); + + /* remove source */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", app_source, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); +} + +static void +flatpak_bundle_or_ref_helper (GsPluginLoader *plugin_loader, + gboolean is_bundle) +{ + GsApp *app_tmp; + GsApp *runtime; + gboolean ret; + GsPluginRefineFlags refine_flags; + g_autofree gchar *fn = NULL; + g_autofree gchar *testdir = NULL; + g_autofree gchar *testdir_repourl = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsApp) app2 = NULL; + g_autoptr(GsApp) app_source = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GsAppList) search1 = NULL; + g_autoptr(GsAppList) search2 = NULL; + g_autoptr(GsAppList) sources = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* drop all caches */ + gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL); + gs_plugin_loader_setup_again (plugin_loader); + + /* no flatpak, abort */ + if (!gs_plugin_loader_get_enabled (plugin_loader, "flatpak")) + return; + + /* add a remote with only the runtime in */ + app_source = gs_flatpak_app_new ("test"); + testdir = gs_test_get_filename (TESTDATADIR, "only-runtime"); + if (testdir == NULL) + return; + testdir_repourl = g_strdup_printf ("file://%s/repo", testdir); + gs_app_set_kind (app_source, AS_APP_KIND_SOURCE); + gs_app_set_management_plugin (app_source, "flatpak"); + gs_app_set_state (app_source, AS_APP_STATE_AVAILABLE); + gs_flatpak_app_set_repo_url (app_source, testdir_repourl); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", app_source, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app_source), ==, AS_APP_STATE_INSTALLED); + + /* refresh the appstream metadata */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFRESH, + "age", (guint64) 0, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* find available application */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH, + "search", "runtime", + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + NULL); + list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (list != NULL); + + /* make sure there is one entry, the flatpak runtime */ + g_assert_cmpint (gs_app_list_length (list), ==, 1); + runtime = gs_app_list_index (list, 0); + g_assert_cmpstr (gs_app_get_id (runtime), ==, "org.test.Runtime"); + g_assert_cmpstr (gs_app_get_unique_id (runtime), ==, "user/flatpak/test/runtime/org.test.Runtime/master"); + g_assert_cmpint (gs_app_get_state (runtime), ==, AS_APP_STATE_AVAILABLE); + + /* install the runtime ahead of time */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", runtime, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (runtime), ==, AS_APP_STATE_INSTALLED); + + if (is_bundle) { + /* find the flatpak bundle file */ + fn = gs_test_get_filename (TESTDATADIR, "chiron.flatpak"); + g_assert_true (fn != NULL); + file = g_file_new_for_path (fn); + refine_flags = GS_PLUGIN_REFINE_FLAGS_DEFAULT; + } else { + const gchar *fn_repo = "test.flatpakrepo"; + g_autoptr(GFile) fn_repo_file = NULL; + g_autofree gchar *fn_repourl = NULL; + g_autofree gchar *testdir2 = NULL; + g_autofree gchar *testdir2_repourl = NULL; + + /* write a flatpakrepo file (the flatpakref below must have a RuntimeRepo= + * to avoid a warning) */ + testdir2 = gs_test_get_filename (TESTDATADIR, "app-with-runtime"); + if (testdir2 == NULL) + return; + ret = gs_flatpak_test_write_repo_file (fn_repo, testdir2, &fn_repo_file, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* write a flatpakref file */ + fn_repourl = g_file_get_uri (fn_repo_file); + testdir2_repourl = g_strdup_printf ("file://%s/repo", testdir2); + fn = g_strdup ("test.flatpakref"); + ret = gs_flatpak_test_write_ref_file (fn, testdir2_repourl, fn_repourl, &file, &error); + g_assert_no_error (error); + g_assert_true (ret); + + refine_flags = GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME; + } + + /* convert it to a GsApp */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP, + "file", file, + "refine-flags", refine_flags, + NULL); + app = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (app != NULL); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_APP_KIND_DESKTOP); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_AVAILABLE_LOCAL); + g_assert_cmpstr (gs_app_get_id (app), ==, "org.test.Chiron"); + g_assert_cmpstr (gs_app_get_name (app), ==, "Chiron"); + g_assert_cmpstr (gs_app_get_summary (app), ==, "Single line synopsis"); + g_assert_cmpstr (gs_app_get_version (app), ==, "1.2.3"); + g_assert_true (gs_app_get_local_file (app) != NULL); + if (is_bundle) { + /* Note: The origin is set to "flatpak" here because an origin remote + * won't be created until the app is installed. + */ + g_assert_true (as_utils_unique_id_equal (gs_app_get_unique_id (app), + "user/flatpak/flatpak/desktop/org.test.Chiron/master")); + g_assert_true (gs_flatpak_app_get_file_kind (app) == GS_FLATPAK_APP_FILE_KIND_BUNDLE); + } else { +#if FLATPAK_CHECK_VERSION(1,1,2) + g_assert_true (as_utils_unique_id_equal (gs_app_get_unique_id (app), + "user/flatpak/chiron-origin/desktop/org.test.Chiron/master")); +#else + g_assert_true (as_utils_unique_id_equal (gs_app_get_unique_id (app), + "user/flatpak/org.test.Chiron-origin/desktop/org.test.Chiron/master")); +#endif + g_assert_true (gs_flatpak_app_get_file_kind (app) == GS_FLATPAK_APP_FILE_KIND_REF); + g_assert_cmpstr (gs_app_get_url (app, AS_URL_KIND_HOMEPAGE), ==, "http://127.0.0.1/"); + g_assert_cmpstr (gs_app_get_description (app), ==, "Long description."); + } + + /* get runtime */ + runtime = gs_app_get_runtime (app); + g_assert_cmpstr (gs_app_get_unique_id (runtime), ==, "user/flatpak/test/runtime/org.test.Runtime/master"); + g_assert_cmpint (gs_app_get_state (runtime), ==, AS_APP_STATE_INSTALLED); + + /* install */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_INSTALLED); + g_assert_cmpstr (gs_app_get_version (app), ==, "1.2.3"); + g_assert_cmpstr (gs_app_get_update_version (app), ==, NULL); + g_assert_cmpstr (gs_app_get_update_details (app), ==, NULL); + + /* search for the application */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH, + "search", "chiron", + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + NULL); + search1 = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (search1 != NULL); + g_assert_cmpint (gs_app_list_length (search1), ==, 1); + app_tmp = gs_app_list_index (search1, 0); + g_assert_cmpstr (gs_app_get_id (app_tmp), ==, "org.test.Chiron"); + + /* convert it to a GsApp again, and get the installed thing */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP, + "file", file, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME, + NULL); + app2 = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (app2 != NULL); + g_assert_cmpint (gs_app_get_state (app2), ==, AS_APP_STATE_INSTALLED); + if (is_bundle) { +#if FLATPAK_CHECK_VERSION(1,1,2) + g_assert_true (as_utils_unique_id_equal (gs_app_get_unique_id (app2), + "user/flatpak/chiron-origin/desktop/org.test.Chiron/master")); +#else + g_assert_true (as_utils_unique_id_equal (gs_app_get_unique_id (app2), + "user/flatpak/org.test.Chiron-origin/desktop/org.test.Chiron/master")); +#endif + } else { + /* Note: the origin is now test-1 because that remote was created from the + * RuntimeRepo= setting + */ + g_assert_true (as_utils_unique_id_equal (gs_app_get_unique_id (app2), + "user/flatpak/test-1/desktop/org.test.Chiron/master")); + } + + /* remove app */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", app2, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* remove runtime */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", runtime, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* remove source */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", app_source, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + + if (!is_bundle) { + /* remove remote added by RuntimeRepo= in flatpakref */ + g_autoptr(GsApp) runtime_source = gs_flatpak_app_new ("test-1"); + gs_app_set_kind (runtime_source, AS_APP_KIND_SOURCE); + gs_app_set_management_plugin (runtime_source, "flatpak"); + gs_app_set_state (runtime_source, AS_APP_STATE_INSTALLED); + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", runtime_source, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + } + + /* there should be no sources now */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_SOURCES, NULL); + sources = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (sources != NULL); + g_assert_cmpint (gs_app_list_length (sources), ==, 0); + + /* there should be no matches now */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH, + "search", "chiron", + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + NULL); + search2 = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (search2 != NULL); + g_assert_cmpint (gs_app_list_length (search2), ==, 0); +} + +static void +gs_plugins_flatpak_ref_func (GsPluginLoader *plugin_loader) +{ + flatpak_bundle_or_ref_helper (plugin_loader, FALSE); +} + +static void +gs_plugins_flatpak_bundle_func (GsPluginLoader *plugin_loader) +{ + flatpak_bundle_or_ref_helper (plugin_loader, TRUE); +} + +static void +gs_plugins_flatpak_count_signal_cb (GsPluginLoader *plugin_loader, guint *cnt) +{ + if (cnt != NULL) + (*cnt)++; +} + +static void +gs_plugins_flatpak_app_update_func (GsPluginLoader *plugin_loader) +{ + GsApp *app; + GsApp *app_tmp; + GsApp *runtime; + GsApp *old_runtime; + gboolean got_progress_installing = FALSE; + gboolean ret; + guint notify_progress_id; + guint notify_state_id; + guint pending_app_changed_cnt = 0; + guint pending_apps_changed_id; + guint progress_cnt = 0; + guint updates_changed_cnt = 0; + guint updates_changed_id; + g_autofree gchar *repodir1_fn = NULL; + g_autofree gchar *repodir2_fn = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsApp) app_source = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GsAppList) list_updates = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GMainLoop) loop = g_main_loop_new (NULL, FALSE); + g_autofree gchar *repo_path = NULL; + g_autofree gchar *repo_url = NULL; + + /* drop all caches */ + gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL); + gs_plugin_loader_setup_again (plugin_loader); + + /* no flatpak, abort */ + if (!gs_plugin_loader_get_enabled (plugin_loader, "flatpak")) + return; + + /* no files to use */ + repodir1_fn = gs_test_get_filename (TESTDATADIR, "app-with-runtime/repo"); + if (repodir1_fn == NULL || + !g_file_test (repodir1_fn, G_FILE_TEST_EXISTS)) { + g_test_skip ("no flatpak test repo"); + return; + } + repodir2_fn = gs_test_get_filename (TESTDATADIR, "app-update/repo"); + if (repodir2_fn == NULL || + !g_file_test (repodir2_fn, G_FILE_TEST_EXISTS)) { + g_test_skip ("no flatpak test repo"); + return; + } + + /* add indirection so we can switch this after install */ + repo_path = g_build_filename (g_getenv ("GS_SELF_TEST_FLATPAK_DATADIR"), "repo", NULL); + unlink (repo_path); + g_assert_true (symlink (repodir1_fn, repo_path) == 0); + + /* add a remote */ + app_source = gs_flatpak_app_new ("test"); + gs_app_set_kind (app_source, AS_APP_KIND_SOURCE); + gs_app_set_management_plugin (app_source, "flatpak"); + gs_app_set_state (app_source, AS_APP_STATE_AVAILABLE); + repo_url = g_strdup_printf ("file://%s", repo_path); + gs_flatpak_app_set_repo_url (app_source, repo_url); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", app_source, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app_source), ==, AS_APP_STATE_INSTALLED); + + /* refresh the appstream metadata */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFRESH, + "age", (guint64) G_MAXUINT, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + + /* find available application */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH, + "search", "Bingo", + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME, + NULL); + list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (list != NULL); + + /* make sure there is one entry, the flatpak app */ + g_assert_cmpint (gs_app_list_length (list), ==, 1); + app = gs_app_list_index (list, 0); + g_assert_cmpstr (gs_app_get_id (app), ==, "org.test.Chiron"); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_AVAILABLE); + + /* install, also installing runtime */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_INSTALLED); + g_assert_cmpstr (gs_app_get_version (app), ==, "1.2.3"); + g_assert_cmpstr (gs_app_get_update_version (app), ==, NULL); + g_assert_cmpstr (gs_app_get_update_details (app), ==, NULL); + + /* switch to the new repo */ + g_assert_true (unlink (repo_path) == 0); + g_assert_true (symlink (repodir2_fn, repo_path) == 0); + + /* refresh the appstream metadata */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFRESH, + "age", (guint64) 0, /* force now */ + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* get the updates list */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_UPDATES, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS, + NULL); + list_updates = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (list_updates != NULL); + + /* make sure there are two entries */ + g_assert_cmpint (gs_app_list_length (list_updates), ==, 1); + for (guint i = 0; i < gs_app_list_length (list_updates); i++) { + app_tmp = gs_app_list_index (list_updates, i); + g_debug ("got update %s", gs_app_get_unique_id (app_tmp)); + } + + /* check they are the same GObject */ + app_tmp = gs_app_list_lookup (list_updates, "*/flatpak/test/*/org.test.Chiron/*"); + g_assert_true (app_tmp == app); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_UPDATABLE_LIVE); + g_assert_cmpstr (gs_app_get_update_details (app), ==, "Version 1.2.4:\nThis is best.\n\nVersion 1.2.3:\nThis is better."); + g_assert_cmpstr (gs_app_get_update_version (app), ==, "1.2.4"); + + /* care about signals */ + pending_apps_changed_id = + g_signal_connect (plugin_loader, "pending-apps-changed", + G_CALLBACK (gs_plugins_flatpak_count_signal_cb), + &pending_app_changed_cnt); + updates_changed_id = + g_signal_connect (plugin_loader, "updates-changed", + G_CALLBACK (gs_plugins_flatpak_count_signal_cb), + &updates_changed_cnt); + notify_state_id = + g_signal_connect (app, "notify::state", + G_CALLBACK (update_app_state_notify_cb), + &got_progress_installing); + notify_progress_id = + g_signal_connect (app, "notify::progress", + G_CALLBACK (update_app_progress_notify_cb), + &progress_cnt); + + /* check that the runtime is not the update's one */ + old_runtime = gs_app_get_runtime (app); + g_assert_true (old_runtime != NULL); + g_assert_cmpstr (gs_app_get_branch (old_runtime), !=, "new_master"); + + /* use a mainloop so we get the events in the default context */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPDATE, + "app", app, + NULL); + gs_plugin_loader_job_process_async (plugin_loader, plugin_job, + NULL, + update_app_action_finish_sync, + loop); + g_main_loop_run (loop); + gs_test_flush_main_context (); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_INSTALLED); + g_assert_cmpstr (gs_app_get_version (app), ==, "1.2.4"); + g_assert_cmpstr (gs_app_get_update_version (app), ==, NULL); + g_assert_cmpstr (gs_app_get_update_details (app), ==, NULL); + g_assert_cmpint (gs_app_get_progress (app), ==, GS_APP_PROGRESS_UNKNOWN); + g_assert_true (got_progress_installing); + //g_assert_cmpint (progress_cnt, >, 20); //FIXME: bug in OSTree + g_assert_cmpint (pending_app_changed_cnt, ==, 0); + g_assert_cmpint (updates_changed_cnt, ==, 1); + + /* check that the app's runtime has changed */ + runtime = gs_app_get_runtime (app); + g_assert_true (runtime != NULL); + g_assert_cmpstr (gs_app_get_unique_id (runtime), ==, "user/flatpak/test/runtime/org.test.Runtime/new_master"); + g_assert_true (old_runtime != runtime); + g_assert_cmpstr (gs_app_get_branch (runtime), ==, "new_master"); + g_assert_true (gs_app_get_state (runtime) == AS_APP_STATE_INSTALLED); + + /* no longer care */ + g_signal_handler_disconnect (plugin_loader, pending_apps_changed_id); + g_signal_handler_disconnect (plugin_loader, updates_changed_id); + g_signal_handler_disconnect (app, notify_state_id); + g_signal_handler_disconnect (app, notify_progress_id); + + /* remove the app */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* remove the old_runtime */ + g_assert_cmpstr (gs_app_get_unique_id (old_runtime), ==, "user/flatpak/test/runtime/org.test.Runtime/master"); + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", old_runtime, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + + /* remove the runtime */ + g_assert_cmpstr (gs_app_get_unique_id (runtime), ==, "user/flatpak/test/runtime/org.test.Runtime/new_master"); + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", runtime, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + + /* remove the remote */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", app_source, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app_source), ==, AS_APP_STATE_AVAILABLE); +} + +static void +gs_plugins_flatpak_runtime_extension_func (GsPluginLoader *plugin_loader) +{ + GsApp *app; + GsApp *runtime; + GsApp *app_tmp; + gboolean got_progress_installing = FALSE; + gboolean ret; + guint notify_progress_id; + guint notify_state_id; + guint pending_app_changed_cnt = 0; + guint pending_apps_changed_id; + guint progress_cnt = 0; + guint updates_changed_cnt = 0; + guint updates_changed_id; + g_autofree gchar *repodir1_fn = NULL; + g_autofree gchar *repodir2_fn = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsApp) app_source = NULL; + g_autoptr(GsApp) extension = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GsAppList) list_updates = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GMainLoop) loop = g_main_loop_new (NULL, FALSE); + g_autofree gchar *repo_path = NULL; + g_autofree gchar *repo_url = NULL; + + /* drop all caches */ + gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL); + gs_plugin_loader_setup_again (plugin_loader); + + /* no flatpak, abort */ + g_assert_true (gs_plugin_loader_get_enabled (plugin_loader, "flatpak")); + + /* no files to use */ + repodir1_fn = gs_test_get_filename (TESTDATADIR, "app-extension/repo"); + if (repodir1_fn == NULL || + !g_file_test (repodir1_fn, G_FILE_TEST_EXISTS)) { + g_test_skip ("no flatpak test repo"); + return; + } + repodir2_fn = gs_test_get_filename (TESTDATADIR, "app-extension-update/repo"); + if (repodir2_fn == NULL || + !g_file_test (repodir2_fn, G_FILE_TEST_EXISTS)) { + g_test_skip ("no flatpak test repo"); + return; + } + + /* add indirection so we can switch this after install */ + repo_path = g_build_filename (g_getenv ("GS_SELF_TEST_FLATPAK_DATADIR"), "repo", NULL); + g_assert_cmpint (symlink (repodir1_fn, repo_path), ==, 0); + + /* add a remote */ + app_source = gs_flatpak_app_new ("test"); + gs_app_set_kind (app_source, AS_APP_KIND_SOURCE); + gs_app_set_management_plugin (app_source, "flatpak"); + gs_app_set_state (app_source, AS_APP_STATE_AVAILABLE); + repo_url = g_strdup_printf ("file://%s", repo_path); + gs_flatpak_app_set_repo_url (app_source, repo_url); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", app_source, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app_source), ==, AS_APP_STATE_INSTALLED); + + /* refresh the appstream metadata */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFRESH, + "age", (guint64) G_MAXUINT, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + + /* find available application */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH, + "search", "Bingo", + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + NULL); + list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_nonnull (list); + + /* make sure there is one entry, the flatpak app */ + g_assert_cmpint (gs_app_list_length (list), ==, 1); + app = gs_app_list_index (list, 0); + g_assert_cmpstr (gs_app_get_id (app), ==, "org.test.Chiron"); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_AVAILABLE); + + /* install, also installing runtime and suggested extensions */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_INSTALLED); + g_assert_cmpstr (gs_app_get_version (app), ==, "1.2.3"); + + /* check if the extension was installed */ + extension = gs_plugin_loader_app_create (plugin_loader, + "user/flatpak/*/runtime/org.test.Chiron.Extension/master"); + g_assert_nonnull (extension); + g_assert_cmpint (gs_app_get_state (extension), ==, AS_APP_STATE_INSTALLED); + + /* switch to the new repo (to get the update) */ + g_assert_cmpint (unlink (repo_path), ==, 0); + g_assert_cmpint (symlink (repodir2_fn, repo_path), ==, 0); + + /* refresh the appstream metadata */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFRESH, + "age", (guint64) 0, /* force now */ + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* get the updates list */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_UPDATES, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS, + NULL); + list_updates = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_nonnull (list_updates); + + g_assert_cmpint (gs_app_list_length (list_updates), ==, 1); + for (guint i = 0; i < gs_app_list_length (list_updates); i++) { + app_tmp = gs_app_list_index (list_updates, i); + g_debug ("got update %s", gs_app_get_unique_id (app_tmp)); + } + + /* check that the extension has no update */ + app_tmp = gs_app_list_lookup (list_updates, "*/flatpak/test/*/org.test.Chiron.Extension/*"); + g_assert_null (app_tmp); + + /* check that the app has an update (it's affected by the extension's update) */ + app_tmp = gs_app_list_lookup (list_updates, "*/flatpak/test/*/org.test.Chiron/*"); + g_assert_true (app_tmp == app); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_UPDATABLE_LIVE); + + /* care about signals */ + pending_apps_changed_id = + g_signal_connect (plugin_loader, "pending-apps-changed", + G_CALLBACK (gs_plugins_flatpak_count_signal_cb), + &pending_app_changed_cnt); + updates_changed_id = + g_signal_connect (plugin_loader, "updates-changed", + G_CALLBACK (gs_plugins_flatpak_count_signal_cb), + &updates_changed_cnt); + notify_state_id = + g_signal_connect (app, "notify::state", + G_CALLBACK (update_app_state_notify_cb), + &got_progress_installing); + notify_progress_id = + g_signal_connect (app, "notify::progress", + G_CALLBACK (update_app_progress_notify_cb), + &progress_cnt); + + /* use a mainloop so we get the events in the default context */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPDATE, + "app", app, + NULL); + gs_plugin_loader_job_process_async (plugin_loader, plugin_job, + NULL, + update_app_action_finish_sync, + loop); + g_main_loop_run (loop); + gs_test_flush_main_context (); +#if !FLATPAK_CHECK_VERSION(1,7,3) + /* Older flatpak versions don't have the API we use to propagate state + * between extension and app + */ + gs_app_set_state (app, AS_APP_STATE_INSTALLED); +#else + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_INSTALLED); +#endif + g_assert_cmpstr (gs_app_get_version (app), ==, "1.2.3"); + g_assert_true (got_progress_installing); + g_assert_cmpint (pending_app_changed_cnt, ==, 0); + + /* check the extension's state after the update */ + g_assert_cmpint (gs_app_get_state (extension), ==, AS_APP_STATE_INSTALLED); + + /* no longer care */ + g_signal_handler_disconnect (plugin_loader, pending_apps_changed_id); + g_signal_handler_disconnect (plugin_loader, updates_changed_id); + g_signal_handler_disconnect (app, notify_state_id); + g_signal_handler_disconnect (app, notify_progress_id); + + /* getting the runtime for later removal */ + runtime = gs_app_get_runtime (app); + + /* remove the app */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* remove the runtime */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", runtime, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (runtime), ==, AS_APP_STATE_AVAILABLE); + + /* remove the remote */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", app_source, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app_source), ==, AS_APP_STATE_AVAILABLE); + + /* verify that the extension has been removed by the app's removal */ + g_assert_false (gs_app_is_installed (extension)); +} + +int +main (int argc, char **argv) +{ + g_autofree gchar *tmp_root = NULL; + gboolean ret; + int retval; + g_autofree gchar *xml = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsPluginLoader) plugin_loader = NULL; + const gchar *allowlist[] = { + "appstream", + "flatpak", + "icons", + 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); + g_setenv ("GS_XMLB_VERBOSE", "1", TRUE); + g_setenv ("GS_SELF_TEST_PLUGIN_ERROR_FAIL_HARD", "1", TRUE); + + /* Use a common cache directory for all tests, since the appstream + * plugin uses it and cannot be reinitialised for each test. */ + tmp_root = g_dir_make_tmp ("gnome-software-flatpak-test-XXXXXX", NULL); + g_assert_true (tmp_root != NULL); + g_setenv ("GS_SELF_TEST_CACHEDIR", tmp_root, TRUE); + g_setenv ("GS_SELF_TEST_FLATPAK_DATADIR", tmp_root, TRUE); + + /* allow dist'ing with no gnome-software installed */ + if (g_getenv ("GS_SELF_TEST_SKIP_ALL") != NULL) + return 0; + + xml = g_strdup ("<?xml version=\"1.0\"?>\n" + "<components version=\"0.9\">\n" + " <component type=\"desktop\">\n" + " <id>zeus.desktop</id>\n" + " <name>Zeus</name>\n" + " <summary>A teaching application</summary>\n" + " </component>\n" + "</components>\n"); + g_setenv ("GS_SELF_TEST_APPSTREAM_XML", xml, TRUE); + + /* 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); + gs_plugin_loader_add_location (plugin_loader, LOCALPLUGINDIR_CORE); + ret = gs_plugin_loader_setup (plugin_loader, + (gchar**) allowlist, + NULL, + NULL, + &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* plugin tests go here */ + g_test_add_data_func ("/gnome-software/plugins/flatpak/app-with-runtime", + plugin_loader, + (GTestDataFunc) gs_plugins_flatpak_app_with_runtime_func); + g_test_add_data_func ("/gnome-software/plugins/flatpak/app-missing-runtime", + plugin_loader, + (GTestDataFunc) gs_plugins_flatpak_app_missing_runtime_func); + g_test_add_data_func ("/gnome-software/plugins/flatpak/ref", + plugin_loader, + (GTestDataFunc) gs_plugins_flatpak_ref_func); + g_test_add_data_func ("/gnome-software/plugins/flatpak/bundle", + plugin_loader, + (GTestDataFunc) gs_plugins_flatpak_bundle_func); + g_test_add_data_func ("/gnome-software/plugins/flatpak/broken-remote", + plugin_loader, + (GTestDataFunc) gs_plugins_flatpak_broken_remote_func); + g_test_add_data_func ("/gnome-software/plugins/flatpak/runtime-repo", + plugin_loader, + (GTestDataFunc) gs_plugins_flatpak_runtime_repo_func); + g_test_add_data_func ("/gnome-software/plugins/flatpak/runtime-repo-redundant", + plugin_loader, + (GTestDataFunc) gs_plugins_flatpak_runtime_repo_redundant_func); + g_test_add_data_func ("/gnome-software/plugins/flatpak/app-runtime-extension", + plugin_loader, + (GTestDataFunc) gs_plugins_flatpak_runtime_extension_func); + g_test_add_data_func ("/gnome-software/plugins/flatpak/app-update-runtime", + plugin_loader, + (GTestDataFunc) gs_plugins_flatpak_app_update_func); + g_test_add_data_func ("/gnome-software/plugins/flatpak/repo", + plugin_loader, + (GTestDataFunc) gs_plugins_flatpak_repo_func); + g_test_add_data_func ("/gnome-software/plugins/flatpak/repo{non-ascii}", + plugin_loader, + (GTestDataFunc) gs_plugins_flatpak_repo_non_ascii_func); + retval = g_test_run (); + + /* Clean up. */ + gs_utils_rmtree (tmp_root, NULL); + + return retval; +} diff --git a/plugins/flatpak/meson.build b/plugins/flatpak/meson.build new file mode 100644 index 0000000..0afc5a9 --- /dev/null +++ b/plugins/flatpak/meson.build @@ -0,0 +1,70 @@ +cargs = ['-DG_LOG_DOMAIN="GsPluginFlatpak"'] +deps = [ + plugin_libs, + flatpak, + libxmlb, + ostree, +] + +if get_option('mogwai') + deps += mogwai_schedule_client +endif + +shared_module( + 'gs_plugin_flatpak', + sources : [ + 'gs-appstream.c', + 'gs-flatpak-app.c', + 'gs-flatpak.c', + 'gs-flatpak-transaction.c', + 'gs-flatpak-utils.c', + 'gs-plugin-flatpak.c' + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : deps, + link_with : [ + libgnomesoftware + ] +) +metainfo = 'org.gnome.Software.Plugin.Flatpak.metainfo.xml' + +i18n.merge_file( + input: metainfo + '.in', + output: metainfo, + type: 'xml', + po_dir: join_paths(meson.source_root(), 'po'), + install: true, + install_dir: join_paths(get_option('datadir'), 'metainfo') +) + +if get_option('tests') + subdir('tests') + + cargs += ['-DLOCALPLUGINDIR="' + meson.current_build_dir() + '"'] + cargs += ['-DLOCALPLUGINDIR_CORE="' + meson.current_build_dir() + '/../core"'] + cargs += ['-DTESTDATADIR="' + join_paths(meson.current_build_dir(), 'tests') + '"'] + e = executable( + 'gs-self-test-flatpak', + compiled_schemas, + sources : [ + 'gs-flatpak-app.c', + 'gs-self-test.c' + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + dependencies : deps, + link_with : [ + libgnomesoftware + ], + c_args : cargs, + ) + test('gs-self-test-flatpak', e, suite: ['plugins', 'flatpak'], env: test_env, timeout : 120) +endif diff --git a/plugins/flatpak/org.gnome.Software.Plugin.Flatpak.metainfo.xml.in b/plugins/flatpak/org.gnome.Software.Plugin.Flatpak.metainfo.xml.in new file mode 100644 index 0000000..44d6d03 --- /dev/null +++ b/plugins/flatpak/org.gnome.Software.Plugin.Flatpak.metainfo.xml.in @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright 2013-2016 Richard Hughes <richard@hughsie.com> --> +<component type="addon"> + <id>org.gnome.Software.Plugin.Flatpak</id> + <extends>org.gnome.Software.desktop</extends> + <name>Flatpak Support</name> + <summary>Flatpak is a framework for desktop applications on Linux</summary> + <url type="homepage">http://flatpak.org/</url> + <metadata_license>CC0-1.0</metadata_license> + <project_license>GPL-2.0+</project_license> + <update_contact>richard_at_hughsie.com</update_contact> +</component> diff --git a/plugins/flatpak/tests/app-extension-update/.gitignore b/plugins/flatpak/tests/app-extension-update/.gitignore new file mode 100644 index 0000000..f606d5e --- /dev/null +++ b/plugins/flatpak/tests/app-extension-update/.gitignore @@ -0,0 +1 @@ +repo diff --git a/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/.gitignore b/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/.gitignore new file mode 100644 index 0000000..db00ec8 --- /dev/null +++ b/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/.gitignore @@ -0,0 +1 @@ +files/share/app-info diff --git a/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/files/.empty b/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/files/.empty new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/files/.empty diff --git a/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/files/share/libtest/README b/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/files/share/libtest/README new file mode 100644 index 0000000..a0b9703 --- /dev/null +++ b/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/files/share/libtest/README @@ -0,0 +1 @@ +UPDATED!
\ No newline at end of file diff --git a/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/files/share/metainfo/org.test.Chiron.Extension.metainfo.xml b/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/files/share/metainfo/org.test.Chiron.Extension.metainfo.xml new file mode 100644 index 0000000..d884539 --- /dev/null +++ b/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/files/share/metainfo/org.test.Chiron.Extension.metainfo.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright 2017 Endless Mobile, Inc. + Author: Joaquim Rocha <jrocha@endlessm.com> +--> +<component type="runtime"> + <id>org.test.Chiron.Extension</id> + <metadata_license>CC0</metadata_license> + <project_license>GPL-2.0+</project_license> + <name>Chiron App Extension</name> + <summary>Test extension for flatpak self tests</summary> +</component> + diff --git a/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/metadata b/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/metadata new file mode 100644 index 0000000..d81f8f9 --- /dev/null +++ b/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/metadata @@ -0,0 +1,6 @@ +[Runtime] +name=org.test.Chiron.Extension +sdk=org.test.Runtime/x86_64/master + +[ExtensionOf] +ref=app/org.test.Chiron/x86_64/master diff --git a/plugins/flatpak/tests/app-extension/.gitignore b/plugins/flatpak/tests/app-extension/.gitignore new file mode 100644 index 0000000..f606d5e --- /dev/null +++ b/plugins/flatpak/tests/app-extension/.gitignore @@ -0,0 +1 @@ +repo diff --git a/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/.gitignore b/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/.gitignore new file mode 100644 index 0000000..db00ec8 --- /dev/null +++ b/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/.gitignore @@ -0,0 +1 @@ +files/share/app-info diff --git a/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/files/.empty b/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/files/.empty new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/files/.empty diff --git a/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/files/share/libtest/README b/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/files/share/libtest/README new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/files/share/libtest/README diff --git a/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/files/share/metainfo/org.test.Chiron.Extension.metainfo.xml b/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/files/share/metainfo/org.test.Chiron.Extension.metainfo.xml new file mode 100644 index 0000000..d884539 --- /dev/null +++ b/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/files/share/metainfo/org.test.Chiron.Extension.metainfo.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright 2017 Endless Mobile, Inc. + Author: Joaquim Rocha <jrocha@endlessm.com> +--> +<component type="runtime"> + <id>org.test.Chiron.Extension</id> + <metadata_license>CC0</metadata_license> + <project_license>GPL-2.0+</project_license> + <name>Chiron App Extension</name> + <summary>Test extension for flatpak self tests</summary> +</component> + diff --git a/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/metadata b/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/metadata new file mode 100644 index 0000000..d81f8f9 --- /dev/null +++ b/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/metadata @@ -0,0 +1,6 @@ +[Runtime] +name=org.test.Chiron.Extension +sdk=org.test.Runtime/x86_64/master + +[ExtensionOf] +ref=app/org.test.Chiron/x86_64/master diff --git a/plugins/flatpak/tests/app-extension/org.test.Chiron/.gitignore b/plugins/flatpak/tests/app-extension/org.test.Chiron/.gitignore new file mode 100644 index 0000000..fea15c0 --- /dev/null +++ b/plugins/flatpak/tests/app-extension/org.test.Chiron/.gitignore @@ -0,0 +1,2 @@ +export +files/share/app-info diff --git a/plugins/flatpak/tests/app-extension/org.test.Chiron/files/bin/chiron.sh b/plugins/flatpak/tests/app-extension/org.test.Chiron/files/bin/chiron.sh new file mode 100755 index 0000000..e61d501 --- /dev/null +++ b/plugins/flatpak/tests/app-extension/org.test.Chiron/files/bin/chiron.sh @@ -0,0 +1,2 @@ +#!/bin/sh +echo "Hello world" diff --git a/plugins/flatpak/tests/app-extension/org.test.Chiron/files/share/appdata/org.test.Chiron.appdata.xml b/plugins/flatpak/tests/app-extension/org.test.Chiron/files/share/appdata/org.test.Chiron.appdata.xml new file mode 100644 index 0000000..0d912a8 --- /dev/null +++ b/plugins/flatpak/tests/app-extension/org.test.Chiron/files/share/appdata/org.test.Chiron.appdata.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright 2016 Richard Hughes <richard@hughsie.com> --> +<component type="desktop"> + <id>org.test.Chiron.desktop</id> + <metadata_license>CC0-1.0</metadata_license> + <project_license>GPL-2.0+</project_license> + <name>Chiron</name> + <summary>Single line synopsis</summary> + <description><p>Long description.</p></description> + <url type="homepage">http://127.0.0.1/</url> + <releases> + <release date="2014-12-15" version="1.2.3"> + <description><p>This is better.</p></description> + </release> + </releases> +</component> diff --git a/plugins/flatpak/tests/app-extension/org.test.Chiron/files/share/applications/org.test.Chiron.desktop b/plugins/flatpak/tests/app-extension/org.test.Chiron/files/share/applications/org.test.Chiron.desktop new file mode 100644 index 0000000..2fbdf95 --- /dev/null +++ b/plugins/flatpak/tests/app-extension/org.test.Chiron/files/share/applications/org.test.Chiron.desktop @@ -0,0 +1,6 @@ +[Desktop Entry] +Type=Application +Name=Chiron +Exec=chiron.sh +Icon=org.test.Chiron +Keywords=Bingo; diff --git a/plugins/flatpak/tests/app-extension/org.test.Chiron/files/share/icons/hicolor/128x128/apps/org.test.Chiron.png b/plugins/flatpak/tests/app-extension/org.test.Chiron/files/share/icons/hicolor/128x128/apps/org.test.Chiron.png Binary files differnew file mode 100644 index 0000000..0c38f2f --- /dev/null +++ b/plugins/flatpak/tests/app-extension/org.test.Chiron/files/share/icons/hicolor/128x128/apps/org.test.Chiron.png diff --git a/plugins/flatpak/tests/app-extension/org.test.Chiron/metadata b/plugins/flatpak/tests/app-extension/org.test.Chiron/metadata new file mode 100644 index 0000000..45b76d6 --- /dev/null +++ b/plugins/flatpak/tests/app-extension/org.test.Chiron/metadata @@ -0,0 +1,10 @@ +[Application] +name=org.test.Chiron +runtime=org.test.Runtime/x86_64/master +command=chiron.sh + +[Extension org.test.Chiron.Extension] +directory=share/extension +subdirectories=true +version=master +autodelete=true diff --git a/plugins/flatpak/tests/app-missing-runtime/.gitignore b/plugins/flatpak/tests/app-missing-runtime/.gitignore new file mode 100644 index 0000000..f606d5e --- /dev/null +++ b/plugins/flatpak/tests/app-missing-runtime/.gitignore @@ -0,0 +1 @@ +repo diff --git a/plugins/flatpak/tests/app-missing-runtime/org.test.Chiron b/plugins/flatpak/tests/app-missing-runtime/org.test.Chiron new file mode 120000 index 0000000..d9384e4 --- /dev/null +++ b/plugins/flatpak/tests/app-missing-runtime/org.test.Chiron @@ -0,0 +1 @@ +../app-with-runtime/org.test.Chiron/
\ No newline at end of file diff --git a/plugins/flatpak/tests/app-update/.gitignore b/plugins/flatpak/tests/app-update/.gitignore new file mode 100644 index 0000000..f606d5e --- /dev/null +++ b/plugins/flatpak/tests/app-update/.gitignore @@ -0,0 +1 @@ +repo diff --git a/plugins/flatpak/tests/app-update/org.test.Chiron/.gitignore b/plugins/flatpak/tests/app-update/org.test.Chiron/.gitignore new file mode 120000 index 0000000..9f2eb6a --- /dev/null +++ b/plugins/flatpak/tests/app-update/org.test.Chiron/.gitignore @@ -0,0 +1 @@ +../../app-with-runtime/org.test.Chiron/.gitignore
\ No newline at end of file diff --git a/plugins/flatpak/tests/app-update/org.test.Chiron/files/bin/chiron.sh b/plugins/flatpak/tests/app-update/org.test.Chiron/files/bin/chiron.sh new file mode 100644 index 0000000..dfed21c --- /dev/null +++ b/plugins/flatpak/tests/app-update/org.test.Chiron/files/bin/chiron.sh @@ -0,0 +1,2 @@ +#!/bin/sh +echo "Hello world, with upgrades" diff --git a/plugins/flatpak/tests/app-update/org.test.Chiron/files/share/appdata/org.test.Chiron.appdata.xml b/plugins/flatpak/tests/app-update/org.test.Chiron/files/share/appdata/org.test.Chiron.appdata.xml new file mode 100644 index 0000000..74eb9db --- /dev/null +++ b/plugins/flatpak/tests/app-update/org.test.Chiron/files/share/appdata/org.test.Chiron.appdata.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright 2016 Richard Hughes <richard@hughsie.com> --> +<component type="desktop"> + <id>org.test.Chiron</id> + <metadata_license>CC0-1.0</metadata_license> + <project_license>GPL-2.0+</project_license> + <name>Chiron</name> + <summary>Single line synopsis</summary> + <description><p>Long description.</p></description> + <url type="homepage">http://127.0.0.1/</url> + <releases> + <release date="2015-02-13" version="1.2.4"> + <description><p>This is best.</p></description> + </release> + <release date="2014-12-15" version="1.2.3"> + <description><p>This is better.</p></description> + </release> + </releases> +</component> diff --git a/plugins/flatpak/tests/app-update/org.test.Chiron/files/share/applications/org.test.Chiron.desktop b/plugins/flatpak/tests/app-update/org.test.Chiron/files/share/applications/org.test.Chiron.desktop new file mode 120000 index 0000000..2b06818 --- /dev/null +++ b/plugins/flatpak/tests/app-update/org.test.Chiron/files/share/applications/org.test.Chiron.desktop @@ -0,0 +1 @@ +../../../../../app-missing-runtime/org.test.Chiron/files/share/applications/org.test.Chiron.desktop
\ No newline at end of file diff --git a/plugins/flatpak/tests/app-update/org.test.Chiron/files/share/icons/hicolor/128x128/apps/org.test.Chiron.png b/plugins/flatpak/tests/app-update/org.test.Chiron/files/share/icons/hicolor/128x128/apps/org.test.Chiron.png new file mode 120000 index 0000000..9c37986 --- /dev/null +++ b/plugins/flatpak/tests/app-update/org.test.Chiron/files/share/icons/hicolor/128x128/apps/org.test.Chiron.png @@ -0,0 +1 @@ +../../../../../../../../app-missing-runtime/org.test.Chiron/files/share/icons/hicolor/128x128/apps/org.test.Chiron.png
\ No newline at end of file diff --git a/plugins/flatpak/tests/app-update/org.test.Chiron/metadata b/plugins/flatpak/tests/app-update/org.test.Chiron/metadata new file mode 100644 index 0000000..1de0ab8 --- /dev/null +++ b/plugins/flatpak/tests/app-update/org.test.Chiron/metadata @@ -0,0 +1,4 @@ +[Application] +name=org.test.Chiron +runtime=org.test.Runtime/x86_64/new_master +command=chiron.sh diff --git a/plugins/flatpak/tests/app-with-runtime/.gitignore b/plugins/flatpak/tests/app-with-runtime/.gitignore new file mode 100644 index 0000000..f606d5e --- /dev/null +++ b/plugins/flatpak/tests/app-with-runtime/.gitignore @@ -0,0 +1 @@ +repo diff --git a/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/.gitignore b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/.gitignore new file mode 100644 index 0000000..fea15c0 --- /dev/null +++ b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/.gitignore @@ -0,0 +1,2 @@ +export +files/share/app-info diff --git a/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/bin/chiron.sh b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/bin/chiron.sh new file mode 100755 index 0000000..e61d501 --- /dev/null +++ b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/bin/chiron.sh @@ -0,0 +1,2 @@ +#!/bin/sh +echo "Hello world" diff --git a/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/share/appdata/org.test.Chiron.appdata.xml b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/share/appdata/org.test.Chiron.appdata.xml new file mode 100644 index 0000000..58af082 --- /dev/null +++ b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/share/appdata/org.test.Chiron.appdata.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright 2016 Richard Hughes <richard@hughsie.com> --> +<component type="desktop"> + <id>org.test.Chiron</id> + <metadata_license>CC0-1.0</metadata_license> + <project_license>GPL-2.0+</project_license> + <name>Chiron</name> + <summary>Single line synopsis</summary> + <description><p>Long description.</p></description> + <url type="homepage">http://127.0.0.1/</url> + <releases> + <release date="2014-12-15" version="1.2.3"> + <description><p>This is better.</p></description> + </release> + </releases> +</component> diff --git a/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/share/applications/org.test.Chiron.desktop b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/share/applications/org.test.Chiron.desktop new file mode 100644 index 0000000..b744766 --- /dev/null +++ b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/share/applications/org.test.Chiron.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Type=Application +Name=Chiron +Exec=chiron.sh +Icon=org.test.Chiron +Keywords=Bingo; +X-Flatpak=org.test.Chiron diff --git a/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/share/icons/hicolor/128x128/apps/org.test.Chiron.png b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/share/icons/hicolor/128x128/apps/org.test.Chiron.png Binary files differnew file mode 100644 index 0000000..0c38f2f --- /dev/null +++ b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/share/icons/hicolor/128x128/apps/org.test.Chiron.png diff --git a/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/metadata b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/metadata new file mode 100644 index 0000000..ce57357 --- /dev/null +++ b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/metadata @@ -0,0 +1,4 @@ +[Application] +name=org.test.Chiron +runtime=org.test.Runtime/x86_64/master +command=chiron.sh diff --git a/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/files/.empty b/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/files/.empty new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/files/.empty diff --git a/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/metadata b/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/metadata new file mode 100644 index 0000000..16f0fa1 --- /dev/null +++ b/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/metadata @@ -0,0 +1,3 @@ +[Runtime] +name=org.test.Runtime +sdk=org.test.Runtime/x86_64/master diff --git a/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/usr/.gitignore b/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/usr/.gitignore new file mode 100644 index 0000000..3600b9c --- /dev/null +++ b/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/usr/.gitignore @@ -0,0 +1 @@ +app-info diff --git a/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/usr/share/libtest/README b/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/usr/share/libtest/README new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/usr/share/libtest/README diff --git a/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/usr/share/metainfo/org.test.Runtime.metainfo.xml b/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/usr/share/metainfo/org.test.Runtime.metainfo.xml new file mode 100644 index 0000000..5d68c60 --- /dev/null +++ b/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/usr/share/metainfo/org.test.Runtime.metainfo.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright 2017 Richard Hughes <richard@hughsie.com> --> +<component type="runtime"> + <id>org.test.Runtime</id> + <metadata_license>CC0</metadata_license> + <project_license>GPL-2.0+</project_license> + <name>Test runtime</name> + <summary>Test runtime for flatpak self tests</summary> +</component> + diff --git a/plugins/flatpak/tests/build.py b/plugins/flatpak/tests/build.py new file mode 100755 index 0000000..acc7e66 --- /dev/null +++ b/plugins/flatpak/tests/build.py @@ -0,0 +1,125 @@ +#!/usr/bin/python3 + +import subprocess +import os +import shutil +import configparser + +def build_flatpak(appid, srcdir, repodir, branch='master', cleanrepodir=True): + print('Building %s from %s into %s' % (appid, srcdir, repodir)) + + # delete repodir + if cleanrepodir and os.path.exists(repodir): + print("Deleting %s" % repodir) + shutil.rmtree(repodir) + + # delete exportdir + exportdir = os.path.join(srcdir, appid, 'export') + if os.path.exists(exportdir): + print("Deleting %s" % exportdir) + shutil.rmtree(exportdir) + + metadata_path = os.path.join(srcdir, appid, 'metadata') + metadata = configparser.ConfigParser() + metadata.read(metadata_path) + is_runtime = True if 'Runtime' in metadata.sections() else False + is_extension = True if 'ExtensionOf' in metadata.sections() else False + + # runtimes have different defaults + if is_runtime and not is_extension: + prefix = 'usr' + else: + prefix = 'files' + + # finish the build + argv = ['flatpak', 'build-finish'] + argv.append(os.path.join(srcdir, appid)) + subprocess.call(argv) + + # compose AppStream data + argv = ['appstream-compose'] + argv.append('--origin=flatpak') + argv.append('--basename=%s' % appid) + argv.append('--prefix=%s' % os.path.join(srcdir, appid, prefix)) + argv.append('--output-dir=%s' % os.path.join(srcdir, appid, prefix, 'share/app-info/xmls')) + argv.append(appid) + subprocess.call(argv) + + # export into repo + argv = ['flatpak', 'build-export'] + argv.append(repodir) + argv.append(os.path.join(srcdir, appid)) + argv.append(branch) + argv.append('--update-appstream') + argv.append('--timestamp=2016-09-15T01:02:03') + if is_runtime: + argv.append('--runtime') + subprocess.call(argv) + +def build_flatpak_bundle(appid, srcdir, repodir, filename, branch='master'): + argv = ['flatpak', 'build-bundle'] + argv.append(repodir) + argv.append(filename) + argv.append(appid) + argv.append(branch) + subprocess.call(argv) + +def copy_repo(srcdir, destdir): + srcdir_repo = os.path.join(srcdir, 'repo') + destdir_repo = os.path.join(destdir, 'repo') + print("Copying %s to %s" % (srcdir_repo, destdir_repo)) + if os.path.exists(destdir_repo): + shutil.rmtree(destdir_repo) + shutil.copytree(srcdir_repo, destdir_repo) + +# normal app with runtime in same remote +build_flatpak('org.test.Chiron', + 'app-with-runtime', + 'app-with-runtime/repo') +build_flatpak('org.test.Runtime', + 'app-with-runtime', + 'app-with-runtime/repo', + cleanrepodir=False) + +# build a flatpak bundle for the app +build_flatpak_bundle('org.test.Chiron', + 'app-with-runtime', + 'app-with-runtime/repo', + 'chiron.flatpak') + +# app referencing remote that cannot be found +build_flatpak('org.test.Chiron', + 'app-with-runtime', + 'app-missing-runtime/repo') + +# app with an update +build_flatpak('org.test.Runtime', + 'app-with-runtime', + 'app-update/repo', + branch='new_master', + cleanrepodir=True) +build_flatpak('org.test.Chiron', + 'app-update', + 'app-update/repo', + cleanrepodir=False) + +# just a runtime present +build_flatpak('org.test.Runtime', + 'only-runtime', + 'only-runtime/repo') + +# app with an extension +copy_repo('only-runtime', 'app-extension') +build_flatpak('org.test.Chiron', + 'app-extension', + 'app-extension/repo', + cleanrepodir=False) +build_flatpak('org.test.Chiron.Extension', + 'app-extension', + 'app-extension/repo', + cleanrepodir=False) +copy_repo('app-extension', 'app-extension-update') +build_flatpak('org.test.Chiron.Extension', + 'app-extension-update', + 'app-extension-update/repo', + cleanrepodir=False) diff --git a/plugins/flatpak/tests/chiron.flatpak b/plugins/flatpak/tests/chiron.flatpak Binary files differnew file mode 100644 index 0000000..ce038e9 --- /dev/null +++ b/plugins/flatpak/tests/chiron.flatpak diff --git a/plugins/flatpak/tests/flatpakrepos.tar.gz b/plugins/flatpak/tests/flatpakrepos.tar.gz Binary files differnew file mode 100644 index 0000000..f8bcfde --- /dev/null +++ b/plugins/flatpak/tests/flatpakrepos.tar.gz diff --git a/plugins/flatpak/tests/meson.build b/plugins/flatpak/tests/meson.build new file mode 100644 index 0000000..9e48b00 --- /dev/null +++ b/plugins/flatpak/tests/meson.build @@ -0,0 +1,34 @@ +tar = find_program('tar') +custom_target( + 'flatpak-self-test-data', + input : 'flatpakrepos.tar.gz', + output : 'done', + command : [ + tar, + '--no-same-owner', + '--directory=' + meson.current_build_dir(), + '-xf', '@INPUT@', + ], + build_by_default : true, +) + +custom_target( + 'flatpak-self-test-bundle', + output : 'flatpakrepos.tar.gz', + command : [ + tar, + '-czf', '@OUTPUT@', + 'app-missing-runtime/repo/', + 'app-update/repo/', + 'app-with-runtime/repo/', + 'only-runtime/repo/', + 'app-extension/repo', + 'app-extension-update/repo', + ], +) + +configure_file( + input : 'chiron.flatpak', + output : 'chiron.flatpak', + copy : true, +) diff --git a/plugins/flatpak/tests/only-runtime/.gitignore b/plugins/flatpak/tests/only-runtime/.gitignore new file mode 100644 index 0000000..f606d5e --- /dev/null +++ b/plugins/flatpak/tests/only-runtime/.gitignore @@ -0,0 +1 @@ +repo diff --git a/plugins/flatpak/tests/only-runtime/org.test.Runtime b/plugins/flatpak/tests/only-runtime/org.test.Runtime new file mode 120000 index 0000000..eb7054c --- /dev/null +++ b/plugins/flatpak/tests/only-runtime/org.test.Runtime @@ -0,0 +1 @@ +../app-with-runtime/org.test.Runtime/
\ No newline at end of file diff --git a/plugins/fwupd/gs-fwupd-app.c b/plugins/fwupd/gs-fwupd-app.c new file mode 100644 index 0000000..df6505b --- /dev/null +++ b/plugins/fwupd/gs-fwupd-app.c @@ -0,0 +1,242 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2017 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <string.h> +#include <glib/gi18n.h> + +#include "gs-fwupd-app.h" + +const gchar * +gs_fwupd_app_get_device_id (GsApp *app) +{ + return gs_app_get_metadata_item (app, "fwupd::DeviceID"); +} + +const gchar * +gs_fwupd_app_get_update_uri (GsApp *app) +{ + return gs_app_get_metadata_item (app, "fwupd::UpdateID"); +} + +gboolean +gs_fwupd_app_get_is_locked (GsApp *app) +{ + GVariant *tmp = gs_app_get_metadata_variant (app, "fwupd::IsLocked"); + if (tmp == NULL) + return FALSE; + return g_variant_get_boolean (tmp); +} + +void +gs_fwupd_app_set_device_id (GsApp *app, const gchar *device_id) +{ + gs_app_set_metadata (app, "fwupd::DeviceID", device_id); +} + +void +gs_fwupd_app_set_update_uri (GsApp *app, const gchar *update_uri) +{ + gs_app_set_metadata (app, "fwupd::UpdateID", update_uri); +} + +void +gs_fwupd_app_set_is_locked (GsApp *app, gboolean is_locked) +{ + g_autoptr(GVariant) tmp = g_variant_new_boolean (is_locked); + gs_app_set_metadata_variant (app, "fwupd::IsLocked", tmp); +} + +void +gs_fwupd_app_set_from_device (GsApp *app, FwupdDevice *dev) +{ + GPtrArray *guids; + + /* something can be done */ + if (fwupd_device_has_flag (dev, FWUPD_DEVICE_FLAG_UPDATABLE)) + gs_app_set_state (app, AS_APP_STATE_UPDATABLE_LIVE); + + /* only can be applied in systemd-offline */ + if (fwupd_device_has_flag (dev, FWUPD_DEVICE_FLAG_ONLY_OFFLINE)) + gs_app_set_metadata (app, "fwupd::OnlyOffline", ""); + + + /* reboot required to apply update */ + if (fwupd_device_has_flag (dev, FWUPD_DEVICE_FLAG_NEEDS_REBOOT)) + gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT); + + /* is removable */ + if (!fwupd_device_has_flag (dev, FWUPD_DEVICE_FLAG_INTERNAL)) + gs_app_add_quirk (app, GS_APP_QUIRK_REMOVABLE_HARDWARE); + + guids = fwupd_device_get_guids (dev); + if (guids->len > 0) { + g_autofree gchar *guid_str = NULL; + g_auto(GStrv) tmp = g_new0 (gchar *, guids->len + 1); + for (guint i = 0; i < guids->len; i++) + tmp[i] = g_strdup (g_ptr_array_index (guids, i)); + guid_str = g_strjoinv (",", tmp); + gs_app_set_metadata (app, "fwupd::Guid", guid_str); + } + if (fwupd_device_get_name (dev) != NULL) { + g_autofree gchar *vendor_name = NULL; + if (fwupd_device_get_vendor (dev) == NULL || + g_str_has_prefix (fwupd_device_get_name (dev), + fwupd_device_get_vendor (dev))) { + vendor_name = g_strdup (fwupd_device_get_name (dev)); + } else { + vendor_name = g_strdup_printf ("%s %s", + fwupd_device_get_vendor (dev), + fwupd_device_get_name (dev)); + } + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, vendor_name); + } + if (fwupd_device_get_summary (dev) != NULL) { + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, + fwupd_device_get_summary (dev)); + } + if (fwupd_device_get_version (dev) != NULL) { + gs_app_set_version (app, fwupd_device_get_version (dev)); + } + if (fwupd_device_get_created (dev) != 0) + gs_app_set_install_date (app, fwupd_device_get_created (dev)); + if (fwupd_device_get_description (dev) != NULL) { + g_autofree gchar *tmp = NULL; + tmp = as_markup_convert (fwupd_device_get_description (dev), + AS_MARKUP_CONVERT_FORMAT_SIMPLE, NULL); + if (tmp != NULL) + gs_app_set_description (app, GS_APP_QUALITY_NORMAL, tmp); + } + + /* needs action */ + if (fwupd_device_has_flag (dev, FWUPD_DEVICE_FLAG_NEEDS_BOOTLOADER)) + gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_USER_ACTION); + else + gs_app_remove_quirk (app, GS_APP_QUIRK_NEEDS_USER_ACTION); +} + +static gchar * +gs_fwupd_release_get_name (FwupdRelease *release) +{ + const gchar *name = fwupd_release_get_name (release); +#if FWUPD_CHECK_VERSION(1,2,7) + GPtrArray *cats = fwupd_release_get_categories (release); + + for (guint i = 0; i < cats->len; i++) { + const gchar *cat = g_ptr_array_index (cats, i); + if (g_strcmp0 (cat, "X-Device") == 0) { + /* TRANSLATORS: a specific part of hardware, + * the first %s is the device name, e.g. 'Unifying Receiver` */ + return g_strdup_printf (_("%s Device Update"), name); + } + if (g_strcmp0 (cat, "X-System") == 0) { + /* TRANSLATORS: the entire system, e.g. all internal devices, + * the first %s is the device name, e.g. 'ThinkPad P50` */ + return g_strdup_printf (_("%s System Update"), name); + } + if (g_strcmp0 (cat, "X-EmbeddedController") == 0) { + /* TRANSLATORS: the EC is typically the keyboard controller chip, + * the first %s is the device name, e.g. 'ThinkPad P50` */ + return g_strdup_printf (_("%s Embedded Controller Update"), name); + } + if (g_strcmp0 (cat, "X-ManagementEngine") == 0) { + /* TRANSLATORS: ME stands for Management Engine, the Intel AMT thing, + * the first %s is the device name, e.g. 'ThinkPad P50` */ + return g_strdup_printf (_("%s ME Update"), name); + } + if (g_strcmp0 (cat, "X-CorporateManagementEngine") == 0) { + /* TRANSLATORS: ME stands for Management Engine (with Intel AMT), + * where the first %s is the device name, e.g. 'ThinkPad P50` */ + return g_strdup_printf (_("%s Corporate ME Update"), name); + } + if (g_strcmp0 (cat, "X-ConsumerManagementEngine") == 0) { + /* TRANSLATORS: ME stands for Management Engine, where + * the first %s is the device name, e.g. 'ThinkPad P50` */ + return g_strdup_printf (_("%s Consumer ME Update"), name); + } + if (g_strcmp0 (cat, "X-Controller") == 0) { + /* TRANSLATORS: the controller is a device that has other devices + * plugged into it, for example ThunderBolt, FireWire or USB, + * the first %s is the device name, e.g. 'Intel ThunderBolt` */ + return g_strdup_printf (_("%s Controller Update"), name); + } + if (g_strcmp0 (cat, "X-ThunderboltController") == 0) { + /* TRANSLATORS: the Thunderbolt controller is a device that + * has other high speed Thunderbolt devices plugged into it; + * the first %s is the system name, e.g. 'ThinkPad P50` */ + return g_strdup_printf (_("%s Thunderbolt Controller Update"), name); + } + if (g_strcmp0 (cat, "X-CpuMicrocode") == 0) { + /* TRANSLATORS: the CPU microcode is firmware loaded onto the CPU + * at system bootup */ + return g_strdup_printf (_("%s CPU Microcode Update"), name); + } + if (g_strcmp0 (cat, "X-Configuration") == 0) { + /* TRANSLATORS: configuration refers to hardware state, + * e.g. a security database or a default power value */ + return g_strdup_printf (_("%s Configuration Update"), name); + } + } +#endif + + /* default fallback */ + return g_strdup (name); +} + +void +gs_fwupd_app_set_from_release (GsApp *app, FwupdRelease *rel) +{ + if (fwupd_release_get_name (rel) != NULL) { + g_autofree gchar *tmp = gs_fwupd_release_get_name (rel); + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, tmp); + } + if (fwupd_release_get_summary (rel) != NULL) { + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, + fwupd_release_get_summary (rel)); + } + if (fwupd_release_get_homepage (rel) != NULL) { + gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, + fwupd_release_get_homepage (rel)); + } + if (fwupd_release_get_size (rel) != 0) { + gs_app_set_size_installed (app, 0); + gs_app_set_size_download (app, fwupd_release_get_size (rel)); + } + if (fwupd_release_get_version (rel) != NULL) + gs_app_set_update_version (app, fwupd_release_get_version (rel)); + if (fwupd_release_get_license (rel) != NULL) { + gs_app_set_license (app, GS_APP_QUALITY_NORMAL, + fwupd_release_get_license (rel)); + } + if (fwupd_release_get_uri (rel) != NULL) { + gs_app_set_origin_hostname (app, + fwupd_release_get_uri (rel)); + gs_fwupd_app_set_update_uri (app, fwupd_release_get_uri (rel)); + } + if (fwupd_release_get_description (rel) != NULL) { + g_autofree gchar *tmp = NULL; + tmp = as_markup_convert (fwupd_release_get_description (rel), + AS_MARKUP_CONVERT_FORMAT_SIMPLE, NULL); + if (tmp != NULL) + gs_app_set_update_details (app, tmp); + } +#if FWUPD_CHECK_VERSION(1,3,3) + if (fwupd_release_get_detach_image (rel) != NULL) { + g_autoptr(AsScreenshot) ss = as_screenshot_new (); + g_autoptr(AsImage) im = as_image_new (); + as_image_set_kind (im, AS_IMAGE_KIND_SOURCE); + as_image_set_url (im, fwupd_release_get_detach_image (rel)); + as_screenshot_set_kind (ss, AS_SCREENSHOT_KIND_DEFAULT); + as_screenshot_add_image (ss, im); + if (fwupd_release_get_detach_caption (rel) != NULL) + as_screenshot_set_caption (ss, NULL, fwupd_release_get_detach_caption (rel)); + gs_app_set_action_screenshot (app, ss); + } +#endif +} diff --git a/plugins/fwupd/gs-fwupd-app.h b/plugins/fwupd/gs-fwupd-app.h new file mode 100644 index 0000000..5a3e9f2 --- /dev/null +++ b/plugins/fwupd/gs-fwupd-app.h @@ -0,0 +1,31 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2017 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gnome-software.h> +#include <fwupd.h> + +G_BEGIN_DECLS + +const gchar *gs_fwupd_app_get_device_id (GsApp *app); +const gchar *gs_fwupd_app_get_update_uri (GsApp *app); +gboolean gs_fwupd_app_get_is_locked (GsApp *app); + +void gs_fwupd_app_set_device_id (GsApp *app, + const gchar *device_id); +void gs_fwupd_app_set_update_uri (GsApp *app, + const gchar *update_uri); +void gs_fwupd_app_set_is_locked (GsApp *app, + gboolean is_locked); +void gs_fwupd_app_set_from_device (GsApp *app, + FwupdDevice *dev); +void gs_fwupd_app_set_from_release (GsApp *app, + FwupdRelease *rel); + +G_END_DECLS diff --git a/plugins/fwupd/gs-plugin-fwupd.c b/plugins/fwupd/gs-plugin-fwupd.c new file mode 100644 index 0000000..d032640 --- /dev/null +++ b/plugins/fwupd/gs-plugin-fwupd.c @@ -0,0 +1,1132 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2018 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <fwupd.h> +#include <fcntl.h> +#include <gio/gio.h> +#include <gio/gunixfdlist.h> +#include <glib/gi18n.h> +#include <glib/gstdio.h> + +#include <gnome-software.h> + +#include "gs-fwupd-app.h" +#include "gs-metered.h" + +/* + * SECTION: + * Queries for new firmware and schedules it to be installed as required. + * + * This plugin calls UpdatesChanged() if any updatable devices are + * added or removed or if a device has been updated live. + */ + +struct GsPluginData { + FwupdClient *client; + GsApp *app_current; + GsApp *cached_origin; +}; + +static void +gs_plugin_fwupd_error_convert (GError **perror) +{ + GError *error = perror != NULL ? *perror : NULL; + + /* not set */ + if (error == NULL) + return; + + /* already correct */ + if (error->domain == GS_PLUGIN_ERROR) + return; + + /* this are allowed for low-level errors */ + if (gs_utils_error_convert_gio (perror)) + return; + + /* this are allowed for low-level errors */ + if (gs_utils_error_convert_gdbus (perror)) + return; + + /* custom to this plugin */ + if (error->domain == FWUPD_ERROR) { + switch (error->code) { + case FWUPD_ERROR_ALREADY_PENDING: + case FWUPD_ERROR_INVALID_FILE: + case FWUPD_ERROR_NOT_SUPPORTED: + error->code = GS_PLUGIN_ERROR_NOT_SUPPORTED; + break; + case FWUPD_ERROR_AUTH_FAILED: + error->code = GS_PLUGIN_ERROR_AUTH_INVALID; + break; + case FWUPD_ERROR_SIGNATURE_INVALID: + error->code = GS_PLUGIN_ERROR_NO_SECURITY; + break; + case FWUPD_ERROR_AC_POWER_REQUIRED: + error->code = GS_PLUGIN_ERROR_AC_POWER_REQUIRED; + break; +#if FWUPD_CHECK_VERSION(1,2,10) + case FWUPD_ERROR_BATTERY_LEVEL_TOO_LOW: + error->code = GS_PLUGIN_ERROR_BATTERY_LEVEL_TOO_LOW; + break; +#endif + default: + error->code = GS_PLUGIN_ERROR_FAILED; + break; + } + } else { + g_warning ("can't reliably fixup error from domain %s", + g_quark_to_string (error->domain)); + error->code = GS_PLUGIN_ERROR_FAILED; + } + error->domain = GS_PLUGIN_ERROR; +} + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData)); + priv->client = fwupd_client_new (); + + /* set name of MetaInfo file */ + gs_plugin_set_appstream_id (plugin, "org.gnome.Software.Plugin.Fwupd"); +} + +void +gs_plugin_destroy (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + if (priv->cached_origin != NULL) + g_object_unref (priv->cached_origin); + g_object_unref (priv->client); +} + +void +gs_plugin_adopt_app (GsPlugin *plugin, GsApp *app) +{ + if (gs_app_get_kind (app) == AS_APP_KIND_FIRMWARE) + gs_app_set_management_plugin (app, gs_plugin_get_name (plugin)); +} + +static void +gs_plugin_fwupd_changed_cb (FwupdClient *client, GsPlugin *plugin) +{ +} + +static void +gs_plugin_fwupd_device_changed_cb (FwupdClient *client, + FwupdDevice *dev, + GsPlugin *plugin) +{ + /* limit number of UI refreshes */ + if (!fwupd_device_has_flag (dev, FWUPD_DEVICE_FLAG_SUPPORTED)) { + g_debug ("%s changed (not supported) so ignoring", + fwupd_device_get_id (dev)); + return; + } + + /* If the flag is set the device matches something in the + * metadata as therefor is worth refreshing the update list */ + g_debug ("%s changed (supported) so reloading", + fwupd_device_get_id (dev)); + gs_plugin_updates_changed (plugin); +} + +static void +gs_plugin_fwupd_notify_percentage_cb (GObject *object, + GParamSpec *pspec, + GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + + /* nothing in progress */ + if (priv->app_current == NULL) { + g_debug ("fwupd percentage: %u%%", + fwupd_client_get_percentage (priv->client)); + return; + } + g_debug ("fwupd percentage for %s: %u%%", + gs_app_get_unique_id (priv->app_current), + fwupd_client_get_percentage (priv->client)); + gs_app_set_progress (priv->app_current, + fwupd_client_get_percentage (priv->client)); +} + +static void +gs_plugin_fwupd_notify_status_cb (GObject *object, + GParamSpec *pspec, + GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + + /* nothing in progress */ + if (priv->app_current == NULL) { + g_debug ("fwupd status: %s", + fwupd_status_to_string (fwupd_client_get_status (priv->client))); + return; + } + + g_debug ("fwupd status for %s: %s", + gs_app_get_unique_id (priv->app_current), + fwupd_status_to_string (fwupd_client_get_status (priv->client))); + switch (fwupd_client_get_status (priv->client)) { + case FWUPD_STATUS_DECOMPRESSING: + case FWUPD_STATUS_DEVICE_RESTART: + case FWUPD_STATUS_DEVICE_WRITE: + case FWUPD_STATUS_DEVICE_VERIFY: + gs_app_set_state (priv->app_current, AS_APP_STATE_INSTALLING); + break; + case FWUPD_STATUS_IDLE: + g_clear_object (&priv->app_current); + break; + default: + break; + } +} + +static gchar * +gs_plugin_fwupd_get_file_checksum (const gchar *filename, + GChecksumType checksum_type, + GError **error) +{ + gsize len; + g_autofree gchar *data = NULL; + + if (!g_file_get_contents (filename, &data, &len, error)) { + gs_utils_error_convert_gio (error); + return NULL; + } + return g_compute_checksum_for_data (checksum_type, (const guchar *)data, len); +} + +gboolean +gs_plugin_setup (GsPlugin *plugin, GCancellable *cancellable, GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(SoupSession) soup_session = NULL; + +#if FWUPD_CHECK_VERSION(1,4,5) + g_autoptr(GError) error_local = NULL; + /* send our implemented feature set */ + if (!fwupd_client_set_feature_flags (priv->client, + FWUPD_FEATURE_FLAG_UPDATE_ACTION | + FWUPD_FEATURE_FLAG_DETACH_ACTION, + cancellable, &error_local)) + g_debug ("Failed to set front-end features: %s", error_local->message); + + /* we know the runtime daemon version now */ + fwupd_client_set_user_agent_for_package (priv->client, PACKAGE_NAME, PACKAGE_VERSION); + if (!fwupd_client_ensure_networking (priv->client, error)) { + gs_plugin_fwupd_error_convert (error); + g_prefix_error (error, "Failed to setup networking: "); + return FALSE; + } + g_object_get (priv->client, "soup-session", &soup_session, NULL); +#else + g_autofree gchar *user_agent = NULL; + /* use a custom user agent to provide the fwupd version */ + user_agent = fwupd_build_user_agent (PACKAGE_NAME, PACKAGE_VERSION); + soup_session = soup_session_new_with_options (SOUP_SESSION_USER_AGENT, user_agent, + SOUP_SESSION_TIMEOUT, 10, + NULL); + soup_session_remove_feature_by_type (soup_session, SOUP_TYPE_CONTENT_DECODER); +#endif + + /* use for gnome-software downloads */ + gs_plugin_set_soup_session (plugin, soup_session); + + /* add source */ + priv->cached_origin = gs_app_new (gs_plugin_get_name (plugin)); + gs_app_set_kind (priv->cached_origin, AS_APP_KIND_SOURCE); + gs_app_set_bundle_kind (priv->cached_origin, AS_BUNDLE_KIND_CABINET); + + /* add the source to the plugin cache which allows us to match the + * unique ID to a GsApp when creating an event */ + gs_plugin_cache_add (plugin, + gs_app_get_unique_id (priv->cached_origin), + priv->cached_origin); + + /* register D-Bus errors */ + fwupd_error_quark (); + g_signal_connect (priv->client, "changed", + G_CALLBACK (gs_plugin_fwupd_changed_cb), plugin); + g_signal_connect (priv->client, "device-added", + G_CALLBACK (gs_plugin_fwupd_device_changed_cb), plugin); + g_signal_connect (priv->client, "device-removed", + G_CALLBACK (gs_plugin_fwupd_device_changed_cb), plugin); + g_signal_connect (priv->client, "device-changed", + G_CALLBACK (gs_plugin_fwupd_device_changed_cb), plugin); + g_signal_connect (priv->client, "notify::percentage", + G_CALLBACK (gs_plugin_fwupd_notify_percentage_cb), plugin); + g_signal_connect (priv->client, "notify::status", + G_CALLBACK (gs_plugin_fwupd_notify_status_cb), plugin); + return TRUE; +} + +static GsApp * +gs_plugin_fwupd_new_app_from_device (GsPlugin *plugin, FwupdDevice *dev) +{ + FwupdRelease *rel = fwupd_device_get_release_default (dev); + GsApp *app; + g_autofree gchar *id = NULL; + g_autoptr(AsIcon) icon = NULL; + + /* older versions of fwups didn't record this for historical devices */ + if (fwupd_release_get_appstream_id (rel) == NULL) + return NULL; + + /* get from cache */ + id = as_utils_unique_id_build (AS_APP_SCOPE_SYSTEM, + AS_BUNDLE_KIND_UNKNOWN, + NULL, /* origin */ + AS_APP_KIND_FIRMWARE, + fwupd_release_get_appstream_id (rel), + NULL); + app = gs_plugin_cache_lookup (plugin, id); + if (app == NULL) { + app = gs_app_new (id); + gs_plugin_cache_add (plugin, id, app); + } + + /* default stuff */ + gs_app_set_kind (app, AS_APP_KIND_FIRMWARE); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_CABINET); + gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE); + gs_app_add_quirk (app, GS_APP_QUIRK_DO_NOT_AUTO_UPDATE); + gs_app_set_management_plugin (app, "fwupd"); + gs_app_add_category (app, "System"); + gs_fwupd_app_set_device_id (app, fwupd_device_get_id (dev)); + + /* create icon */ + icon = as_icon_new (); + as_icon_set_kind (icon, AS_ICON_KIND_STOCK); + as_icon_set_name (icon, "application-x-firmware"); + gs_app_add_icon (app, icon); + gs_fwupd_app_set_from_device (app, dev); + gs_fwupd_app_set_from_release (app, rel); + + if (fwupd_release_get_appstream_id (rel) != NULL) + gs_app_set_id (app, fwupd_release_get_appstream_id (rel)); + + /* the same as we have already */ + if (g_strcmp0 (fwupd_device_get_version (dev), + fwupd_release_get_version (rel)) == 0) { + g_warning ("same firmware version as installed"); + } + + return app; +} + +static gchar * +gs_plugin_fwupd_build_device_id (FwupdDevice *dev) +{ + g_autofree gchar *tmp = g_strdup (fwupd_device_get_id (dev)); + g_strdelimit (tmp, "/", '_'); + return g_strdup_printf ("org.fwupd.%s.device", tmp); +} + +static GsApp * +gs_plugin_fwupd_new_app_from_device_raw (GsPlugin *plugin, FwupdDevice *device) +{ + GPtrArray *icons; + g_autofree gchar *id = NULL; + g_autoptr(GsApp) app = NULL; + + /* create a GsApp based on the device, not the release */ + id = gs_plugin_fwupd_build_device_id (device); + app = gs_app_new (id); + gs_app_set_kind (app, AS_APP_KIND_FIRMWARE); + gs_app_set_scope (app, AS_APP_SCOPE_SYSTEM); + gs_app_set_state (app, AS_APP_STATE_INSTALLED); + gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE); + gs_app_add_quirk (app, GS_APP_QUIRK_DO_NOT_AUTO_UPDATE); + gs_app_set_version (app, fwupd_device_get_version (device)); + gs_app_set_name (app, GS_APP_QUALITY_LOWEST, fwupd_device_get_name (device)); + gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, fwupd_device_get_summary (device)); + gs_app_set_description (app, GS_APP_QUALITY_LOWEST, fwupd_device_get_description (device)); + gs_app_set_origin (app, fwupd_device_get_vendor (device)); + gs_fwupd_app_set_device_id (app, fwupd_device_get_id (device)); + gs_app_set_management_plugin (app, "fwupd"); + + /* create icon */ + icons = fwupd_device_get_icons (device); + for (guint j = 0; j < icons->len; j++) { + const gchar *icon = g_ptr_array_index (icons, j); + g_autoptr(AsIcon) icon_tmp = as_icon_new (); + if (g_str_has_prefix (icon, "/")) { + as_icon_set_kind (icon_tmp, AS_ICON_KIND_LOCAL); + as_icon_set_filename (icon_tmp, icon); + } else { + as_icon_set_kind (icon_tmp, AS_ICON_KIND_STOCK); + as_icon_set_name (icon_tmp, icon); + } + gs_app_add_icon (app, icon_tmp); + } + return g_steal_pointer (&app); +} + +static GsApp * +gs_plugin_fwupd_new_app (GsPlugin *plugin, FwupdDevice *dev, GError **error) +{ + FwupdRelease *rel = fwupd_device_get_release_default (dev); + GPtrArray *checksums; + const gchar *update_uri; + g_autofree gchar *basename = NULL; + g_autofree gchar *filename_cache = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GsApp) app = NULL; + + /* update unsupported */ + app = gs_plugin_fwupd_new_app_from_device (plugin, dev); + if (gs_app_get_state (app) != AS_APP_STATE_UPDATABLE_LIVE) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "%s [%s] cannot be updated", + gs_app_get_name (app), gs_app_get_id (app)); + return NULL; + } + + /* some missing */ + if (gs_app_get_id (app) == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "fwupd: No id for firmware"); + return NULL; + } + if (gs_app_get_version (app) == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "fwupd: No version! for %s!", gs_app_get_id (app)); + return NULL; + } + if (gs_app_get_update_version (app) == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "fwupd: No update-version! for %s!", gs_app_get_id (app)); + return NULL; + } + checksums = fwupd_release_get_checksums (rel); + if (checksums->len == 0) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NO_SECURITY, + "%s [%s] (%s) has no checksums, ignoring as unsafe", + gs_app_get_name (app), + gs_app_get_id (app), + gs_app_get_update_version (app)); + return NULL; + } + update_uri = fwupd_release_get_uri (rel); + if (update_uri == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "no location available for %s [%s]", + gs_app_get_name (app), gs_app_get_id (app)); + return NULL; + } + + /* does the firmware already exist in the cache? */ + basename = g_path_get_basename (update_uri); + filename_cache = gs_utils_get_cache_filename ("fwupd", + basename, + GS_UTILS_CACHE_FLAG_NONE, + error); + if (filename_cache == NULL) + return NULL; + + /* delete the file if the checksum does not match */ + if (g_file_test (filename_cache, G_FILE_TEST_EXISTS)) { + const gchar *checksum_tmp = NULL; + g_autofree gchar *checksum = NULL; + + /* we can migrate to something better than SHA1 when the LVFS + * starts producing metadata with multiple hash types */ + checksum_tmp = fwupd_checksum_get_by_kind (checksums, + G_CHECKSUM_SHA1); + if (checksum_tmp == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "No valid checksum for %s", + filename_cache); + } + checksum = gs_plugin_fwupd_get_file_checksum (filename_cache, + G_CHECKSUM_SHA1, + error); + if (checksum == NULL) + return NULL; + if (g_strcmp0 (checksum_tmp, checksum) != 0) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "%s does not match checksum, expected %s got %s", + filename_cache, checksum_tmp, checksum); + g_unlink (filename_cache); + return NULL; + } + } + + /* already downloaded, so overwrite */ + if (g_file_test (filename_cache, G_FILE_TEST_EXISTS)) + gs_app_set_size_download (app, 0); + + /* actually add the application */ + file = g_file_new_for_path (filename_cache); + gs_app_set_local_file (app, file); + return g_steal_pointer (&app); +} + +gboolean +gs_plugin_add_updates_historical (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GError) error_local = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(FwupdDevice) dev = NULL; + + /* get historical updates */ + dev = fwupd_client_get_results (priv->client, + FWUPD_DEVICE_ID_ANY, + cancellable, + &error_local); + if (dev == NULL) { + if (g_error_matches (error_local, + FWUPD_ERROR, + FWUPD_ERROR_NOTHING_TO_DO)) + return TRUE; + if (g_error_matches (error_local, + FWUPD_ERROR, + FWUPD_ERROR_NOT_FOUND)) + return TRUE; + g_propagate_error (error, g_steal_pointer (&error_local)); + gs_plugin_fwupd_error_convert (error); + return FALSE; + } + + /* parse */ + app = gs_plugin_fwupd_new_app_from_device (plugin, dev); + if (app == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED, + "failed to build result for %s", + fwupd_device_get_id (dev)); + return FALSE; + } + gs_app_list_add (list, app); + return TRUE; +} + +gboolean +gs_plugin_add_updates (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GError) error_local = NULL; + g_autoptr(GPtrArray) devices = NULL; + + /* get current list of updates */ + devices = fwupd_client_get_devices (priv->client, cancellable, &error_local); + if (devices == NULL) { + if (g_error_matches (error_local, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO) || + g_error_matches (error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED) || + g_error_matches (error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { + g_debug ("no devices (%s)", error_local->message); + return TRUE; + } + g_propagate_error (error, g_steal_pointer (&error_local)); + gs_plugin_fwupd_error_convert (error); + return FALSE; + } + for (guint i = 0; i < devices->len; i++) { + FwupdDevice *dev = g_ptr_array_index (devices, i); + FwupdRelease *rel_newest; + g_autoptr(GError) error_local2 = NULL; + g_autoptr(GPtrArray) rels = NULL; + g_autoptr(GsApp) app = NULL; + + /* locked device that needs unlocking */ + if (fwupd_device_has_flag (dev, FWUPD_DEVICE_FLAG_LOCKED)) { + app = gs_plugin_fwupd_new_app_from_device_raw (plugin, dev); + gs_fwupd_app_set_is_locked (app, TRUE); + gs_app_list_add (list, app); + continue; + } + + /* not going to have results, so save a D-Bus round-trip */ + if (!fwupd_device_has_flag (dev, FWUPD_DEVICE_FLAG_SUPPORTED)) + continue; + + /* get the releases for this device and filter for validity */ + rels = fwupd_client_get_upgrades (priv->client, + fwupd_device_get_id (dev), + cancellable, &error_local2); + if (rels == NULL) { + if (g_error_matches (error_local2, + FWUPD_ERROR, + FWUPD_ERROR_NOTHING_TO_DO)) { + g_debug ("no updates for %s", fwupd_device_get_id (dev)); + continue; + } + if (g_error_matches (error_local2, + FWUPD_ERROR, + FWUPD_ERROR_NOT_SUPPORTED)) { + g_debug ("not supported for %s", fwupd_device_get_id (dev)); + continue; + } + g_warning ("failed to get upgrades for %s: %s]", + fwupd_device_get_id (dev), + error_local2->message); + continue; + } + + /* normal device update */ + rel_newest = g_ptr_array_index (rels, 0); + fwupd_device_add_release (dev, rel_newest); + app = gs_plugin_fwupd_new_app (plugin, dev, &error_local2); + if (app == NULL) { + g_debug ("%s", error_local2->message); + continue; + } + + /* add update descriptions for all releases inbetween */ + if (rels->len > 1) { + g_autoptr(GString) update_desc = g_string_new (NULL); + for (guint j = 0; j < rels->len; j++) { + FwupdRelease *rel = g_ptr_array_index (rels, j); + g_autofree gchar *desc = NULL; + if (fwupd_release_get_description (rel) == NULL) + continue; + desc = as_markup_convert (fwupd_release_get_description (rel), + AS_MARKUP_CONVERT_FORMAT_SIMPLE, + NULL); + if (desc == NULL) + continue; + g_string_append_printf (update_desc, + "Version %s:\n%s\n\n", + fwupd_release_get_version (rel), + desc); + } + if (update_desc->len > 2) { + g_string_truncate (update_desc, update_desc->len - 2); + gs_app_set_update_details (app, update_desc->str); + } + } + gs_app_list_add (list, app); + } + return TRUE; +} + +static gboolean +gs_plugin_fwupd_refresh_remote (GsPlugin *plugin, + FwupdRemote *remote, + guint cache_age, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + GChecksumType checksum_kind; + const gchar *url_sig = NULL; + const gchar *url = NULL; + g_autoptr(GError) error_local = NULL; + g_autofree gchar *basename = NULL; + g_autofree gchar *basename_sig = NULL; + g_autofree gchar *cache_id = NULL; + g_autofree gchar *checksum = NULL; + g_autofree gchar *filename = NULL; + g_autofree gchar *filename_sig = NULL; + g_autoptr(GBytes) data = NULL; + g_autoptr(GsApp) app_dl = gs_app_new (gs_plugin_get_name (plugin)); + + /* sanity check */ + if (fwupd_remote_get_filename_cache_sig (remote) == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED, + "remote %s has no cache signature", + fwupd_remote_get_id (remote)); + return FALSE; + } + + /* check cache age */ + if (cache_age > 0) { + guint64 age = fwupd_remote_get_age (remote); + guint tmp = age < G_MAXUINT ? (guint) age : G_MAXUINT; + if (tmp < cache_age) { + g_debug ("fwupd remote is only %u seconds old, so ignoring refresh", + tmp); + return TRUE; + } + } + + /* download the signature first, it's smaller */ + cache_id = g_strdup_printf ("fwupd/remotes.d/%s", fwupd_remote_get_id (remote)); + basename_sig = g_path_get_basename (fwupd_remote_get_filename_cache_sig (remote)); + filename_sig = gs_utils_get_cache_filename (cache_id, basename_sig, + GS_UTILS_CACHE_FLAG_WRITEABLE, + error); + + /* download the signature first, it's smaller */ + url_sig = fwupd_remote_get_metadata_uri_sig (remote); + gs_app_set_summary_missing (app_dl, + /* TRANSLATORS: status text when downloading */ + _("Downloading firmware update signature…")); + data = gs_plugin_download_data (plugin, app_dl, url_sig, cancellable, error); + if (data == NULL) { + gs_utils_error_add_origin_id (error, priv->cached_origin); + return FALSE; + } + + /* is the signature hash the same as we had before? */ + checksum_kind = fwupd_checksum_guess_kind (fwupd_remote_get_checksum (remote)); + checksum = g_compute_checksum_for_data (checksum_kind, + (const guchar *) g_bytes_get_data (data, NULL), + g_bytes_get_size (data)); + if (g_strcmp0 (checksum, fwupd_remote_get_checksum (remote)) == 0) { + g_debug ("signature of %s is unchanged", url_sig); + return TRUE; + } + + /* save to a file */ + g_debug ("saving new remote signature to %s:", filename_sig); + if (!g_file_set_contents (filename_sig, + g_bytes_get_data (data, NULL), + (guint) g_bytes_get_size (data), + &error_local)) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_WRITE_FAILED, + "Failed to save firmware signature: %s", + error_local->message); + return FALSE; + } + + /* download the payload and save to file */ + basename = g_path_get_basename (fwupd_remote_get_filename_cache (remote)); + filename = gs_utils_get_cache_filename (cache_id, basename, + GS_UTILS_CACHE_FLAG_WRITEABLE, + error); + if (filename == NULL) + return FALSE; + g_debug ("saving new firmware metadata to %s:", filename); + gs_app_set_summary_missing (app_dl, + /* TRANSLATORS: status text when downloading */ + _("Downloading firmware update metadata…")); + url = fwupd_remote_get_metadata_uri (remote); + if (!gs_plugin_download_file (plugin, app_dl, url, filename, + cancellable, error)) { + gs_utils_error_add_origin_id (error, priv->cached_origin); + return FALSE; + } + + /* phew, lets send all this to fwupd */ + if (!fwupd_client_update_metadata (priv->client, + fwupd_remote_get_id (remote), + filename, + filename_sig, + cancellable, + error)) { + gs_plugin_fwupd_error_convert (error); + return FALSE; + } + return TRUE; +} + +gboolean +gs_plugin_refresh (GsPlugin *plugin, + guint cache_age, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GError) error_local = NULL; + g_autoptr(GPtrArray) remotes = NULL; + + /* get the list of enabled remotes */ + remotes = fwupd_client_get_remotes (priv->client, cancellable, &error_local); + if (remotes == NULL) { + g_debug ("No remotes found: %s", error_local ? error_local->message : "Unknown error"); + if (g_error_matches (error_local, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO) || + g_error_matches (error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED) || + g_error_matches (error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) + return TRUE; + g_propagate_error (error, g_steal_pointer (&error_local)); + gs_plugin_fwupd_error_convert (error); + return FALSE; + } + for (guint i = 0; i < remotes->len; i++) { + FwupdRemote *remote = g_ptr_array_index (remotes, i); + if (!fwupd_remote_get_enabled (remote)) + continue; + if (fwupd_remote_get_kind (remote) == FWUPD_REMOTE_KIND_LOCAL) + continue; + if (!gs_plugin_fwupd_refresh_remote (plugin, remote, cache_age, + cancellable, error)) + return FALSE; + } + return TRUE; +} + +static gboolean +gs_plugin_fwupd_install (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + const gchar *device_id; + FwupdInstallFlags install_flags = 0; + GFile *local_file; + g_autofree gchar *filename = NULL; + gboolean downloaded_to_cache = FALSE; + g_autoptr(FwupdDevice) dev = NULL; + g_autoptr(GError) error_local = NULL; + + /* not set */ + local_file = gs_app_get_local_file (app); + if (local_file == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED, + "not enough data for fwupd %s", + filename); + return FALSE; + } + + /* file does not yet exist */ + filename = g_file_get_path (local_file); + if (!g_file_query_exists (local_file, cancellable)) { + const gchar *uri = gs_fwupd_app_get_update_uri (app); + gs_app_set_state (app, AS_APP_STATE_INSTALLING); + if (!gs_plugin_download_file (plugin, app, uri, filename, + cancellable, error)) + return FALSE; + downloaded_to_cache = TRUE; + } + + /* limit to single device? */ + device_id = gs_fwupd_app_get_device_id (app); + if (device_id == NULL) + device_id = FWUPD_DEVICE_ID_ANY; + + /* set the last object */ + g_set_object (&priv->app_current, app); + + /* only offline supported */ + if (gs_app_get_metadata_item (app, "fwupd::OnlyOffline") != NULL) + install_flags |= FWUPD_INSTALL_FLAG_OFFLINE; + + gs_app_set_state (app, AS_APP_STATE_INSTALLING); + if (!fwupd_client_install (priv->client, device_id, + filename, install_flags, + cancellable, error)) { + gs_plugin_fwupd_error_convert (error); + gs_app_set_state_recover (app); + return FALSE; + } + + /* delete the file from the cache */ + gs_app_set_state (app, AS_APP_STATE_INSTALLED); + if (downloaded_to_cache) { + if (!g_file_delete (local_file, cancellable, error)) + return FALSE; + } + + /* does the device have an update message */ + dev = fwupd_client_get_device_by_id (priv->client, device_id, + cancellable, &error_local); + if (dev == NULL) { + /* NOTE: this is probably entirely fine; some devices do not + * re-enumerate until replugged manually or the machine is + * rebooted -- and the metadata to know that is only available + * in a too-new-to-depend-on fwupd version */ + g_debug ("failed to find device after install: %s", error_local->message); + } else { + if (fwupd_device_get_update_message (dev) != NULL) { + g_autoptr(AsScreenshot) ss = as_screenshot_new (); + +#if FWUPD_CHECK_VERSION(1,4,5) + /* image is optional */ + if (fwupd_device_get_update_image (dev) != NULL) { + g_autoptr(AsImage) im = as_image_new (); + as_image_set_kind (im, AS_IMAGE_KIND_SOURCE); + as_image_set_url (im, fwupd_device_get_update_image (dev)); + as_screenshot_add_image (ss, im); + } +#endif + + /* caption is required */ + as_screenshot_set_kind (ss, AS_SCREENSHOT_KIND_DEFAULT); + as_screenshot_set_caption (ss, NULL, fwupd_device_get_update_message (dev)); + gs_app_set_action_screenshot (app, ss); + + /* require the dialog */ + gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_USER_ACTION); + } + } + + /* success */ + return TRUE; +} + +static gboolean +gs_plugin_fwupd_modify_source (GsPlugin *plugin, GsApp *app, gboolean enabled, + GCancellable *cancellable, GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + const gchar *remote_id = gs_app_get_metadata_item (app, "fwupd::remote-id"); + if (remote_id == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED, + "not enough data for fwupd %s", + gs_app_get_unique_id (app)); + return FALSE; + } + gs_app_set_state (app, enabled ? + AS_APP_STATE_INSTALLING : AS_APP_STATE_REMOVING); + if (!fwupd_client_modify_remote (priv->client, + remote_id, + "Enabled", + enabled ? "true" : "false", + cancellable, + error)) { + gs_app_set_state_recover (app); + return FALSE; + } + gs_app_set_state (app, enabled ? + AS_APP_STATE_INSTALLED : AS_APP_STATE_AVAILABLE); + return TRUE; +} + +gboolean +gs_plugin_app_install (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + /* only process this app if was created by this plugin */ + if (g_strcmp0 (gs_app_get_management_plugin (app), + gs_plugin_get_name (plugin)) != 0) + return TRUE; + + /* source -> remote */ + if (gs_app_get_kind (app) == AS_APP_KIND_SOURCE) { + return gs_plugin_fwupd_modify_source (plugin, app, TRUE, + cancellable, error); + } + + /* firmware */ + return gs_plugin_fwupd_install (plugin, app, cancellable, error); +} + +gboolean +gs_plugin_app_remove (GsPlugin *plugin, GsApp *app, + GCancellable *cancellable, GError **error) +{ + /* only process this app if was created by this plugin */ + if (g_strcmp0 (gs_app_get_management_plugin (app), + gs_plugin_get_name (plugin)) != 0) + return TRUE; + + /* source -> remote */ + return gs_plugin_fwupd_modify_source (plugin, app, FALSE, cancellable, error); +} + +gboolean +gs_plugin_download_app (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GFile *local_file; + g_autofree gchar *filename = NULL; + + /* only process this app if was created by this plugin */ + if (g_strcmp0 (gs_app_get_management_plugin (app), + gs_plugin_get_name (plugin)) != 0) + return TRUE; + + /* not set */ + local_file = gs_app_get_local_file (app); + if (local_file == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED, + "not enough data for fwupd %s", + filename); + return FALSE; + } + + /* file does not yet exist */ + filename = g_file_get_path (local_file); + if (!g_file_query_exists (local_file, cancellable)) { + const gchar *uri = gs_fwupd_app_get_update_uri (app); + + if (!gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)) { + g_autoptr(GError) error_local = NULL; + + if (!gs_metered_block_app_on_download_scheduler (app, cancellable, &error_local)) { + g_warning ("Failed to block on download scheduler: %s", + error_local->message); + g_clear_error (&error_local); + } + } + + if (!gs_plugin_download_file (plugin, app, uri, filename, + cancellable, error)) + return FALSE; + } + gs_app_set_size_download (app, 0); + return TRUE; +} + +gboolean +gs_plugin_update_app (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + /* only process this app if was created by this plugin */ + if (g_strcmp0 (gs_app_get_management_plugin (app), + gs_plugin_get_name (plugin)) != 0) + return TRUE; + + /* locked devices need unlocking, rather than installing */ + if (gs_fwupd_app_get_is_locked (app)) { + const gchar *device_id; + device_id = gs_fwupd_app_get_device_id (app); + if (device_id == NULL) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "not enough data for fwupd unlock"); + return FALSE; + } + if (!fwupd_client_unlock (priv->client, device_id, + cancellable, error)) { + gs_plugin_fwupd_error_convert (error); + return FALSE; + } + return TRUE; + } + + /* update means install */ + if (!gs_plugin_fwupd_install (plugin, app, cancellable, error)) { + gs_plugin_fwupd_error_convert (error); + return FALSE; + } + return TRUE; +} + +gboolean +gs_plugin_file_to_app (GsPlugin *plugin, + GsAppList *list, + GFile *file, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autofree gchar *content_type = NULL; + g_autofree gchar *filename = NULL; + g_autoptr(GPtrArray) devices = NULL; + const gchar *mimetypes[] = { + "application/vnd.ms-cab-compressed", + NULL }; + + /* does this match any of the mimetypes we support */ + content_type = gs_utils_get_content_type (file, cancellable, error); + if (content_type == NULL) + return FALSE; + if (!g_strv_contains (mimetypes, content_type)) + return TRUE; + + /* get results */ + filename = g_file_get_path (file); + devices = fwupd_client_get_details (priv->client, + filename, + cancellable, + error); + if (devices == NULL) { + gs_plugin_fwupd_error_convert (error); + return FALSE; + } + for (guint i = 0; i < devices->len; i++) { + FwupdDevice *dev = g_ptr_array_index (devices, i); + g_autoptr(GsApp) app = NULL; + + /* create each app */ + app = gs_plugin_fwupd_new_app_from_device (plugin, dev); + + /* we *might* have no update view for local files */ + gs_app_set_version (app, gs_app_get_update_version (app)); + gs_app_set_description (app, GS_APP_QUALITY_LOWEST, + gs_app_get_update_details (app)); + gs_app_list_add (list, app); + } + return TRUE; +} + +gboolean +gs_plugin_add_sources (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GPtrArray) remotes = NULL; + + /* find all remotes */ + remotes = fwupd_client_get_remotes (priv->client, cancellable, error); + if (remotes == NULL) + return FALSE; + for (guint i = 0; i < remotes->len; i++) { + FwupdRemote *remote = g_ptr_array_index (remotes, i); + g_autofree gchar *id = NULL; + g_autoptr(GsApp) app = NULL; + + /* ignore these, they're built in */ + if (fwupd_remote_get_kind (remote) != FWUPD_REMOTE_KIND_DOWNLOAD) + continue; + + /* create something that we can use to enable/disable */ + id = g_strdup_printf ("org.fwupd.%s.remote", fwupd_remote_get_id (remote)); + app = gs_app_new (id); + gs_app_set_kind (app, AS_APP_KIND_SOURCE); + gs_app_set_scope (app, AS_APP_SCOPE_SYSTEM); + gs_app_set_state (app, fwupd_remote_get_enabled (remote) ? + AS_APP_STATE_INSTALLED : AS_APP_STATE_AVAILABLE); + gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE); + gs_app_set_name (app, GS_APP_QUALITY_LOWEST, + fwupd_remote_get_title (remote)); +#if FWUPD_CHECK_VERSION(1,0,7) + gs_app_set_agreement (app, fwupd_remote_get_agreement (remote)); +#endif + gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, + fwupd_remote_get_metadata_uri (remote)); + gs_app_set_metadata (app, "fwupd::remote-id", + fwupd_remote_get_id (remote)); + gs_app_set_management_plugin (app, "fwupd"); + gs_app_list_add (list, app); + } + return TRUE; +} diff --git a/plugins/fwupd/gs-self-test.c b/plugins/fwupd/gs-self-test.c new file mode 100644 index 0000000..ca5fcb8 --- /dev/null +++ b/plugins/fwupd/gs-self-test.c @@ -0,0 +1,105 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2017 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include "gnome-software-private.h" + +#include "gs-test.h" + +static void +gs_plugins_fwupd_func (GsPluginLoader *plugin_loader) +{ + g_autofree gchar *fn = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* no fwupd, abort */ + if (!gs_plugin_loader_get_enabled (plugin_loader, "fwupd")) { + g_test_skip ("not enabled"); + return; + } + + /* load local file */ + fn = gs_test_get_filename (TESTDATADIR, "chiron-0.2.cab"); + g_assert (fn != NULL); + file = g_file_new_for_path (fn); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP, + "file", file, + NULL); + app = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (app != NULL); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_APP_KIND_FIRMWARE); + g_assert (gs_app_get_license (app) != NULL); + g_assert (gs_app_has_category (app, "System")); + g_assert_cmpstr (gs_app_get_id (app), ==, "com.test.chiron.firmware"); + g_assert_cmpstr (gs_app_get_url (app, AS_URL_KIND_HOMEPAGE), ==, "http://127.0.0.1/"); + g_assert_cmpstr (gs_app_get_name (app), ==, "Chiron"); + g_assert_cmpstr (gs_app_get_summary (app), ==, "Single line synopsis"); + g_assert_cmpstr (gs_app_get_version (app), ==, "0.2"); + g_assert_cmpint ((gint64) gs_app_get_size_download (app), ==, 32784); + g_assert_cmpstr (gs_app_get_description (app), ==, + "This is the first paragraph in the example " + "cab file.\n\nThis is the second paragraph."); + g_assert_cmpstr (gs_app_get_update_details (app), ==, + "Latest firmware release."); + + /* seems wrong, but this is only set if the update is available */ + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_UNKNOWN); +} + +int +main (int argc, char **argv) +{ + gboolean ret; + g_autoptr(GError) error = NULL; + g_autoptr(GsPluginLoader) plugin_loader = NULL; + const gchar *allowlist[] = { + "fwupd", + NULL + }; + + /* While we use %G_TEST_OPTION_ISOLATE_DIRS to create temporary directories + * for each of the tests, we want to use the system MIME registry, assuming + * that it exists and correctly has shared-mime-info installed. */ +#if GLIB_CHECK_VERSION(2, 60, 0) + g_content_type_set_mime_dirs (NULL); +#endif + + 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); + + /* 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/fwupd", + plugin_loader, + (GTestDataFunc) gs_plugins_fwupd_func); + + return g_test_run (); +} diff --git a/plugins/fwupd/meson.build b/plugins/fwupd/meson.build new file mode 100644 index 0000000..7c89add --- /dev/null +++ b/plugins/fwupd/meson.build @@ -0,0 +1,60 @@ +cargs = ['-DG_LOG_DOMAIN="GsPluginFwupd"'] +cargs += ['-DLOCALPLUGINDIR="' + meson.current_build_dir() + '"'] +deps = [ + plugin_libs, + fwupd, +] + +if get_option('mogwai') + deps += [mogwai_schedule_client] +endif + +shared_module( + 'gs_plugin_fwupd', + sources : [ + 'gs-fwupd-app.c', + 'gs-plugin-fwupd.c', + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : deps, + link_with : [ + libgnomesoftware + ] +) +metainfo = 'org.gnome.Software.Plugin.Fwupd.metainfo.xml' + +i18n.merge_file( + input: metainfo + '.in', + output: metainfo, + type: 'xml', + po_dir: join_paths(meson.source_root(), 'po'), + install: true, + install_dir: join_paths(get_option('datadir'), 'metainfo') +) + +if get_option('tests') + cargs += ['-DTESTDATADIR="' + join_paths(meson.current_source_dir(), 'tests') + '"'] + e = executable( + 'gs-self-test-fwupd', + compiled_schemas, + sources : [ + 'gs-self-test.c' + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + dependencies : deps, + link_with : [ + libgnomesoftware + ], + c_args : cargs, + ) + test('gs-self-test-fwupd', e, suite: ['plugins', 'fwupd'], env: test_env) +endif diff --git a/plugins/fwupd/org.gnome.Software.Plugin.Fwupd.metainfo.xml.in b/plugins/fwupd/org.gnome.Software.Plugin.Fwupd.metainfo.xml.in new file mode 100644 index 0000000..3f2067d --- /dev/null +++ b/plugins/fwupd/org.gnome.Software.Plugin.Fwupd.metainfo.xml.in @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright 2013-2016 Richard Hughes <richard@hughsie.com> --> +<component type="addon"> + <id>org.gnome.Software.Plugin.Fwupd</id> + <extends>org.gnome.Software.desktop</extends> + <name>Firmware Upgrade Support</name> + <summary>Provides support for firmware upgrades</summary> + <url type="homepage">http://www.fwupd.org</url> + <metadata_license>CC0-1.0</metadata_license> + <project_license>GPL-2.0+</project_license> + <update_contact>richard_at_hughsie.com</update_contact> +</component> diff --git a/plugins/fwupd/tests/build-cab.sh b/plugins/fwupd/tests/build-cab.sh new file mode 100755 index 0000000..ea7bed6 --- /dev/null +++ b/plugins/fwupd/tests/build-cab.sh @@ -0,0 +1,4 @@ +gcab --create chiron-0.2.cab \ + firmware.dfu \ + firmware.dfu.asc \ + firmware.metainfo.xml diff --git a/plugins/fwupd/tests/chiron-0.2.cab b/plugins/fwupd/tests/chiron-0.2.cab Binary files differnew file mode 100644 index 0000000..6618361 --- /dev/null +++ b/plugins/fwupd/tests/chiron-0.2.cab diff --git a/plugins/fwupd/tests/firmware.dfu b/plugins/fwupd/tests/firmware.dfu Binary files differnew file mode 100644 index 0000000..50f00c0 --- /dev/null +++ b/plugins/fwupd/tests/firmware.dfu diff --git a/plugins/fwupd/tests/firmware.dfu.asc b/plugins/fwupd/tests/firmware.dfu.asc new file mode 100644 index 0000000..0ea79a7 --- /dev/null +++ b/plugins/fwupd/tests/firmware.dfu.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- +Version: GnuPG v2.0.14 (GNU/Linux) + +iQEcBAABAgAGBQJWUy6EAAoJEEim2A5FOLrCxokIAJJtLVkuXZHgEu2C2Eq9jGrR +FZ9/z9XtsMgs33teLdmYUAQwvnNIbtIb6z7JViP8llCREP8y2fH+1OjrOOdtuS/A +bIJ0r40c9wYeH97ZcXBdHZiYVEFO+etbMBUg5ifuRO5VPjD9H1NqL05Wx9kUg/1T +a1fwgHopXR0T4jYcg5aijp3mdgfmg4boIklDaRV/g2c93W+0VhDZ2h5sKwBxxlFS +TrptclTMCvRYmVvL1CDOsBtgzu3jGo03wV9rcnSKzeBWvINcvlRLdS0ejlPaRYDK +MUY4MBVz3fDW1vFsqLpU80XMOYk0bxtQqQ2MsrlXWp9qazB+A6mC7kOnJQfx0yI= +=A3W8 +-----END PGP SIGNATURE----- diff --git a/plugins/fwupd/tests/firmware.metainfo.xml b/plugins/fwupd/tests/firmware.metainfo.xml new file mode 100644 index 0000000..d942fd8 --- /dev/null +++ b/plugins/fwupd/tests/firmware.metainfo.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright 2016 Richard Hughes <richard@hughsie.com> --> +<component type="firmware"> + <id>com.test.chiron.firmware</id> + <name>Chiron</name> + <summary>Single line synopsis</summary> + <description> + <p> + This is the first paragraph in the example cab file. + </p> + <p> + This is the second paragraph. + </p> + </description> + <provides> + <firmware type="flashed">fd9f37b4-36fb-5245-86a8-4d5993bb153b</firmware> + </provides> + <url type="homepage">http://127.0.0.1/</url> + <metadata_license>CC0-1.0</metadata_license> + <project_license>GPL-2.0+</project_license> + <developer_name>ACME Corp</developer_name> + <releases> + <release urgency="medium" version="0.2" timestamp="1447353015"> + <checksum target="content" filename="firmware.dfu"/> + <description> + <p>Latest firmware release.</p> + </description> + </release> + </releases> +</component> diff --git a/plugins/malcontent/gs-plugin-malcontent.c b/plugins/malcontent/gs-plugin-malcontent.c new file mode 100644 index 0000000..d474168 --- /dev/null +++ b/plugins/malcontent/gs-plugin-malcontent.c @@ -0,0 +1,343 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2018-2019 Endless Mobile + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <glib/gi18n.h> +#include <gnome-software.h> +#include <libmalcontent/malcontent.h> +#include <string.h> +#include <math.h> + +/* + * SECTION: + * Adds the %GS_APP_QUIRK_PARENTAL_FILTER and + * %GS_APP_QUIRK_PARENTAL_NOT_LAUNCHABLE quirks to applications if they + * contravene the effective user’s current parental controls policy. + * + * Specifically, %GS_APP_QUIRK_PARENTAL_FILTER will be added if an app’s OARS + * rating is too extreme for the current parental controls OARS policy. + * %GS_APP_QUIRK_PARENTAL_NOT_LAUNCHABLE will be added if the app is listed on + * the current parental controls blocklist. + * + * Parental controls policy is loaded using libmalcontent. + * + * This plugin is ordered after flatpak and appstream as it uses OARS data from + * them. + * + * Limiting access to applications by not allowing them to be launched by + * gnome-software is only one part of a wider approach to parental controls. + * In order to guarantee users do not have access to applications they shouldn’t + * have access to, an LSM (such as AppArmor) needs to be used. That complements, + * rather than substitutes for, filtering in user visible UIs. + */ + +struct GsPluginData { + GMutex mutex; /* protects @app_filter **/ + MctManager *manager; /* (owned) */ + gulong manager_app_filter_changed_id; + MctAppFilter *app_filter; /* (mutex) (owned) (nullable) */ +}; + +/* Convert an #MctAppFilterOarsValue to an #AsContentRatingValue. This is + * actually a trivial cast, since the types are defined the same; but throw in + * a static assertion to be sure. */ +static AsContentRatingValue +convert_app_filter_oars_value (MctAppFilterOarsValue filter_value) +{ + G_STATIC_ASSERT (AS_CONTENT_RATING_VALUE_LAST == MCT_APP_FILTER_OARS_VALUE_INTENSE + 1); + + return (AsContentRatingValue) filter_value; +} + +static gboolean +app_is_expected_to_have_content_rating (GsApp *app) +{ + if (gs_app_has_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE)) + return FALSE; + + switch (gs_app_get_kind (app)) { + case AS_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: + return FALSE; + case AS_APP_KIND_UNKNOWN: + case AS_APP_KIND_DESKTOP: + case AS_APP_KIND_WEB_APP: + case AS_APP_KIND_SHELL_EXTENSION: + case AS_APP_KIND_CONSOLE: + default: + break; + } + + return TRUE; +} + +/* Check whether the OARS rating for @app is as, or less, extreme than the + * user’s preferences in @app_filter. If so (i.e. if the app is suitable for + * this user to use), return %TRUE; otherwise return %FALSE. + * + * The #AsContentRating in @app may be %NULL if no OARS ratings are provided for + * the app. If so, we have to assume the most restrictive ratings. However, if + * @rating is provided but is empty, we assume that every section in it has + * value %AS_CONTENT_RATING_VALUE_NONE. See + * https://github.com/hughsie/oars/blob/master/specification/oars-1.1.md */ +static gboolean +app_is_content_rating_appropriate (GsApp *app, MctAppFilter *app_filter) +{ + AsContentRating *rating = gs_app_get_content_rating (app); /* (nullable) */ + g_autofree const gchar **oars_sections = mct_app_filter_get_oars_sections (app_filter); + AsContentRatingValue default_rating_value; + + if (rating == NULL && !app_is_expected_to_have_content_rating (app)) { + /* Some apps, such as flatpak runtimes, are not expected to have + * content ratings. */ + return TRUE; + } else if (rating == NULL) { + g_debug ("No OARS ratings provided for ‘%s’: assuming most extreme", + gs_app_get_unique_id (app)); + default_rating_value = AS_CONTENT_RATING_VALUE_INTENSE; + } else { + default_rating_value = AS_CONTENT_RATING_VALUE_NONE; + } + + for (gsize i = 0; oars_sections[i] != NULL; i++) { + AsContentRatingValue rating_value; + MctAppFilterOarsValue filter_value; + + filter_value = mct_app_filter_get_oars_value (app_filter, oars_sections[i]); + + if (rating != NULL) + rating_value = as_content_rating_get_value (rating, oars_sections[i]); + else + rating_value = AS_CONTENT_RATING_VALUE_UNKNOWN; + + if (rating_value == AS_CONTENT_RATING_VALUE_UNKNOWN) + rating_value = default_rating_value; + + if (filter_value == MCT_APP_FILTER_OARS_VALUE_UNKNOWN) + continue; + else if (convert_app_filter_oars_value (filter_value) < rating_value) + return FALSE; + } + + return TRUE; +} + +static gboolean +app_is_parentally_blocklisted (GsApp *app, MctAppFilter *app_filter) +{ + const gchar *desktop_id; + g_autoptr(GAppInfo) appinfo = NULL; + + desktop_id = gs_app_get_id (app); + if (desktop_id == NULL) + return FALSE; + appinfo = G_APP_INFO (gs_utils_get_desktop_app_info (desktop_id)); + if (appinfo == NULL) + return FALSE; + + return !mct_app_filter_is_appinfo_allowed (app_filter, appinfo); +} + +static gboolean +app_set_parental_quirks (GsPlugin *plugin, GsApp *app, MctAppFilter *app_filter) +{ + /* note that both quirks can be set on an app at the same time, and they + * have slightly different meanings */ + gboolean filtered = FALSE; + + /* check the OARS ratings to see if this app should be installable */ + if (!app_is_content_rating_appropriate (app, app_filter)) { + g_debug ("Filtering ‘%s’: app OARS rating is too extreme for this user", + gs_app_get_unique_id (app)); + gs_app_add_quirk (app, GS_APP_QUIRK_PARENTAL_FILTER); + filtered = TRUE; + } else { + gs_app_remove_quirk (app, GS_APP_QUIRK_PARENTAL_FILTER); + } + + /* check the app blocklist to see if this app should be launchable */ + if (app_is_parentally_blocklisted (app, app_filter)) { + g_debug ("Filtering ‘%s’: app is blocklisted for this user", + gs_app_get_unique_id (app)); + gs_app_add_quirk (app, GS_APP_QUIRK_PARENTAL_NOT_LAUNCHABLE); + filtered = TRUE; + } else { + gs_app_remove_quirk (app, GS_APP_QUIRK_PARENTAL_NOT_LAUNCHABLE); + } + + return filtered; +} + +static MctAppFilter * +query_app_filter (GsPlugin *plugin, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + + return mct_manager_get_app_filter (priv->manager, getuid (), + MCT_GET_APP_FILTER_FLAGS_INTERACTIVE, cancellable, + error); +} + +static gboolean +reload_app_filter (GsPlugin *plugin, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(MctAppFilter) new_app_filter = NULL; + g_autoptr(MctAppFilter) old_app_filter = NULL; + + /* Refresh the app filter. This blocks on a D-Bus request. */ + new_app_filter = query_app_filter (plugin, cancellable, error); + + /* on failure, keep the old app filter around since it might be more + * useful than nothing */ + if (new_app_filter == NULL) + return FALSE; + + { + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex); + old_app_filter = g_steal_pointer (&priv->app_filter); + priv->app_filter = g_steal_pointer (&new_app_filter); + } + + return TRUE; +} + +static void +app_filter_changed_cb (MctManager *manager, + guint64 user_id, + gpointer user_data) +{ + GsPlugin *plugin = GS_PLUGIN (user_data); + g_autoptr(GError) error_local = NULL; + + if (user_id != getuid ()) + return; + + /* The user’s app filter has changed, which means that different + * apps could be filtered from before. Reload everything to be + * sure of re-filtering correctly. */ + g_debug ("Reloading due to app filter changing for user %" G_GUINT64_FORMAT, user_id); + if (reload_app_filter (plugin, NULL, &error_local)) + gs_plugin_reload (plugin); + else + g_warning ("Failed to reload changed app filter: %s", error_local->message); +} + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + gs_plugin_alloc_data (plugin, sizeof (GsPluginData)); + + /* need application IDs and content ratings */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "appstream"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "flatpak"); + + /* set plugin name; it’s not a loadable plugin, but this is descriptive and harmless */ + gs_plugin_set_appstream_id (plugin, "org.gnome.Software.Plugin.Malcontent"); +} + +gboolean +gs_plugin_setup (GsPlugin *plugin, GCancellable *cancellable, GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex); + g_autoptr(GDBusConnection) system_bus = NULL; + + system_bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, cancellable, error); + if (system_bus == NULL) + return FALSE; + + priv->manager = mct_manager_new (system_bus); + priv->manager_app_filter_changed_id = g_signal_connect (priv->manager, + "app-filter-changed", + (GCallback) app_filter_changed_cb, + plugin); + priv->app_filter = query_app_filter (plugin, cancellable, error); + + return (priv->app_filter != NULL); +} + +static gboolean +refine_app_locked (GsPlugin *plugin, + GsApp *app, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + + /* not valid */ + if (gs_app_get_id (app) == NULL) + return TRUE; + + /* Filter by various parental filters. The filter can’t be %NULL, + * otherwise setup() would have failed and the plugin would have been + * disabled. */ + g_assert (priv->app_filter != NULL); + + app_set_parental_quirks (plugin, app, priv->app_filter); + + return TRUE; +} + +gboolean +gs_plugin_refine (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex); + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + if (!refine_app_locked (plugin, app, flags, cancellable, error)) + return FALSE; + } + + return TRUE; +} + +gboolean +gs_plugin_refresh (GsPlugin *plugin, + guint cache_age, + GCancellable *cancellable, + GError **error) +{ + return reload_app_filter (plugin, cancellable, error); +} + +void +gs_plugin_destroy (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + + g_clear_pointer (&priv->app_filter, mct_app_filter_unref); + if (priv->manager != NULL && priv->manager_app_filter_changed_id != 0) { + g_signal_handler_disconnect (priv->manager, + priv->manager_app_filter_changed_id); + priv->manager_app_filter_changed_id = 0; + } + g_clear_object (&priv->manager); +}
\ No newline at end of file diff --git a/plugins/malcontent/meson.build b/plugins/malcontent/meson.build new file mode 100644 index 0000000..ea0740a --- /dev/null +++ b/plugins/malcontent/meson.build @@ -0,0 +1,17 @@ +c_args = ['-DG_LOG_DOMAIN="GsPluginMalcontent"'] + +shared_module( + 'gs_plugin_malcontent', + sources : 'gs-plugin-malcontent.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : c_args, + dependencies : [ plugin_libs, malcontent ], + link_with : [ + libgnomesoftware, + ], +)
\ No newline at end of file diff --git a/plugins/meson.build b/plugins/meson.build new file mode 100644 index 0000000..d749b3d --- /dev/null +++ b/plugins/meson.build @@ -0,0 +1,47 @@ +plugin_dir = join_paths(get_option('libdir'), 'gs-plugins-' + gs_plugin_api_version) +plugin_libs = [ + appstream_glib, + gio_unix, + goa, + gtk, + json_glib, + libsoup +] + +subdir('core') +subdir('dpkg') +subdir('dummy') +subdir('fedora-langpacks') +subdir('fedora-pkgdb-collections') + +if get_option('eos_updater') + subdir('eos-updater') +endif +if get_option('flatpak') + subdir('flatpak') +endif +if get_option('fwupd') + subdir('fwupd') +endif +if get_option('gudev') + subdir('modalias') +endif +if get_option('malcontent') + subdir('malcontent') +endif +if get_option('odrs') + subdir('odrs') +endif +if get_option('packagekit') + subdir('packagekit') +endif +subdir('repos') +if get_option('rpm_ostree') + subdir('rpm-ostree') +endif +if get_option('snap') + subdir('snap') +endif +if get_option('external_appstream') + subdir('external-appstream') +endif diff --git a/plugins/modalias/gs-plugin-modalias.c b/plugins/modalias/gs-plugin-modalias.c new file mode 100644 index 0000000..d4e27a2 --- /dev/null +++ b/plugins/modalias/gs-plugin-modalias.c @@ -0,0 +1,151 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <fnmatch.h> +#include <gudev/gudev.h> + +#include <gnome-software.h> + +struct GsPluginData { + GUdevClient *client; + GPtrArray *devices; +}; + +static void +gs_plugin_modalias_uevent_cb (GUdevClient *client, + const gchar *action, + GUdevDevice *device, + GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + if (g_strcmp0 (action, "add") == 0 || + g_strcmp0 (action, "remove") == 0) { + g_debug ("invalidating devices as '%s' sent action '%s'", + g_udev_device_get_sysfs_path (device), + action); + g_ptr_array_set_size (priv->devices, 0); + } +} + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData)); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "appstream"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_BEFORE, "icons"); + priv->devices = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + priv->client = g_udev_client_new (NULL); + g_signal_connect (priv->client, "uevent", + G_CALLBACK (gs_plugin_modalias_uevent_cb), plugin); +} + +void +gs_plugin_destroy (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_object_unref (priv->client); + g_ptr_array_unref (priv->devices); +} + +static void +gs_plugin_modalias_ensure_devices (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GList) list = NULL; + + /* already set */ + if (priv->devices->len > 0) + return; + + /* get the devices, and assume ownership of each */ + list = g_udev_client_query_by_subsystem (priv->client, NULL); + for (GList *l = list; l != NULL; l = l->next) { + GUdevDevice *device = G_UDEV_DEVICE (l->data); + if (g_udev_device_get_sysfs_attr (device, "modalias") == NULL) { + g_object_unref (device); + continue; + } + g_ptr_array_add (priv->devices, device); + } + g_debug ("%u devices with modalias", priv->devices->len); +} + +static gboolean +gs_plugin_modalias_matches (GsPlugin *plugin, const gchar *modalias) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + + gs_plugin_modalias_ensure_devices (plugin); + for (guint i = 0; i < priv->devices->len; i++) { + GUdevDevice *device = g_ptr_array_index (priv->devices, i); + const gchar *modalias_tmp; + + /* get the (optional) device modalias */ + modalias_tmp = g_udev_device_get_sysfs_attr (device, "modalias"); + if (modalias_tmp == NULL) + continue; + if (fnmatch (modalias, modalias_tmp, 0) == 0) { + g_debug ("matched %s against %s", modalias_tmp, modalias); + return TRUE; + } + } + return FALSE; +} + +static gboolean +refine_app (GsPlugin *plugin, + GsApp *app, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + GPtrArray *provides; + guint i; + + /* not required */ + if (gs_app_get_icons(app)->len > 0) + return TRUE; + if (gs_app_get_kind (app) != AS_APP_KIND_DRIVER) + return TRUE; + + /* do any of the modaliases match any installed hardware */ + provides = gs_app_get_provides (app); + for (i = 0 ; i < provides->len; i++) { + AsProvide *prov = g_ptr_array_index (provides, i); + if (as_provide_get_kind (prov) != AS_PROVIDE_KIND_MODALIAS) + continue; + if (gs_plugin_modalias_matches (plugin, as_provide_get_value (prov))) { + g_autoptr(AsIcon) ic = NULL; + ic = as_icon_new (); + as_icon_set_kind (ic, AS_ICON_KIND_STOCK); + as_icon_set_name (ic, "emblem-system-symbolic"); + gs_app_add_icon (app, ic); + gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE); + 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/modalias/gs-self-test.c b/plugins/modalias/gs-self-test.c new file mode 100644 index 0000000..5cbb39c --- /dev/null +++ b/plugins/modalias/gs-self-test.c @@ -0,0 +1,116 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2017 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include "gnome-software-private.h" + +#include "gs-test.h" + +static void +gs_plugins_modalias_func (GsPluginLoader *plugin_loader) +{ + GsApp *app; + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* get search result based on addon keyword */ + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH, + "search", "colorhug2", + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_CATEGORIES, + NULL); + list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (list != NULL); + + /* make sure there is one entry, the parent app */ + g_assert_cmpint (gs_app_list_length (list), ==, 1); + app = gs_app_list_index (list, 0); + g_assert_cmpstr (gs_app_get_id (app), ==, "com.hughski.ColorHug2.driver"); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_APP_KIND_DRIVER); + g_assert (gs_app_has_category (app, "Addon")); + g_assert (gs_app_has_category (app, "Driver")); +} + +int +main (int argc, char **argv) +{ + g_autofree gchar *tmp_root = NULL; + gboolean ret; + int retval; + g_autofree gchar *xml = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsPluginLoader) plugin_loader = NULL; + const gchar *allowlist[] = { + "appstream", + "dummy", + "modalias", + NULL + }; + + 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); + g_setenv ("GS_SELF_TEST_DUMMY_ENABLE", "1", TRUE); + + xml = g_strdup_printf ("<?xml version=\"1.0\"?>\n" + "<components version=\"0.9\">\n" + " <component type=\"driver\">\n" + " <id>com.hughski.ColorHug2.driver</id>\n" + " <name>ColorHug2</name>\n" + " <summary>ColorHug2 Colorimeter Driver</summary>\n" + " <pkgname>colorhug-client</pkgname>\n" + " <provides>\n" + " <modalias>pci:*</modalias>\n" + " </provides>\n" + " </component>\n" + " <info>\n" + " <scope>system</scope>\n" + " </info>\n" + "</components>\n"); + g_setenv ("GS_SELF_TEST_APPSTREAM_XML", xml, TRUE); + + /* Use a common cache directory for all tests, since the appstream + * plugin uses it and cannot be reinitialised for each test. */ + tmp_root = g_dir_make_tmp ("gnome-software-modalias-test-XXXXXX", NULL); + g_assert (tmp_root != NULL); + g_setenv ("GS_SELF_TEST_CACHEDIR", tmp_root, TRUE); + + /* 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); + gs_plugin_loader_add_location (plugin_loader, LOCALPLUGINDIR_CORE); + gs_plugin_loader_add_location (plugin_loader, LOCALPLUGINDIR_DUMMY); + ret = gs_plugin_loader_setup (plugin_loader, + (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/modalias", + plugin_loader, + (GTestDataFunc) gs_plugins_modalias_func); + + retval = g_test_run (); + + /* Clean up. */ + gs_utils_rmtree (tmp_root, NULL); + + return retval; +} diff --git a/plugins/modalias/meson.build b/plugins/modalias/meson.build new file mode 100644 index 0000000..22cac6d --- /dev/null +++ b/plugins/modalias/meson.build @@ -0,0 +1,42 @@ +cargs = ['-DG_LOG_DOMAIN="GsPluginModalias"'] +cargs += ['-DLOCALPLUGINDIR="' + meson.current_build_dir() + '"'] +cargs += ['-DLOCALPLUGINDIR_CORE="' + meson.current_build_dir() + '/../core"'] +cargs += ['-DLOCALPLUGINDIR_DUMMY="' + meson.current_build_dir() + '/../dummy"'] + +shared_module( + 'gs_plugin_modalias', +sources : 'gs-plugin-modalias.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : [ plugin_libs, gudev ], + link_with : [ + libgnomesoftware + ] +) + +if get_option('tests') + e = executable( + 'gs-self-test-modalias', + compiled_schemas, + sources : [ + 'gs-self-test.c' + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + dependencies : [ + plugin_libs, + ], + link_with : [ + libgnomesoftware + ], + c_args : cargs, + ) + test('gs-self-test-modalias', e, suite: ['plugins', 'modalias'], env: test_env) +endif diff --git a/plugins/odrs/gs-plugin-odrs.c b/plugins/odrs/gs-plugin-odrs.c new file mode 100644 index 0000000..8c3e9a4 --- /dev/null +++ b/plugins/odrs/gs-plugin-odrs.c @@ -0,0 +1,1214 @@ +/* -*- 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-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <glib/gi18n.h> +#include <gnome-software.h> +#include <json-glib/json-glib.h> +#include <string.h> +#include <math.h> + +/* + * SECTION: + * Provides review data from the Open Desktop Ratings Serice. + */ + +#if !GLIB_CHECK_VERSION(2, 62, 0) +typedef struct +{ + guint8 *data; + guint len; + guint alloc; + guint elt_size; + guint zero_terminated : 1; + guint clear : 1; + gatomicrefcount ref_count; + GDestroyNotify clear_func; +} GRealArray; + +gboolean +g_array_binary_search (GArray *array, + gconstpointer target, + GCompareFunc compare_func, + guint *out_match_index) +{ + gboolean result = FALSE; + GRealArray *_array = (GRealArray *) array; + guint left, middle, right; + gint val; + + g_return_val_if_fail (_array != NULL, FALSE); + g_return_val_if_fail (compare_func != NULL, FALSE); + + if (G_LIKELY(_array->len)) + { + left = 0; + right = _array->len - 1; + + while (left <= right) + { + middle = left + (right - left) / 2; + + val = compare_func (_array->data + (_array->elt_size * middle), target); + if (val == 0) + { + result = TRUE; + break; + } + else if (val < 0) + left = middle + 1; + else if (/* val > 0 && */ middle > 0) + right = middle - 1; + else + break; /* element not found */ + } + } + + if (result && out_match_index != NULL) + *out_match_index = middle; + + return result; +} +#endif /* glib < 2.62.0 */ + +#define ODRS_REVIEW_CACHE_AGE_MAX 237000 /* 1 week */ +#define ODRS_REVIEW_NUMBER_RESULTS_MAX 20 + +/* Element in priv->ratings, all allocated in one big block and sorted + * alphabetically to reduce the number of allocations and fragmentation. */ +typedef struct { + gchar *app_id; /* (owned) */ + guint32 n_star_ratings[6]; +} GsOdrsRating; + +static int +rating_compare (const GsOdrsRating *a, const GsOdrsRating *b) +{ + return g_strcmp0 (a->app_id, b->app_id); +} + +static void +rating_clear (GsOdrsRating *rating) +{ + g_free (rating->app_id); +} + +struct GsPluginData { + GSettings *settings; + gchar *distro; + gchar *user_hash; + gchar *review_server; + GArray *ratings; /* (element-type GsOdrsRating) (mutex ratings_mutex) (owned) (nullable) */ + GMutex ratings_mutex; + GsApp *cached_origin; +}; + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData)); + g_autoptr(GError) error = NULL; + g_autoptr(GsOsRelease) os_release = NULL; + + g_mutex_init (&priv->ratings_mutex); + priv->settings = g_settings_new ("org.gnome.software"); + priv->review_server = g_settings_get_string (priv->settings, + "review-server"); + priv->ratings = NULL; /* until first refreshed */ + + /* get the machine+user ID hash value */ + priv->user_hash = gs_utils_get_user_hash (&error); + if (priv->user_hash == NULL) { + g_warning ("Failed to get machine+user hash: %s", error->message); + return; + } + + /* get the distro name (e.g. 'Fedora') but allow a fallback */ + os_release = gs_os_release_new (&error); + if (os_release != NULL) { + priv->distro = g_strdup (gs_os_release_get_name (os_release)); + if (priv->distro == NULL) { + g_warning ("no distro name specified"); + priv->distro = g_strdup ("Unknown"); + } + } else { + g_warning ("failed to get distro name: %s", error->message); + priv->distro = g_strdup ("Unknown"); + } + + /* add source */ + priv->cached_origin = gs_app_new (gs_plugin_get_name (plugin)); + gs_app_set_kind (priv->cached_origin, AS_APP_KIND_SOURCE); + gs_app_set_origin_hostname (priv->cached_origin, priv->review_server); + + /* add the source to the plugin cache which allows us to match the + * unique ID to a GsApp when creating an event */ + gs_plugin_cache_add (plugin, + gs_app_get_unique_id (priv->cached_origin), + priv->cached_origin); + + /* need application IDs and version */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "appstream"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "flatpak"); + + /* set name of MetaInfo file */ + gs_plugin_set_appstream_id (plugin, "org.gnome.Software.Plugin.Odrs"); +} + +static gboolean +gs_plugin_odrs_load_ratings_for_app (JsonObject *json_app, const gchar *app_id, GsOdrsRating *rating_out) +{ + guint i; + const gchar *names[] = { "star0", "star1", "star2", "star3", + "star4", "star5", NULL }; + + for (i = 0; names[i] != NULL; i++) { + if (!json_object_has_member (json_app, names[i])) + return FALSE; + rating_out->n_star_ratings[i] = (guint64) json_object_get_int_member (json_app, names[i]); + } + + rating_out->app_id = g_strdup (app_id); + + return TRUE; +} + +static gboolean +gs_plugin_odrs_load_ratings (GsPlugin *plugin, const gchar *fn, GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + JsonNode *json_root; + JsonObject *json_item; + g_autoptr(JsonParser) json_parser = NULL; + const gchar *app_id; + JsonNode *json_app_node; + JsonObjectIter iter; + g_autoptr(GArray) new_ratings = NULL; + g_autoptr(GMutexLocker) locker = NULL; + + /* parse the data and find the success */ + json_parser = json_parser_new_immutable (); +#if JSON_CHECK_VERSION(1, 6, 0) + if (!json_parser_load_from_mapped_file (json_parser, fn, error)) { +#else + if (!json_parser_load_from_file (json_parser, fn, error)) { +#endif + gs_utils_error_convert_json_glib (error); + return FALSE; + } + json_root = json_parser_get_root (json_parser); + if (json_root == NULL) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "no ratings root"); + return FALSE; + } + if (json_node_get_node_type (json_root) != JSON_NODE_OBJECT) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "no ratings array"); + return FALSE; + } + + json_item = json_node_get_object (json_root); + + new_ratings = g_array_sized_new (FALSE, /* don’t zero-terminate */ + FALSE, /* don’t clear */ + sizeof (GsOdrsRating), + json_object_get_size (json_item)); + g_array_set_clear_func (new_ratings, (GDestroyNotify) rating_clear); + + /* parse each app */ + json_object_iter_init (&iter, json_item); + while (json_object_iter_next (&iter, &app_id, &json_app_node)) { + GsOdrsRating rating; + JsonObject *json_app; + + if (!JSON_NODE_HOLDS_OBJECT (json_app_node)) + continue; + json_app = json_node_get_object (json_app_node); + + if (gs_plugin_odrs_load_ratings_for_app (json_app, app_id, &rating)) + g_array_append_val (new_ratings, rating); + } + + /* Allow for binary searches later. */ + g_array_sort (new_ratings, (GCompareFunc) rating_compare); + + /* Update the shared state */ + locker = g_mutex_locker_new (&priv->ratings_mutex); + g_clear_pointer (&priv->ratings, g_array_unref); + priv->ratings = g_steal_pointer (&new_ratings); + + return TRUE; +} + +gboolean +gs_plugin_refresh (GsPlugin *plugin, + guint cache_age, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autofree gchar *cache_filename = NULL; + g_autofree gchar *uri = NULL; + g_autoptr(GError) error_local = NULL; + g_autoptr(GsApp) app_dl = gs_app_new (gs_plugin_get_name (plugin)); + + /* check cache age */ + cache_filename = gs_utils_get_cache_filename ("odrs", + "ratings.json", + GS_UTILS_CACHE_FLAG_WRITEABLE, + error); + if (cache_filename == NULL) + return FALSE; + if (cache_age > 0) { + guint tmp; + g_autoptr(GFile) file = NULL; + file = g_file_new_for_path (cache_filename); + tmp = gs_utils_get_file_age (file); + if (tmp < cache_age) { + g_debug ("%s is only %u seconds old, so ignoring refresh", + cache_filename, tmp); + return gs_plugin_odrs_load_ratings (plugin, cache_filename, error); + } + } + + /* download the complete file */ + uri = g_strdup_printf ("%s/ratings", priv->review_server); + g_debug ("Updating ODRS cache from %s to %s", uri, cache_filename); + gs_app_set_summary_missing (app_dl, + /* TRANSLATORS: status text when downloading */ + _("Downloading application ratings…")); + if (!gs_plugin_download_file (plugin, app_dl, uri, cache_filename, cancellable, &error_local)) { + g_autoptr(GsPluginEvent) event = gs_plugin_event_new (); + + gs_plugin_event_set_error (event, error_local); + gs_plugin_event_set_action (event, GS_PLUGIN_ACTION_DOWNLOAD); + gs_plugin_event_set_origin (event, priv->cached_origin); + if (gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)) + gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE); + else + gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING); + gs_plugin_report_event (plugin, event); + + /* don't fail updates if the ratings server is unavailable */ + return TRUE; + } + return gs_plugin_odrs_load_ratings (plugin, cache_filename, error); +} + +void +gs_plugin_destroy (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_free (priv->user_hash); + g_free (priv->distro); + g_free (priv->review_server); + g_clear_pointer (&priv->ratings, g_array_unref); + g_object_unref (priv->settings); + g_object_unref (priv->cached_origin); + g_mutex_clear (&priv->ratings_mutex); +} + +static AsReview * +gs_plugin_odrs_parse_review_object (GsPlugin *plugin, JsonObject *item) +{ + AsReview *rev = as_review_new (); + + /* date */ + if (json_object_has_member (item, "date_created")) { + gint64 timestamp; + g_autoptr(GDateTime) dt = NULL; + timestamp = json_object_get_int_member (item, "date_created"); + dt = g_date_time_new_from_unix_utc (timestamp); + as_review_set_date (rev, dt); + } + + /* assemble review */ + if (json_object_has_member (item, "rating")) + as_review_set_rating (rev, (gint) json_object_get_int_member (item, "rating")); + if (json_object_has_member (item, "score")) { + as_review_set_priority (rev, (gint) json_object_get_int_member (item, "score")); + } else if (json_object_has_member (item, "karma_up") && + json_object_has_member (item, "karma_down")) { + gdouble ku = (gdouble) json_object_get_int_member (item, "karma_up"); + gdouble kd = (gdouble) json_object_get_int_member (item, "karma_down"); + gdouble wilson = 0.f; + + /* from http://www.evanmiller.org/how-not-to-sort-by-average-rating.html */ + if (ku > 0 || kd > 0) { + wilson = ((ku + 1.9208) / (ku + kd) - + 1.96 * sqrt ((ku * kd) / (ku + kd) + 0.9604) / + (ku + kd)) / (1 + 3.8416 / (ku + kd)); + wilson *= 100.f; + } + as_review_set_priority (rev, (gint) wilson); + } + if (json_object_has_member (item, "user_hash")) + as_review_set_reviewer_id (rev, json_object_get_string_member (item, "user_hash")); + if (json_object_has_member (item, "user_display")) + as_review_set_reviewer_name (rev, json_object_get_string_member (item, "user_display")); + if (json_object_has_member (item, "summary")) + as_review_set_summary (rev, json_object_get_string_member (item, "summary")); + if (json_object_has_member (item, "description")) + as_review_set_description (rev, json_object_get_string_member (item, "description")); + if (json_object_has_member (item, "version")) + as_review_set_version (rev, json_object_get_string_member (item, "version")); + + /* add extra metadata for the plugin */ + if (json_object_has_member (item, "user_skey")) { + as_review_add_metadata (rev, "user_skey", + json_object_get_string_member (item, "user_skey")); + } + if (json_object_has_member (item, "app_id")) { + as_review_add_metadata (rev, "app_id", + json_object_get_string_member (item, "app_id")); + } + if (json_object_has_member (item, "review_id")) { + g_autofree gchar *review_id = NULL; + review_id = g_strdup_printf ("%" G_GINT64_FORMAT, + json_object_get_int_member (item, "review_id")); + as_review_set_id (rev, review_id); + } + + /* don't allow multiple votes */ + if (json_object_has_member (item, "vote_id")) + as_review_add_flags (rev, AS_REVIEW_FLAG_VOTED); + + return rev; +} + +static GPtrArray * +gs_plugin_odrs_parse_reviews (GsPlugin *plugin, + const gchar *data, + gssize data_len, + GError **error) +{ + JsonArray *json_reviews; + JsonNode *json_root; + guint i; + g_autoptr(JsonParser) json_parser = NULL; + g_autoptr(GHashTable) reviewer_ids = NULL; + g_autoptr(GPtrArray) reviews = NULL; + + /* nothing */ + if (data == NULL) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "server returned no data"); + return NULL; + } + + /* parse the data and find the array or ratings */ + json_parser = json_parser_new_immutable (); + if (!json_parser_load_from_data (json_parser, data, data_len, error)) { + gs_utils_error_convert_json_glib (error); + return NULL; + } + json_root = json_parser_get_root (json_parser); + if (json_root == NULL) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "no root"); + return NULL; + } + if (json_node_get_node_type (json_root) != JSON_NODE_ARRAY) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "no array"); + return NULL; + } + + /* parse each rating */ + reviews = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + json_reviews = json_node_get_array (json_root); + reviewer_ids = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + for (i = 0; i < json_array_get_length (json_reviews); i++) { + JsonNode *json_review; + JsonObject *json_item; + const gchar *reviewer_id; + g_autoptr(AsReview) review = NULL; + + /* extract the data */ + json_review = json_array_get_element (json_reviews, i); + if (json_node_get_node_type (json_review) != JSON_NODE_OBJECT) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "no object type"); + return NULL; + } + json_item = json_node_get_object (json_review); + if (json_item == NULL) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "no object"); + return NULL; + } + + /* create review */ + review = gs_plugin_odrs_parse_review_object (plugin, + json_item); + + reviewer_id = as_review_get_reviewer_id (review); + if (reviewer_id == NULL) + continue; + + /* dedupe each on the user_hash */ + if (g_hash_table_lookup (reviewer_ids, reviewer_id) != NULL) { + g_debug ("duplicate review %s, skipping", reviewer_id); + continue; + } + g_hash_table_add (reviewer_ids, g_strdup (reviewer_id)); + g_ptr_array_add (reviews, g_object_ref (review)); + } + return g_steal_pointer (&reviews); +} + +static gboolean +gs_plugin_odrs_parse_success (const gchar *data, gssize data_len, GError **error) +{ + JsonNode *json_root; + JsonObject *json_item; + const gchar *msg = NULL; + g_autoptr(JsonParser) json_parser = NULL; + + /* nothing */ + if (data == NULL) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "server returned no data"); + return FALSE; + } + + /* parse the data and find the success */ + json_parser = json_parser_new_immutable (); + if (!json_parser_load_from_data (json_parser, data, data_len, error)) { + gs_utils_error_convert_json_glib (error); + return FALSE; + } + json_root = json_parser_get_root (json_parser); + if (json_root == NULL) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "no error root"); + return FALSE; + } + if (json_node_get_node_type (json_root) != JSON_NODE_OBJECT) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "no error object"); + return FALSE; + } + json_item = json_node_get_object (json_root); + if (json_item == NULL) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "no error object"); + return FALSE; + } + + /* failed? */ + if (json_object_has_member (json_item, "msg")) + msg = json_object_get_string_member (json_item, "msg"); + if (!json_object_get_boolean_member (json_item, "success")) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + msg != NULL ? msg : "unknown failure"); + return FALSE; + } + + /* just for the console */ + if (msg != NULL) + g_debug ("success: %s", msg); + return TRUE; +} + +static gboolean +gs_plugin_odrs_json_post (SoupSession *session, + const gchar *uri, + const gchar *data, + GError **error) +{ + guint status_code; + g_autoptr(SoupMessage) msg = NULL; + + /* create the GET data */ + g_debug ("Sending ODRS request to %s: %s", uri, data); + msg = soup_message_new (SOUP_METHOD_POST, uri); + soup_message_set_request (msg, "application/json; charset=utf-8", + SOUP_MEMORY_COPY, data, strlen (data)); + + /* set sync request */ + status_code = soup_session_send_message (session, msg); + g_debug ("ODRS server returned status %u: %s", status_code, msg->response_body->data); + if (status_code != SOUP_STATUS_OK) { + g_warning ("Failed to set rating on ODRS: %s", + soup_status_get_phrase (status_code)); + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED, + "Failed to submit review to ODRS: %s", soup_status_get_phrase (status_code)); + return FALSE; + } + + /* process returned JSON */ + return gs_plugin_odrs_parse_success (msg->response_body->data, + msg->response_body->length, + error); +} + +static GPtrArray * +_gs_app_get_reviewable_ids (GsApp *app) +{ + GPtrArray *ids = g_ptr_array_new_with_free_func (g_free); + GPtrArray *provides = gs_app_get_provides (app); + + /* add the main component id */ + g_ptr_array_add (ids, g_strdup (gs_app_get_id (app))); + + /* add any ID provides */ + for (guint i = 0; i < provides->len; i++) { + AsProvide *provide = g_ptr_array_index (provides, i); + if (as_provide_get_kind (provide) == AS_PROVIDE_KIND_ID && + as_provide_get_value (provide) != NULL) { + g_ptr_array_add (ids, g_strdup (as_provide_get_value (provide))); + } + } + return ids; +} + +static gboolean +gs_plugin_odrs_refine_ratings (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + gint rating; + guint32 ratings_raw[6] = { 0, 0, 0, 0, 0, 0 }; + guint cnt = 0; + g_autoptr(GArray) review_ratings = NULL; + g_autoptr(GPtrArray) reviewable_ids = NULL; + g_autoptr(GMutexLocker) locker = NULL; + + /* get ratings for each reviewable ID */ + reviewable_ids = _gs_app_get_reviewable_ids (app); + + locker = g_mutex_locker_new (&priv->ratings_mutex); + + if (priv->ratings == NULL) + return TRUE; + + for (guint i = 0; i < reviewable_ids->len; i++) { + const gchar *id = g_ptr_array_index (reviewable_ids, i); + const GsOdrsRating search_rating = { id, { 0, }}; + guint found_index; + const GsOdrsRating *found_rating; + + if (!g_array_binary_search (priv->ratings, &search_rating, + (GCompareFunc) rating_compare, &found_index)) + continue; + + found_rating = &g_array_index (priv->ratings, GsOdrsRating, found_index); + + /* copy into accumulator array */ + for (guint j = 0; j < 6; j++) + ratings_raw[j] += found_rating->n_star_ratings[j]; + cnt++; + } + if (cnt == 0) + return TRUE; + + /* Done with priv->ratings now */ + g_clear_pointer (&locker, g_mutex_locker_free); + + /* merge to accumulator array back to one GArray blob */ + review_ratings = g_array_sized_new (FALSE, TRUE, sizeof(guint32), 6); + for (guint i = 0; i < 6; i++) + g_array_append_val (review_ratings, ratings_raw[i]); + gs_app_set_review_ratings (app, review_ratings); + + /* find the wilson rating */ + rating = gs_utils_get_wilson_rating (g_array_index (review_ratings, guint32, 1), + g_array_index (review_ratings, guint32, 2), + g_array_index (review_ratings, guint32, 3), + g_array_index (review_ratings, guint32, 4), + g_array_index (review_ratings, guint32, 5)); + if (rating > 0) + gs_app_set_rating (app, rating); + return TRUE; +} + +static JsonNode * +gs_plugin_odrs_get_compat_ids (GsApp *app) +{ + GPtrArray *provides = gs_app_get_provides (app); + g_autoptr(GHashTable) ids = NULL; + g_autoptr(JsonArray) json_array = json_array_new (); + g_autoptr(JsonNode) json_node = json_node_new (JSON_NODE_ARRAY); + + ids = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + for (guint i = 0; i < provides->len; i++) { + AsProvide *provide = g_ptr_array_index (provides, i); + if (as_provide_get_kind (provide) != AS_PROVIDE_KIND_ID) + continue; + if (as_provide_get_value (provide) == NULL) + continue; + if (g_hash_table_lookup (ids, as_provide_get_value (provide)) != NULL) + continue; + g_hash_table_add (ids, g_strdup (as_provide_get_value (provide))); + json_array_add_string_element (json_array, as_provide_get_value (provide)); + } + if (json_array_get_length (json_array) == 0) + return NULL; + json_node_set_array (json_node, json_array); + return g_steal_pointer (&json_node); +} + +static GPtrArray * +gs_plugin_odrs_fetch_for_app (GsPlugin *plugin, GsApp *app, GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + JsonNode *json_compat_ids; + const gchar *version; + guint status_code; + g_autofree gchar *cachefn_basename = NULL; + g_autofree gchar *cachefn = NULL; + g_autofree gchar *data = NULL; + g_autofree gchar *uri = NULL; + g_autoptr(GFile) cachefn_file = NULL; + g_autoptr(GPtrArray) reviews = NULL; + g_autoptr(JsonBuilder) builder = NULL; + g_autoptr(JsonGenerator) json_generator = NULL; + g_autoptr(JsonNode) json_root = NULL; + g_autoptr(SoupMessage) msg = NULL; + + /* look in the cache */ + cachefn_basename = g_strdup_printf ("%s.json", gs_app_get_id (app)); + cachefn = gs_utils_get_cache_filename ("odrs", + cachefn_basename, + GS_UTILS_CACHE_FLAG_WRITEABLE, + error); + if (cachefn == NULL) + return NULL; + cachefn_file = g_file_new_for_path (cachefn); + if (gs_utils_get_file_age (cachefn_file) < ODRS_REVIEW_CACHE_AGE_MAX) { + g_autoptr(GMappedFile) mapped_file = NULL; + + mapped_file = g_mapped_file_new (cachefn, FALSE, error); + if (mapped_file == NULL) + return NULL; + + g_debug ("got review data for %s from %s", + gs_app_get_id (app), cachefn); + return gs_plugin_odrs_parse_reviews (plugin, + g_mapped_file_get_contents (mapped_file), + g_mapped_file_get_length (mapped_file), + error); + } + + /* not always available */ + version = gs_app_get_version (app); + if (version == NULL) + version = "unknown"; + + /* create object with review data */ + builder = json_builder_new (); + json_builder_begin_object (builder); + json_builder_set_member_name (builder, "user_hash"); + json_builder_add_string_value (builder, priv->user_hash); + json_builder_set_member_name (builder, "app_id"); + json_builder_add_string_value (builder, gs_app_get_id (app)); + json_builder_set_member_name (builder, "locale"); + json_builder_add_string_value (builder, gs_plugin_get_locale (plugin)); + json_builder_set_member_name (builder, "distro"); + json_builder_add_string_value (builder, priv->distro); + json_builder_set_member_name (builder, "version"); + json_builder_add_string_value (builder, version); + json_builder_set_member_name (builder, "limit"); + json_builder_add_int_value (builder, ODRS_REVIEW_NUMBER_RESULTS_MAX); + json_compat_ids = gs_plugin_odrs_get_compat_ids (app); + if (json_compat_ids != NULL) { + json_builder_set_member_name (builder, "compat_ids"); + json_builder_add_value (builder, json_compat_ids); + } + json_builder_end_object (builder); + + /* export as a string */ + json_root = json_builder_get_root (builder); + json_generator = json_generator_new (); + json_generator_set_pretty (json_generator, TRUE); + json_generator_set_root (json_generator, json_root); + data = json_generator_to_data (json_generator, NULL); + if (data == NULL) + return NULL; + uri = g_strdup_printf ("%s/fetch", priv->review_server); + g_debug ("Updating ODRS cache for %s from %s to %s; request %s", gs_app_get_id (app), + uri, cachefn, data); + msg = soup_message_new (SOUP_METHOD_POST, uri); + soup_message_set_request (msg, "application/json; charset=utf-8", + SOUP_MEMORY_COPY, data, strlen (data)); + status_code = soup_session_send_message (gs_plugin_get_soup_session (plugin), msg); + if (status_code != SOUP_STATUS_OK) { + if (!gs_plugin_odrs_parse_success (msg->response_body->data, + msg->response_body->length, + error)) + return NULL; + /* not sure what to do here */ + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_DOWNLOAD_FAILED, + "status code invalid"); + gs_utils_error_add_origin_id (error, priv->cached_origin); + return NULL; + } + reviews = gs_plugin_odrs_parse_reviews (plugin, + msg->response_body->data, + msg->response_body->length, + error); + if (reviews == NULL) + return NULL; + + /* save to the cache */ + if (!g_file_set_contents (cachefn, + msg->response_body->data, + msg->response_body->length, + error)) + return NULL; + + /* success */ + return g_steal_pointer (&reviews); +} + +static gboolean +gs_plugin_odrs_refine_reviews (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + AsReview *review; + g_autoptr(GPtrArray) reviews = NULL; + + /* get from server */ + reviews = gs_plugin_odrs_fetch_for_app (plugin, app, error); + if (reviews == NULL) + return FALSE; + for (guint i = 0; i < reviews->len; i++) { + review = g_ptr_array_index (reviews, i); + + /* save this on the application object so we can use it for + * submitting a new review */ + if (i == 0) { + gs_app_set_metadata (app, "ODRS::user_skey", + as_review_get_metadata_item (review, "user_skey")); + } + + /* ignore invalid reviews */ + if (as_review_get_rating (review) == 0) + continue; + + /* the user_hash matches, so mark this as our own review */ + if (g_strcmp0 (as_review_get_reviewer_id (review), + priv->user_hash) == 0) { + as_review_set_flags (review, AS_REVIEW_FLAG_SELF); + } + gs_app_add_review (app, review); + } + return TRUE; +} + +static gboolean +refine_app (GsPlugin *plugin, + GsApp *app, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + /* not valid */ + if (gs_app_get_kind (app) == AS_APP_KIND_ADDON) + return TRUE; + if (gs_app_get_id (app) == NULL) + return TRUE; + + /* add reviews if possible */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS) { + if (gs_app_get_reviews(app)->len > 0) + return TRUE; + if (!gs_plugin_odrs_refine_reviews (plugin, app, + cancellable, error)) + return FALSE; + } + + /* add ratings if possible */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEW_RATINGS || + flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING) { + if (gs_app_get_review_ratings(app) != NULL) + return TRUE; + if (!gs_plugin_odrs_refine_ratings (plugin, app, + cancellable, error)) + return FALSE; + } + + 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_REVIEWS | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEW_RATINGS | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING)) == 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; +} + +static gchar * +gs_plugin_odrs_sanitize_version (const gchar *version) +{ + gchar *str; + gchar *tmp; + + /* nothing set */ + if (version == NULL) + return g_strdup ("unknown"); + + /* remove epoch */ + str = g_strrstr (version, ":"); + if (str != NULL) + version = str + 1; + + /* remove release */ + tmp = g_strdup (version); + g_strdelimit (tmp, "-", '\0'); + + /* remove '+dfsg' suffix */ + str = g_strstr_len (tmp, -1, "+dfsg"); + if (str != NULL) + *str = '\0'; + + return tmp; +} + +static gboolean +gs_plugin_odrs_invalidate_cache (AsReview *review, GError **error) +{ + g_autofree gchar *cachefn_basename = NULL; + g_autofree gchar *cachefn = NULL; + g_autoptr(GFile) cachefn_file = NULL; + + /* look in the cache */ + cachefn_basename = g_strdup_printf ("%s.json", + as_review_get_metadata_item (review, "app_id")); + cachefn = gs_utils_get_cache_filename ("odrs", + cachefn_basename, + GS_UTILS_CACHE_FLAG_WRITEABLE, + error); + if (cachefn == NULL) + return FALSE; + cachefn_file = g_file_new_for_path (cachefn); + if (!g_file_query_exists (cachefn_file, NULL)) + return TRUE; + return g_file_delete (cachefn_file, NULL, error); +} + +gboolean +gs_plugin_review_submit (GsPlugin *plugin, + GsApp *app, + AsReview *review, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autofree gchar *data = NULL; + g_autofree gchar *uri = NULL; + g_autofree gchar *version = NULL; + g_autoptr(JsonBuilder) builder = NULL; + g_autoptr(JsonGenerator) json_generator = NULL; + g_autoptr(JsonNode) json_root = NULL; + + /* save as we don't re-request the review from the server */ + as_review_add_flags (review, AS_REVIEW_FLAG_SELF); + as_review_set_reviewer_name (review, g_get_real_name ()); + as_review_add_metadata (review, "app_id", gs_app_get_id (app)); + as_review_add_metadata (review, "user_skey", + gs_app_get_metadata_item (app, "ODRS::user_skey")); + + /* create object with review data */ + builder = json_builder_new (); + json_builder_begin_object (builder); + json_builder_set_member_name (builder, "user_hash"); + json_builder_add_string_value (builder, priv->user_hash); + json_builder_set_member_name (builder, "user_skey"); + json_builder_add_string_value (builder, + as_review_get_metadata_item (review, "user_skey")); + json_builder_set_member_name (builder, "app_id"); + json_builder_add_string_value (builder, + as_review_get_metadata_item (review, "app_id")); + json_builder_set_member_name (builder, "locale"); + json_builder_add_string_value (builder, gs_plugin_get_locale (plugin)); + json_builder_set_member_name (builder, "distro"); + json_builder_add_string_value (builder, priv->distro); + json_builder_set_member_name (builder, "version"); + version = gs_plugin_odrs_sanitize_version (as_review_get_version (review)); + json_builder_add_string_value (builder, version); + json_builder_set_member_name (builder, "user_display"); + json_builder_add_string_value (builder, as_review_get_reviewer_name (review)); + json_builder_set_member_name (builder, "summary"); + json_builder_add_string_value (builder, as_review_get_summary (review)); + json_builder_set_member_name (builder, "description"); + json_builder_add_string_value (builder, as_review_get_description (review)); + json_builder_set_member_name (builder, "rating"); + json_builder_add_int_value (builder, as_review_get_rating (review)); + json_builder_end_object (builder); + + /* export as a string */ + json_root = json_builder_get_root (builder); + json_generator = json_generator_new (); + json_generator_set_pretty (json_generator, TRUE); + json_generator_set_root (json_generator, json_root); + data = json_generator_to_data (json_generator, NULL); + + /* clear cache */ + if (!gs_plugin_odrs_invalidate_cache (review, error)) + return FALSE; + + /* POST */ + uri = g_strdup_printf ("%s/submit", priv->review_server); + return gs_plugin_odrs_json_post (gs_plugin_get_soup_session (plugin), + uri, data, error); +} + +static gboolean +gs_plugin_odrs_vote (GsPlugin *plugin, AsReview *review, + const gchar *uri, GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + const gchar *tmp; + g_autofree gchar *data = NULL; + g_autoptr(JsonBuilder) builder = NULL; + g_autoptr(JsonGenerator) json_generator = NULL; + g_autoptr(JsonNode) json_root = NULL; + + /* create object with vote data */ + builder = json_builder_new (); + json_builder_begin_object (builder); + + json_builder_set_member_name (builder, "user_hash"); + json_builder_add_string_value (builder, priv->user_hash); + json_builder_set_member_name (builder, "user_skey"); + json_builder_add_string_value (builder, + as_review_get_metadata_item (review, "user_skey")); + json_builder_set_member_name (builder, "app_id"); + json_builder_add_string_value (builder, + as_review_get_metadata_item (review, "app_id")); + tmp = as_review_get_id (review); + if (tmp != NULL) { + gint64 review_id; + json_builder_set_member_name (builder, "review_id"); + review_id = g_ascii_strtoll (tmp, NULL, 10); + json_builder_add_int_value (builder, review_id); + } + json_builder_end_object (builder); + + /* export as a string */ + json_root = json_builder_get_root (builder); + json_generator = json_generator_new (); + json_generator_set_pretty (json_generator, TRUE); + json_generator_set_root (json_generator, json_root); + data = json_generator_to_data (json_generator, NULL); + if (data == NULL) + return FALSE; + + /* clear cache */ + if (!gs_plugin_odrs_invalidate_cache (review, error)) + return FALSE; + + /* send to server */ + if (!gs_plugin_odrs_json_post (gs_plugin_get_soup_session (plugin), + uri, data, error)) + return FALSE; + + /* mark as voted */ + as_review_add_flags (review, AS_REVIEW_FLAG_VOTED); + + /* success */ + return TRUE; +} + +gboolean +gs_plugin_review_report (GsPlugin *plugin, + GsApp *app, + AsReview *review, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autofree gchar *uri = NULL; + uri = g_strdup_printf ("%s/report", priv->review_server); + return gs_plugin_odrs_vote (plugin, review, uri, error); +} + +gboolean +gs_plugin_review_upvote (GsPlugin *plugin, + GsApp *app, + AsReview *review, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autofree gchar *uri = NULL; + uri = g_strdup_printf ("%s/upvote", priv->review_server); + return gs_plugin_odrs_vote (plugin, review, uri, error); +} + +gboolean +gs_plugin_review_downvote (GsPlugin *plugin, + GsApp *app, + AsReview *review, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autofree gchar *uri = NULL; + uri = g_strdup_printf ("%s/downvote", priv->review_server); + return gs_plugin_odrs_vote (plugin, review, uri, error); +} + +gboolean +gs_plugin_review_dismiss (GsPlugin *plugin, + GsApp *app, + AsReview *review, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autofree gchar *uri = NULL; + uri = g_strdup_printf ("%s/dismiss", priv->review_server); + return gs_plugin_odrs_vote (plugin, review, uri, error); +} + +gboolean +gs_plugin_review_remove (GsPlugin *plugin, + GsApp *app, + AsReview *review, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autofree gchar *uri = NULL; + uri = g_strdup_printf ("%s/remove", priv->review_server); + return gs_plugin_odrs_vote (plugin, review, uri, error); +} + +static GsApp * +gs_plugin_create_app_dummy (const gchar *id) +{ + GsApp *app = gs_app_new (id); + g_autoptr(GString) str = NULL; + str = g_string_new (id); + as_utils_string_replace (str, ".desktop", ""); + g_string_prepend (str, "No description is available for "); + gs_app_set_name (app, GS_APP_QUALITY_LOWEST, "Unknown Application"); + gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, "Application not found"); + gs_app_set_description (app, GS_APP_QUALITY_LOWEST, str->str); + return app; +} + +gboolean +gs_plugin_add_unvoted_reviews (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + guint status_code; + guint i; + g_autofree gchar *uri = NULL; + g_autoptr(GHashTable) hash = NULL; + g_autoptr(GPtrArray) reviews = NULL; + g_autoptr(SoupMessage) msg = NULL; + + /* create the GET data *with* the machine hash so we can later + * review the application ourselves */ + uri = g_strdup_printf ("%s/moderate/%s/%s", + priv->review_server, + priv->user_hash, + gs_plugin_get_locale (plugin)); + msg = soup_message_new (SOUP_METHOD_GET, uri); + status_code = soup_session_send_message (gs_plugin_get_soup_session (plugin), msg); + if (status_code != SOUP_STATUS_OK) { + if (!gs_plugin_odrs_parse_success (msg->response_body->data, + msg->response_body->length, + error)) + return FALSE; + /* not sure what to do here */ + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_DOWNLOAD_FAILED, + "status code invalid"); + gs_utils_error_add_origin_id (error, priv->cached_origin); + return FALSE; + } + g_debug ("odrs returned: %s", msg->response_body->data); + reviews = gs_plugin_odrs_parse_reviews (plugin, + msg->response_body->data, + msg->response_body->length, + error); + if (reviews == NULL) + return FALSE; + + /* look at all the reviews; faking application objects */ + hash = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, (GDestroyNotify) g_object_unref); + for (i = 0; i < reviews->len; i++) { + GsApp *app; + AsReview *review; + const gchar *app_id; + + /* same app? */ + review = g_ptr_array_index (reviews, i); + app_id = as_review_get_metadata_item (review, "app_id"); + app = g_hash_table_lookup (hash, app_id); + if (app == NULL) { + app = gs_plugin_create_app_dummy (app_id); + gs_app_list_add (list, app); + g_hash_table_insert (hash, g_strdup (app_id), app); + } + gs_app_add_review (app, review); + } + + return TRUE; +} diff --git a/plugins/odrs/meson.build b/plugins/odrs/meson.build new file mode 100644 index 0000000..254530d --- /dev/null +++ b/plugins/odrs/meson.build @@ -0,0 +1,27 @@ +cargs = ['-DG_LOG_DOMAIN="GsPluginOdrs"'] + +shared_module( + 'gs_plugin_odrs', + sources : 'gs-plugin-odrs.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : plugin_libs, + link_with : [ + libgnomesoftware + ] +) +metainfo = 'org.gnome.Software.Plugin.Odrs.metainfo.xml' + +i18n.merge_file( + input: metainfo + '.in', + output: metainfo, + type: 'xml', + po_dir: join_paths(meson.source_root(), 'po'), + install: true, + install_dir: join_paths(get_option('datadir'), 'metainfo') +) diff --git a/plugins/odrs/org.gnome.Software.Plugin.Odrs.metainfo.xml.in b/plugins/odrs/org.gnome.Software.Plugin.Odrs.metainfo.xml.in new file mode 100644 index 0000000..833f69c --- /dev/null +++ b/plugins/odrs/org.gnome.Software.Plugin.Odrs.metainfo.xml.in @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright 2013-2016 Richard Hughes <richard@hughsie.com> --> +<component type="addon"> + <id>org.gnome.Software.Plugin.Odrs</id> + <extends>org.gnome.Software.desktop</extends> + <name>Open Desktop Ratings Support</name> + <summary>ODRS is a service providing user reviews of applications</summary> + <url type="homepage">https://odrs.gnome.org/</url> + <metadata_license>CC0-1.0</metadata_license> + <project_license>GPL-2.0+</project_license> + <update_contact>richard_at_hughsie.com</update_contact> +</component> diff --git a/plugins/packagekit/gs-markdown.c b/plugins/packagekit/gs-markdown.c new file mode 100644 index 0000000..b7be06b --- /dev/null +++ b/plugins/packagekit/gs-markdown.c @@ -0,0 +1,856 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2008 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <string.h> +#include <glib.h> + +#include "gs-markdown.h" + +/******************************************************************************* + * + * This is a simple Markdown parser. + * It can output to Pango, HTML or plain text. The following limitations are + * already known, and properly deliberate: + * + * - No code section support + * - No ordered list support + * - No blockquote section support + * - No image support + * - No links or email support + * - No backslash escapes support + * - No HTML escaping support + * - Auto-escapes certain word patterns, like http:// + * + * It does support the rest of the standard pretty well, although it's not + * been run against any conformance tests. The parsing is single pass, with + * a simple enumerated interpretor mode and a single line back-memory. + * + ******************************************************************************/ + +typedef enum { + GS_MARKDOWN_MODE_BLANK, + GS_MARKDOWN_MODE_RULE, + GS_MARKDOWN_MODE_BULLETT, + GS_MARKDOWN_MODE_PARA, + GS_MARKDOWN_MODE_H1, + GS_MARKDOWN_MODE_H2, + GS_MARKDOWN_MODE_UNKNOWN +} GsMarkdownMode; + +typedef struct { + const gchar *em_start; + const gchar *em_end; + const gchar *strong_start; + const gchar *strong_end; + const gchar *code_start; + const gchar *code_end; + const gchar *h1_start; + const gchar *h1_end; + const gchar *h2_start; + const gchar *h2_end; + const gchar *bullet_start; + const gchar *bullet_end; + const gchar *rule; +} GsMarkdownTags; + +struct _GsMarkdown { + GObject parent_instance; + + GsMarkdownMode mode; + GsMarkdownTags tags; + GsMarkdownOutputKind output; + gint max_lines; + gint line_count; + gboolean smart_quoting; + gboolean escape; + gboolean autocode; + gboolean autolinkify; + GString *pending; + GString *processed; +}; + +G_DEFINE_TYPE (GsMarkdown, gs_markdown, G_TYPE_OBJECT) + +/* + * gs_markdown_to_text_line_is_rule: + * + * Horizontal rules are created by placing three or more hyphens, asterisks, + * or underscores on a line by themselves. + * You may use spaces between the hyphens or asterisks. + **/ +static gboolean +gs_markdown_to_text_line_is_rule (const gchar *line) +{ + guint i; + guint len; + guint count = 0; + g_autofree gchar *copy = NULL; + + len = (guint) strlen (line); + if (len == 0) + return FALSE; + + /* replace non-rule chars with ~ */ + copy = g_strdup (line); + g_strcanon (copy, "-*_ ", '~'); + for (i = 0; i < len; i++) { + if (copy[i] == '~') + return FALSE; + if (copy[i] != ' ') + count++; + } + + /* if we matched, return true */ + if (count >= 3) + return TRUE; + return FALSE; +} + +static gboolean +gs_markdown_to_text_line_is_bullet (const gchar *line) +{ + return (g_str_has_prefix (line, "- ") || + g_str_has_prefix (line, "* ") || + g_str_has_prefix (line, "+ ") || + g_str_has_prefix (line, " - ") || + g_str_has_prefix (line, " * ") || + g_str_has_prefix (line, " + ")); +} + +static gboolean +gs_markdown_to_text_line_is_header1 (const gchar *line) +{ + return g_str_has_prefix (line, "# "); +} + +static gboolean +gs_markdown_to_text_line_is_header2 (const gchar *line) +{ + return g_str_has_prefix (line, "## "); +} + +static gboolean +gs_markdown_to_text_line_is_header1_type2 (const gchar *line) +{ + return g_str_has_prefix (line, "==="); +} + +static gboolean +gs_markdown_to_text_line_is_header2_type2 (const gchar *line) +{ + return g_str_has_prefix (line, "---"); +} + +#if 0 +static gboolean +gs_markdown_to_text_line_is_code (const gchar *line) +{ + return (g_str_has_prefix (line, " ") || + g_str_has_prefix (line, "\t")); +} + +static gboolean +gs_markdown_to_text_line_is_blockquote (const gchar *line) +{ + return (g_str_has_prefix (line, "> ")); +} +#endif + +static gboolean +gs_markdown_to_text_line_is_blank (const gchar *line) +{ + guint i; + guint len; + + /* a line with no characters is blank by definition */ + len = (guint) strlen (line); + if (len == 0) + return TRUE; + + /* find if there are only space chars */ + for (i = 0; i < len; i++) { + if (line[i] != ' ' && line[i] != '\t') + return FALSE; + } + + /* if we matched, return true */ + return TRUE; +} + +static gchar * +gs_markdown_replace (const gchar *haystack, + const gchar *needle, + const gchar *replace) +{ + g_auto(GStrv) split = NULL; + split = g_strsplit (haystack, needle, -1); + return g_strjoinv (replace, split); +} + +static gchar * +gs_markdown_strstr_spaces (const gchar *haystack, const gchar *needle) +{ + gchar *found; + const gchar *haystack_new = haystack; + +retry: + /* don't find if surrounded by spaces */ + found = strstr (haystack_new, needle); + if (found == NULL) + return NULL; + + /* start of the string, always valid */ + if (found == haystack) + return found; + + /* end of the string, always valid */ + if (*(found-1) == ' ' && *(found+1) == ' ') { + haystack_new = found+1; + goto retry; + } + return found; +} + +static gchar * +gs_markdown_to_text_line_formatter (const gchar *line, + const gchar *formatter, + const gchar *left, + const gchar *right) +{ + guint len; + gchar *str1; + gchar *str2; + gchar *start = NULL; + gchar *middle = NULL; + gchar *end = NULL; + g_autofree gchar *copy = NULL; + + /* needed to know for shifts */ + len = (guint) strlen (formatter); + if (len == 0) + return NULL; + + /* find sections */ + copy = g_strdup (line); + str1 = gs_markdown_strstr_spaces (copy, formatter); + if (str1 != NULL) { + *str1 = '\0'; + str2 = gs_markdown_strstr_spaces (str1+len, formatter); + if (str2 != NULL) { + *str2 = '\0'; + middle = str1 + len; + start = copy; + end = str2 + len; + } + } + + /* if we found, replace and keep looking for the same string */ + if (start != NULL && middle != NULL && end != NULL) { + g_autofree gchar *temp = NULL; + temp = g_strdup_printf ("%s%s%s%s%s", start, left, middle, right, end); + /* recursive */ + return gs_markdown_to_text_line_formatter (temp, formatter, left, right); + } + + /* not found, keep return as-is */ + return g_strdup (line); +} + +static gchar * +gs_markdown_to_text_line_format_sections (GsMarkdown *self, const gchar *line) +{ + gchar *data = g_strdup (line); + gchar *temp; + + /* bold1 */ + temp = data; + data = gs_markdown_to_text_line_formatter (temp, "**", + self->tags.strong_start, + self->tags.strong_end); + g_free (temp); + + /* bold2 */ + temp = data; + data = gs_markdown_to_text_line_formatter (temp, "__", + self->tags.strong_start, + self->tags.strong_end); + g_free (temp); + + /* italic1 */ + temp = data; + data = gs_markdown_to_text_line_formatter (temp, "*", + self->tags.em_start, + self->tags.em_end); + g_free (temp); + + /* italic2 */ + temp = data; + data = gs_markdown_to_text_line_formatter (temp, "_", + self->tags.em_start, + self->tags.em_end); + g_free (temp); + + /* em-dash */ + temp = data; + data = gs_markdown_replace (temp, " -- ", " — "); + g_free (temp); + + /* smart quoting */ + if (self->smart_quoting) { + temp = data; + data = gs_markdown_to_text_line_formatter (temp, "\"", "“", "”"); + g_free (temp); + + temp = data; + data = gs_markdown_to_text_line_formatter (temp, "'", "‘", "’"); + g_free (temp); + } + + return data; +} + +static gchar * +gs_markdown_to_text_line_format (GsMarkdown *self, const gchar *line) +{ + GString *string; + gboolean mode = FALSE; + gchar *text; + guint i; + g_auto(GStrv) codes = NULL; + + /* optimise the trivial case where we don't have any code tags */ + text = strstr (line, "`"); + if (text == NULL) + return gs_markdown_to_text_line_format_sections (self, line); + + /* we want to parse the code sections without formatting */ + codes = g_strsplit (line, "`", -1); + string = g_string_new (""); + for (i = 0; codes[i] != NULL; i++) { + if (!mode) { + text = gs_markdown_to_text_line_format_sections (self, codes[i]); + g_string_append (string, text); + g_free (text); + mode = TRUE; + } else { + /* just append without formatting */ + g_string_append (string, self->tags.code_start); + g_string_append (string, codes[i]); + g_string_append (string, self->tags.code_end); + mode = FALSE; + } + } + return g_string_free (string, FALSE); +} + +static gboolean +gs_markdown_add_pending (GsMarkdown *self, const gchar *line) +{ + g_autofree gchar *copy = NULL; + + /* would put us over the limit */ + if (self->max_lines > 0 && self->line_count >= self->max_lines) + return FALSE; + + copy = g_strdup (line); + + /* strip leading and trailing spaces */ + g_strstrip (copy); + + /* append */ + g_string_append_printf (self->pending, "%s ", copy); + return TRUE; +} + +static gboolean +gs_markdown_add_pending_header (GsMarkdown *self, const gchar *line) +{ + g_autofree gchar *copy = NULL; + + /* strip trailing # */ + copy = g_strdup (line); + g_strdelimit (copy, "#", ' '); + return gs_markdown_add_pending (self, copy); +} + +static guint +gs_markdown_count_chars_in_word (const gchar *text, gchar find) +{ + guint i; + guint len; + guint count = 0; + + /* get length */ + len = (guint) strlen (text); + if (len == 0) + return 0; + + /* find matching chars */ + for (i = 0; i < len; i++) { + if (text[i] == find) + count++; + } + return count; +} + +static gboolean +gs_markdown_word_is_code (const gchar *text) +{ + /* already code */ + if (g_str_has_prefix (text, "`")) + return FALSE; + if (g_str_has_suffix (text, "`")) + return FALSE; + + /* paths */ + if (g_str_has_prefix (text, "/")) + return TRUE; + + /* bugzillas */ + if (g_str_has_prefix (text, "#")) + return TRUE; + + /* patch files */ + if (g_strrstr (text, ".patch") != NULL) + return TRUE; + if (g_strrstr (text, ".diff") != NULL) + return TRUE; + + /* function names */ + if (g_strrstr (text, "()") != NULL) + return TRUE; + + /* email addresses */ + if (g_strrstr (text, "@") != NULL) + return TRUE; + + /* compiler defines */ + if (text[0] != '_' && + gs_markdown_count_chars_in_word (text, '_') > 1) + return TRUE; + + /* nothing special */ + return FALSE; +} + +static gchar * +gs_markdown_word_auto_format_code (const gchar *text) +{ + guint i; + gchar *temp; + gboolean ret = FALSE; + g_auto(GStrv) words = NULL; + + /* split sentence up with space */ + words = g_strsplit (text, " ", -1); + + /* search each word */ + for (i = 0; words[i] != NULL; i++) { + if (gs_markdown_word_is_code (words[i])) { + temp = g_strdup_printf ("`%s`", words[i]); + g_free (words[i]); + words[i] = temp; + ret = TRUE; + } + } + + /* no replacements, so just return a copy */ + if (!ret) + return g_strdup (text); + + /* join the array back into a string */ + return g_strjoinv (" ", words); +} + +static gboolean +gs_markdown_word_is_url (const gchar *text) +{ + if (g_str_has_prefix (text, "http://")) + return TRUE; + if (g_str_has_prefix (text, "https://")) + return TRUE; + if (g_str_has_prefix (text, "ftp://")) + return TRUE; + return FALSE; +} + +static gchar * +gs_markdown_word_auto_format_urls (const gchar *text) +{ + guint i; + gchar *temp; + gboolean ret = FALSE; + g_auto(GStrv) words = NULL; + + /* split sentence up with space */ + words = g_strsplit (text, " ", -1); + + /* search each word */ + for (i = 0; words[i] != NULL; i++) { + if (gs_markdown_word_is_url (words[i])) { + temp = g_strdup_printf ("<a href=\"%s\">%s</a>", + words[i], words[i]); + g_free (words[i]); + words[i] = temp; + ret = TRUE; + } + } + + /* no replacements, so just return a copy */ + if (!ret) + return g_strdup (text); + + /* join the array back into a string */ + return g_strjoinv (" ", words); +} + +static void +gs_markdown_flush_pending (GsMarkdown *self) +{ + g_autofree gchar *copy = NULL; + g_autofree gchar *temp = NULL; + + /* no data yet */ + if (self->mode == GS_MARKDOWN_MODE_UNKNOWN) + return; + + /* remove trailing spaces */ + while (g_str_has_suffix (self->pending->str, " ")) + g_string_set_size (self->pending, self->pending->len - 1); + + /* pango requires escaping */ + copy = g_strdup (self->pending->str); + if (!self->escape && self->output == GS_MARKDOWN_OUTPUT_PANGO) { + g_strdelimit (copy, "<", '('); + g_strdelimit (copy, ">", ')'); + g_strdelimit (copy, "&", '+'); + } + + /* check words for code */ + if (self->autocode && + (self->mode == GS_MARKDOWN_MODE_PARA || + self->mode == GS_MARKDOWN_MODE_BULLETT)) { + temp = gs_markdown_word_auto_format_code (copy); + g_free (copy); + copy = temp; + } + + /* escape */ + if (self->escape) { + temp = g_markup_escape_text (copy, -1); + g_free (copy); + copy = temp; + } + + /* check words for URLS */ + if (self->autolinkify && + self->output == GS_MARKDOWN_OUTPUT_PANGO && + (self->mode == GS_MARKDOWN_MODE_PARA || + self->mode == GS_MARKDOWN_MODE_BULLETT)) { + temp = gs_markdown_word_auto_format_urls (copy); + g_free (copy); + copy = temp; + } + + /* do formatting */ + temp = gs_markdown_to_text_line_format (self, copy); + if (self->mode == GS_MARKDOWN_MODE_BULLETT) { + g_string_append_printf (self->processed, "%s%s%s\n", + self->tags.bullet_start, + temp, + self->tags.bullet_end); + self->line_count++; + } else if (self->mode == GS_MARKDOWN_MODE_H1) { + g_string_append_printf (self->processed, "%s%s%s\n", + self->tags.h1_start, + temp, + self->tags.h1_end); + } else if (self->mode == GS_MARKDOWN_MODE_H2) { + g_string_append_printf (self->processed, "%s%s%s\n", + self->tags.h2_start, + temp, + self->tags.h2_end); + } else if (self->mode == GS_MARKDOWN_MODE_PARA || + self->mode == GS_MARKDOWN_MODE_RULE) { + g_string_append_printf (self->processed, "%s\n", temp); + self->line_count++; + } + + /* clear */ + g_string_truncate (self->pending, 0); +} + +static gboolean +gs_markdown_to_text_line_process (GsMarkdown *self, const gchar *line) +{ + gboolean ret; + + /* blank */ + ret = gs_markdown_to_text_line_is_blank (line); + if (ret) { + gs_markdown_flush_pending (self); + /* a new line after a list is the end of list, not a gap */ + if (self->mode != GS_MARKDOWN_MODE_BULLETT) + ret = gs_markdown_add_pending (self, "\n"); + self->mode = GS_MARKDOWN_MODE_BLANK; + goto out; + } + + /* header1_type2 */ + ret = gs_markdown_to_text_line_is_header1_type2 (line); + if (ret) { + if (self->mode == GS_MARKDOWN_MODE_PARA) + self->mode = GS_MARKDOWN_MODE_H1; + goto out; + } + + /* header2_type2 */ + ret = gs_markdown_to_text_line_is_header2_type2 (line); + if (ret) { + if (self->mode == GS_MARKDOWN_MODE_PARA) + self->mode = GS_MARKDOWN_MODE_H2; + goto out; + } + + /* rule */ + ret = gs_markdown_to_text_line_is_rule (line); + if (ret) { + gs_markdown_flush_pending (self); + self->mode = GS_MARKDOWN_MODE_RULE; + ret = gs_markdown_add_pending (self, self->tags.rule); + goto out; + } + + /* bullet */ + ret = gs_markdown_to_text_line_is_bullet (line); + if (ret) { + gs_markdown_flush_pending (self); + self->mode = GS_MARKDOWN_MODE_BULLETT; + ret = gs_markdown_add_pending (self, &line[2]); + goto out; + } + + /* header1 */ + ret = gs_markdown_to_text_line_is_header1 (line); + if (ret) { + gs_markdown_flush_pending (self); + self->mode = GS_MARKDOWN_MODE_H1; + ret = gs_markdown_add_pending_header (self, &line[2]); + goto out; + } + + /* header2 */ + ret = gs_markdown_to_text_line_is_header2 (line); + if (ret) { + gs_markdown_flush_pending (self); + self->mode = GS_MARKDOWN_MODE_H2; + ret = gs_markdown_add_pending_header (self, &line[3]); + goto out; + } + + /* paragraph */ + if (self->mode == GS_MARKDOWN_MODE_BLANK || + self->mode == GS_MARKDOWN_MODE_UNKNOWN) { + gs_markdown_flush_pending (self); + self->mode = GS_MARKDOWN_MODE_PARA; + } + + /* add to pending */ + ret = gs_markdown_add_pending (self, line); +out: + /* if we failed to add, we don't know the mode */ + if (!ret) + self->mode = GS_MARKDOWN_MODE_UNKNOWN; + return ret; +} + +static void +gs_markdown_set_output_kind (GsMarkdown *self, GsMarkdownOutputKind output) +{ + g_return_if_fail (GS_IS_MARKDOWN (self)); + + self->output = output; + switch (output) { + case GS_MARKDOWN_OUTPUT_PANGO: + /* PangoMarkup */ + self->tags.em_start = "<i>"; + self->tags.em_end = "</i>"; + self->tags.strong_start = "<b>"; + self->tags.strong_end = "</b>"; + self->tags.code_start = "<tt>"; + self->tags.code_end = "</tt>"; + self->tags.h1_start = "<big>"; + self->tags.h1_end = "</big>"; + self->tags.h2_start = "<b>"; + self->tags.h2_end = "</b>"; + self->tags.bullet_start = "• "; + self->tags.bullet_end = ""; + self->tags.rule = "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯\n"; + self->escape = TRUE; + self->autolinkify = TRUE; + break; + case GS_MARKDOWN_OUTPUT_HTML: + /* XHTML */ + self->tags.em_start = "<em>"; + self->tags.em_end = "<em>"; + self->tags.strong_start = "<strong>"; + self->tags.strong_end = "</strong>"; + self->tags.code_start = "<code>"; + self->tags.code_end = "</code>"; + self->tags.h1_start = "<h1>"; + self->tags.h1_end = "</h1>"; + self->tags.h2_start = "<h2>"; + self->tags.h2_end = "</h2>"; + self->tags.bullet_start = "<li>"; + self->tags.bullet_end = "</li>"; + self->tags.rule = "<hr>"; + self->escape = TRUE; + self->autolinkify = TRUE; + break; + case GS_MARKDOWN_OUTPUT_TEXT: + /* plain text */ + self->tags.em_start = ""; + self->tags.em_end = ""; + self->tags.strong_start = ""; + self->tags.strong_end = ""; + self->tags.code_start = ""; + self->tags.code_end = ""; + self->tags.h1_start = "["; + self->tags.h1_end = "]"; + self->tags.h2_start = "-"; + self->tags.h2_end = "-"; + self->tags.bullet_start = "* "; + self->tags.bullet_end = ""; + self->tags.rule = " ----- \n"; + self->escape = FALSE; + self->autolinkify = FALSE; + break; + default: + g_warning ("unknown output enum"); + break; + } +} + +void +gs_markdown_set_max_lines (GsMarkdown *self, gint max_lines) +{ + g_return_if_fail (GS_IS_MARKDOWN (self)); + self->max_lines = max_lines; +} + +void +gs_markdown_set_smart_quoting (GsMarkdown *self, gboolean smart_quoting) +{ + g_return_if_fail (GS_IS_MARKDOWN (self)); + self->smart_quoting = smart_quoting; +} + +void +gs_markdown_set_escape (GsMarkdown *self, gboolean escape) +{ + g_return_if_fail (GS_IS_MARKDOWN (self)); + self->escape = escape; +} + +void +gs_markdown_set_autocode (GsMarkdown *self, gboolean autocode) +{ + g_return_if_fail (GS_IS_MARKDOWN (self)); + self->autocode = autocode; +} + +void +gs_markdown_set_autolinkify (GsMarkdown *self, gboolean autolinkify) +{ + g_return_if_fail (GS_IS_MARKDOWN (self)); + self->autolinkify = autolinkify; +} + +gchar * +gs_markdown_parse (GsMarkdown *self, const gchar *markdown) +{ + gboolean ret; + gchar *temp; + guint i; + guint len; + g_auto(GStrv) lines = NULL; + + g_return_val_if_fail (GS_IS_MARKDOWN (self), NULL); + + /* process */ + self->mode = GS_MARKDOWN_MODE_UNKNOWN; + self->line_count = 0; + g_string_truncate (self->pending, 0); + g_string_truncate (self->processed, 0); + lines = g_strsplit (markdown, "\n", -1); + len = g_strv_length (lines); + + /* process each line */ + for (i = 0; i < len; i++) { + ret = gs_markdown_to_text_line_process (self, lines[i]); + if (!ret) + break; + } + gs_markdown_flush_pending (self); + + /* remove trailing \n */ + while (g_str_has_suffix (self->processed->str, "\n")) + g_string_set_size (self->processed, self->processed->len - 1); + + /* get a copy */ + temp = g_strdup (self->processed->str); + g_string_truncate (self->pending, 0); + g_string_truncate (self->processed, 0); + return temp; +} + +static void +gs_markdown_finalize (GObject *object) +{ + GsMarkdown *self; + + g_return_if_fail (GS_IS_MARKDOWN (object)); + + self = GS_MARKDOWN (object); + + g_string_free (self->pending, TRUE); + g_string_free (self->processed, TRUE); + + G_OBJECT_CLASS (gs_markdown_parent_class)->finalize (object); +} + +static void +gs_markdown_class_init (GsMarkdownClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = gs_markdown_finalize; +} + +static void +gs_markdown_init (GsMarkdown *self) +{ + self->mode = GS_MARKDOWN_MODE_UNKNOWN; + self->pending = g_string_new (""); + self->processed = g_string_new (""); + self->max_lines = -1; + self->smart_quoting = FALSE; + self->escape = FALSE; + self->autocode = FALSE; +} + +GsMarkdown * +gs_markdown_new (GsMarkdownOutputKind output) +{ + GsMarkdown *self; + self = g_object_new (GS_TYPE_MARKDOWN, NULL); + gs_markdown_set_output_kind (self, output); + return GS_MARKDOWN (self); +} diff --git a/plugins/packagekit/gs-markdown.h b/plugins/packagekit/gs-markdown.h new file mode 100644 index 0000000..51e6233 --- /dev/null +++ b/plugins/packagekit/gs-markdown.h @@ -0,0 +1,41 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2008-2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GS_TYPE_MARKDOWN (gs_markdown_get_type ()) + +G_DECLARE_FINAL_TYPE (GsMarkdown, gs_markdown, GS, MARKDOWN, GObject) + +typedef enum { + GS_MARKDOWN_OUTPUT_TEXT, + GS_MARKDOWN_OUTPUT_PANGO, + GS_MARKDOWN_OUTPUT_HTML, + GS_MARKDOWN_OUTPUT_LAST +} GsMarkdownOutputKind; + +GsMarkdown *gs_markdown_new (GsMarkdownOutputKind output); +void gs_markdown_set_max_lines (GsMarkdown *self, + gint max_lines); +void gs_markdown_set_smart_quoting (GsMarkdown *self, + gboolean smart_quoting); +void gs_markdown_set_escape (GsMarkdown *self, + gboolean escape); +void gs_markdown_set_autocode (GsMarkdown *self, + gboolean autocode); +void gs_markdown_set_autolinkify (GsMarkdown *self, + gboolean autolinkify); +gchar *gs_markdown_parse (GsMarkdown *self, + const gchar *text); + +G_END_DECLS diff --git a/plugins/packagekit/gs-packagekit-helper.c b/plugins/packagekit/gs-packagekit-helper.c new file mode 100644 index 0000000..3afadcc --- /dev/null +++ b/plugins/packagekit/gs-packagekit-helper.c @@ -0,0 +1,131 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016-2018 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2019 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib.h> + +#include "gs-packagekit-helper.h" +#include "packagekit-common.h" + +struct _GsPackagekitHelper { + GObject parent_instance; + GHashTable *apps; + GsApp *progress_app; + GsPlugin *plugin; +}; + +G_DEFINE_TYPE (GsPackagekitHelper, gs_packagekit_helper, G_TYPE_OBJECT) + +void +gs_packagekit_helper_cb (PkProgress *progress, PkProgressType type, gpointer user_data) +{ + GsPackagekitHelper *self = (GsPackagekitHelper *) user_data; + GsPlugin *plugin = gs_packagekit_helper_get_plugin (self); + const gchar *package_id = pk_progress_get_package_id (progress); + GsApp *app = NULL; + + /* optional */ + if (self->progress_app != NULL) + app = self->progress_app; + else if (package_id != NULL) + app = gs_packagekit_helper_get_app_by_id (self, package_id); + + if (type == PK_PROGRESS_TYPE_STATUS) { + PkStatusEnum status = pk_progress_get_status (progress); + GsPluginStatus plugin_status = packagekit_status_enum_to_plugin_status (status); + if (plugin_status != GS_PLUGIN_STATUS_UNKNOWN) + gs_plugin_status_update (plugin, app, plugin_status); + } else if (type == PK_PROGRESS_TYPE_PERCENTAGE) { + gint percentage = pk_progress_get_percentage (progress); + if (app != NULL && percentage >= 0 && percentage <= 100) + gs_app_set_progress (app, (guint) percentage); + } + + /* Only go from TRUE to FALSE - it doesn't make sense for a package + * install to become uncancellable later on */ + if (app != NULL && gs_app_get_allow_cancel (app)) + gs_app_set_allow_cancel (app, pk_progress_get_allow_cancel (progress)); +} + +void +gs_packagekit_helper_add_app (GsPackagekitHelper *self, GsApp *app) +{ + GPtrArray *source_ids = gs_app_get_source_ids (app); + + g_return_if_fail (GS_IS_PACKAGEKIT_HELPER (self)); + g_return_if_fail (GS_IS_APP (app)); + + for (guint i = 0; i < source_ids->len; i++) { + const gchar *source_id = g_ptr_array_index (source_ids, i); + g_hash_table_insert (self->apps, + g_strdup (source_id), + g_object_ref (app)); + } +} + +void +gs_packagekit_helper_set_progress_app (GsPackagekitHelper *self, GsApp *progress_app) +{ + g_set_object (&self->progress_app, progress_app); +} + +GsPlugin * +gs_packagekit_helper_get_plugin (GsPackagekitHelper *self) +{ + g_return_val_if_fail (GS_IS_PACKAGEKIT_HELPER (self), NULL); + return self->plugin; +} + +GsApp * +gs_packagekit_helper_get_app_by_id (GsPackagekitHelper *self, const gchar *package_id) +{ + g_return_val_if_fail (GS_IS_PACKAGEKIT_HELPER (self), NULL); + g_return_val_if_fail (package_id != NULL, NULL); + return g_hash_table_lookup (self->apps, package_id); +} + +static void +gs_packagekit_helper_finalize (GObject *object) +{ + GsPackagekitHelper *self; + + g_return_if_fail (GS_IS_PACKAGEKIT_HELPER (object)); + + self = GS_PACKAGEKIT_HELPER (object); + + g_object_unref (self->plugin); + g_clear_object (&self->progress_app); + g_hash_table_unref (self->apps); + + G_OBJECT_CLASS (gs_packagekit_helper_parent_class)->finalize (object); +} + +static void +gs_packagekit_helper_class_init (GsPackagekitHelperClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = gs_packagekit_helper_finalize; +} + +static void +gs_packagekit_helper_init (GsPackagekitHelper *self) +{ + self->apps = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, (GDestroyNotify) g_object_unref); +} + +GsPackagekitHelper * +gs_packagekit_helper_new (GsPlugin *plugin) +{ + GsPackagekitHelper *self; + self = g_object_new (GS_TYPE_PACKAGEKIT_HELPER, NULL); + self->plugin = g_object_ref (plugin); + return GS_PACKAGEKIT_HELPER (self); +} diff --git a/plugins/packagekit/gs-packagekit-helper.h b/plugins/packagekit/gs-packagekit-helper.h new file mode 100644 index 0000000..94a6ea1 --- /dev/null +++ b/plugins/packagekit/gs-packagekit-helper.h @@ -0,0 +1,35 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016-2018 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2019 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> +#include <gnome-software.h> +#include <packagekit-glib2/packagekit.h> + +G_BEGIN_DECLS + +#define GS_TYPE_PACKAGEKIT_HELPER (gs_packagekit_helper_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPackagekitHelper, gs_packagekit_helper, GS, PACKAGEKIT_HELPER, GObject) + +GsPackagekitHelper *gs_packagekit_helper_new (GsPlugin *plugin); +GsPlugin *gs_packagekit_helper_get_plugin (GsPackagekitHelper *self); +void gs_packagekit_helper_add_app (GsPackagekitHelper *self, + GsApp *app); +void gs_packagekit_helper_set_progress_app (GsPackagekitHelper *self, + GsApp *progress_app); +GsApp *gs_packagekit_helper_get_app_by_id (GsPackagekitHelper *self, + const gchar *package_id); +void gs_packagekit_helper_cb (PkProgress *progress, + PkProgressType type, + gpointer user_data); + + +G_END_DECLS diff --git a/plugins/packagekit/gs-plugin-packagekit-history.c b/plugins/packagekit/gs-plugin-packagekit-history.c new file mode 100644 index 0000000..6175316 --- /dev/null +++ b/plugins/packagekit/gs-plugin-packagekit-history.c @@ -0,0 +1,261 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <packagekit-glib2/packagekit.h> + +#include <gnome-software.h> + +#include "packagekit-common.h" + +#define GS_PLUGIN_PACKAGEKIT_HISTORY_TIMEOUT 5000 /* ms */ + +/* + * SECTION: + * This returns update history using the system PackageKit instance. + */ + +struct GsPluginData { + GDBusConnection *connection; +}; + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + gs_plugin_alloc_data (plugin, sizeof(GsPluginData)); + + /* need pkgname */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "appstream"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "packagekit-refine"); +} + +void +gs_plugin_destroy (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + if (priv->connection != NULL) + g_object_unref (priv->connection); +} + +static void +gs_plugin_packagekit_refine_add_history (GsApp *app, GVariant *dict) +{ + const gchar *version; + gboolean ret; + guint64 timestamp; + PkInfoEnum info_enum; + g_autoptr(GsApp) history = NULL; + + /* create new history item with same ID as parent */ + history = gs_app_new (gs_app_get_id (app)); + gs_app_set_kind (history, AS_APP_KIND_GENERIC); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_name (history, GS_APP_QUALITY_NORMAL, gs_app_get_name (app)); + + /* get the installed state */ + ret = g_variant_lookup (dict, "info", "u", &info_enum); + g_assert (ret); + switch (info_enum) { + case PK_INFO_ENUM_INSTALLING: + gs_app_set_state (history, AS_APP_STATE_INSTALLED); + break; + case PK_INFO_ENUM_REMOVING: + gs_app_set_state (history, AS_APP_STATE_AVAILABLE); + break; + case PK_INFO_ENUM_UPDATING: + gs_app_set_state (history, AS_APP_STATE_UPDATABLE); + break; + default: + g_debug ("ignoring history kind: %s", + pk_info_enum_to_string (info_enum)); + return; + } + + /* set the history time and date */ + ret = g_variant_lookup (dict, "timestamp", "t", ×tamp); + g_assert (ret); + gs_app_set_install_date (history, timestamp); + + /* set the history version number */ + ret = g_variant_lookup (dict, "version", "&s", &version); + g_assert (ret); + gs_app_set_version (history, version); + + /* add the package to the main application */ + gs_app_add_history (app, history); + + /* use the last event as approximation of the package timestamp */ + gs_app_set_install_date (app, timestamp); +} + +gboolean +gs_plugin_setup (GsPlugin *plugin, GCancellable *cancellable, GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + priv->connection = g_bus_get_sync (G_BUS_TYPE_SYSTEM, + cancellable, + error); + return priv->connection != NULL; +} + +static gboolean +gs_plugin_packagekit_refine (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + gboolean ret; + guint j; + GsApp *app; + guint i = 0; + GVariantIter iter; + GVariant *value; + g_autofree const gchar **package_names = NULL; + g_autoptr(GError) error_local = NULL; + g_autoptr(GVariant) result = NULL; + g_autoptr(GVariant) tuple = NULL; + + /* get an array of package names */ + package_names = g_new0 (const gchar *, gs_app_list_length (list) + 1); + for (j = 0; j < gs_app_list_length (list); j++) { + app = gs_app_list_index (list, j); + package_names[i++] = gs_app_get_source_default (app); + } + + g_debug ("getting history for %u packages", gs_app_list_length (list)); + result = g_dbus_connection_call_sync (priv->connection, + "org.freedesktop.PackageKit", + "/org/freedesktop/PackageKit", + "org.freedesktop.PackageKit", + "GetPackageHistory", + g_variant_new ("(^asu)", package_names, 0), + NULL, + G_DBUS_CALL_FLAGS_NONE, + GS_PLUGIN_PACKAGEKIT_HISTORY_TIMEOUT, + cancellable, + &error_local); + if (result == NULL) { + if (g_error_matches (error_local, + G_DBUS_ERROR, + G_DBUS_ERROR_UNKNOWN_METHOD)) { + g_debug ("No history available as PackageKit is too old: %s", + error_local->message); + + /* just set this to something non-zero so we don't keep + * trying to call GetPackageHistory */ + for (i = 0; i < gs_app_list_length (list); i++) { + app = gs_app_list_index (list, i); + gs_app_set_install_date (app, GS_APP_INSTALL_DATE_UNKNOWN); + } + } else if (g_error_matches (error_local, + G_IO_ERROR, + G_IO_ERROR_CANCELLED)) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_CANCELLED, + "Failed to get history: %s", + error_local->message); + return FALSE; + } else if (g_error_matches (error_local, + G_IO_ERROR, + G_IO_ERROR_TIMED_OUT)) { + g_debug ("No history as PackageKit took too long: %s", + error_local->message); + for (i = 0; i < gs_app_list_length (list); i++) { + app = gs_app_list_index (list, i); + gs_app_set_install_date (app, GS_APP_INSTALL_DATE_UNKNOWN); + } + } + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "Failed to get history: %s", + error_local->message); + return FALSE; + } + + /* get any results */ + tuple = g_variant_get_child_value (result, 0); + for (i = 0; i < gs_app_list_length (list); i++) { + g_autoptr(GVariant) entries = NULL; + app = gs_app_list_index (list, i); + ret = g_variant_lookup (tuple, + gs_app_get_source_default (app), + "@aa{sv}", + &entries); + if (!ret) { + /* make up a fake entry as we know this package was at + * least installed at some point in time */ + if (gs_app_get_state (app) == AS_APP_STATE_INSTALLED) { + g_autoptr(GsApp) app_dummy = NULL; + app_dummy = gs_app_new (gs_app_get_id (app)); + gs_plugin_packagekit_set_packaging_format (plugin, app); + gs_app_set_metadata (app_dummy, "GnomeSoftware::Creator", + gs_plugin_get_name (plugin)); + gs_app_set_install_date (app_dummy, GS_APP_INSTALL_DATE_UNKNOWN); + gs_app_set_kind (app_dummy, AS_APP_KIND_GENERIC); + gs_app_set_state (app_dummy, AS_APP_STATE_INSTALLED); + gs_app_set_version (app_dummy, gs_app_get_version (app)); + gs_app_add_history (app, app_dummy); + } + gs_app_set_install_date (app, GS_APP_INSTALL_DATE_UNKNOWN); + continue; + } + + /* add history for application */ + g_variant_iter_init (&iter, entries); + while ((value = g_variant_iter_next_value (&iter))) { + gs_plugin_packagekit_refine_add_history (app, value); + g_variant_unref (value); + } + } + return TRUE; +} + +gboolean +gs_plugin_refine (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + gboolean ret; + guint i; + GsApp *app; + GPtrArray *sources; + g_autoptr(GsAppList) packages = NULL; + + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_HISTORY) == 0) + return TRUE; + + /* add any missing history data */ + packages = gs_app_list_new (); + for (i = 0; i < gs_app_list_length (list); i++) { + app = gs_app_list_index (list, i); + if (g_strcmp0 (gs_app_get_management_plugin (app), "packagekit") != 0) + continue; + sources = gs_app_get_sources (app); + if (sources->len == 0) + continue; + if (gs_app_get_install_date (app) != 0) + continue; + gs_app_list_add (packages, app); + } + if (gs_app_list_length (packages) > 0) { + ret = gs_plugin_packagekit_refine (plugin, + packages, + cancellable, + error); + if (!ret) + return FALSE; + } + return TRUE; +} diff --git a/plugins/packagekit/gs-plugin-packagekit-local.c b/plugins/packagekit/gs-plugin-packagekit-local.c new file mode 100644 index 0000000..53b51ac --- /dev/null +++ b/plugins/packagekit/gs-plugin-packagekit-local.c @@ -0,0 +1,271 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <string.h> + +#include <packagekit-glib2/packagekit.h> +#include <gnome-software.h> + +#include "packagekit-common.h" +#include "gs-packagekit-helper.h" + +struct GsPluginData { + PkTask *task; + GMutex task_mutex; +}; + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData)); + + g_mutex_init (&priv->task_mutex); + priv->task = pk_task_new (); + pk_client_set_background (PK_CLIENT (priv->task), FALSE); +} + +void +gs_plugin_destroy (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_mutex_clear (&priv->task_mutex); + g_object_unref (priv->task); +} + +static gboolean +gs_plugin_packagekit_refresh_guess_app_id (GsPlugin *plugin, + GsApp *app, + const gchar *filename, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + g_auto(GStrv) files = NULL; + g_autoptr(PkResults) results = NULL; + g_autoptr(GPtrArray) array = NULL; + g_autoptr(GString) basename_best = g_string_new (NULL); + + /* get file list so we can work out ID */ + files = g_strsplit (filename, "\t", -1); + gs_packagekit_helper_add_app (helper, app); + g_mutex_lock (&priv->task_mutex); + results = pk_client_get_files_local (PK_CLIENT (priv->task), + files, + cancellable, + gs_packagekit_helper_cb, helper, + error); + g_mutex_unlock (&priv->task_mutex); + if (!gs_plugin_packagekit_results_valid (results, error)) { + gs_utils_error_add_origin_id (error, app); + return FALSE; + } + array = pk_results_get_files_array (results); + if (array->len == 0) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "no files for %s", filename); + return FALSE; + } + + /* find the smallest length desktop file, on the logic that + * ${app}.desktop is going to be better than ${app}-${action}.desktop */ + for (guint i = 0; i < array->len; i++) { + PkFiles *item = g_ptr_array_index (array, i); + gchar **fns = pk_files_get_files (item); + for (guint j = 0; fns[j] != NULL; j++) { + if (g_str_has_prefix (fns[j], "/etc/yum.repos.d/") && + g_str_has_suffix (fns[j], ".repo")) { + gs_app_add_quirk (app, GS_APP_QUIRK_HAS_SOURCE); + } + if (g_str_has_prefix (fns[j], "/usr/share/applications/") && + g_str_has_suffix (fns[j], ".desktop")) { + g_autofree gchar *basename = g_path_get_basename (fns[j]); + if (basename_best->len == 0 || + strlen (basename) < basename_best->len) + g_string_assign (basename_best, basename); + } + } + } + if (basename_best->len > 0) { + gs_app_set_kind (app, AS_APP_KIND_DESKTOP); + gs_app_set_id (app, basename_best->str); + } + + return TRUE; +} + +static void +add_quirks_from_package_name (GsApp *app, const gchar *package_name) +{ + /* these packages don't have a .repo file in their file lists, but + * instead install one through rpm scripts / cron job */ + const gchar *packages_with_repos[] = { + "google-chrome-stable", + "google-earth-pro-stable", + "google-talkplugin", + NULL }; + + if (g_strv_contains (packages_with_repos, package_name)) + gs_app_add_quirk (app, GS_APP_QUIRK_HAS_SOURCE); +} + +static gboolean +gs_plugin_packagekit_local_check_installed (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + PkBitfield filter; + const gchar *names[] = { gs_app_get_source_default (app), NULL }; + g_autoptr(GPtrArray) packages = NULL; + g_autoptr(PkResults) results = NULL; + + filter = pk_bitfield_from_enums (PK_FILTER_ENUM_NEWEST, + PK_FILTER_ENUM_ARCH, + PK_FILTER_ENUM_INSTALLED, + -1); + results = pk_client_resolve (PK_CLIENT (priv->task), filter, (gchar **) names, + cancellable, NULL, NULL, error); + if (results == NULL) + return FALSE; + packages = pk_results_get_package_array (results); + if (packages->len > 0) { + gs_app_set_state (app, AS_APP_STATE_UNKNOWN); + gs_app_set_state (app, AS_APP_STATE_INSTALLED); + for (guint i = 0; i < packages->len; i++){ + PkPackage *pkg = g_ptr_array_index (packages, i); + gs_app_add_source_id (app, pk_package_get_id (pkg)); + } + } + return TRUE; +} + +gboolean +gs_plugin_file_to_app (GsPlugin *plugin, + GsAppList *list, + GFile *file, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + const gchar *package_id; + PkDetails *item; + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + g_autoptr(PkResults) results = NULL; + g_autofree gchar *content_type = NULL; + g_autofree gchar *filename = NULL; + g_autofree gchar *license_spdx = NULL; + g_auto(GStrv) files = NULL; + g_auto(GStrv) split = NULL; + g_autoptr(GPtrArray) array = NULL; + g_autoptr(GsApp) app = NULL; + const gchar *mimetypes[] = { + "application/x-app-package", + "application/x-deb", + "application/vnd.debian.binary-package", + "application/x-redhat-package-manager", + "application/x-rpm", + NULL }; + + /* does this match any of the mimetypes we support */ + content_type = gs_utils_get_content_type (file, cancellable, error); + if (content_type == NULL) + return FALSE; + if (!g_strv_contains (mimetypes, content_type)) + return TRUE; + + /* get details */ + filename = g_file_get_path (file); + files = g_strsplit (filename, "\t", -1); + g_mutex_lock (&priv->task_mutex); + pk_client_set_cache_age (PK_CLIENT (priv->task), G_MAXUINT); + results = pk_client_get_details_local (PK_CLIENT (priv->task), + files, + cancellable, + gs_packagekit_helper_cb, helper, + error); + g_mutex_unlock (&priv->task_mutex); + if (!gs_plugin_packagekit_results_valid (results, error)) + return FALSE; + + /* get results */ + array = pk_results_get_details_array (results); + if (array->len == 0) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "no details for %s", filename); + return FALSE; + } + if (array->len > 1) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "too many details [%u] for %s", + array->len, filename); + return FALSE; + } + + /* create application */ + item = g_ptr_array_index (array, 0); + app = gs_app_new (NULL); + gs_plugin_packagekit_set_packaging_format (plugin, app); + gs_app_set_metadata (app, "GnomeSoftware::Creator", + gs_plugin_get_name (plugin)); + package_id = pk_details_get_package_id (item); + split = pk_package_id_split (package_id); + if (split == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "invalid package-id: %s", package_id); + return FALSE; + } + gs_app_set_management_plugin (app, "packagekit"); + gs_app_set_kind (app, AS_APP_KIND_GENERIC); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_state (app, AS_APP_STATE_AVAILABLE_LOCAL); + gs_app_set_name (app, GS_APP_QUALITY_LOWEST, split[PK_PACKAGE_ID_NAME]); + gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, + pk_details_get_summary (item)); + gs_app_set_version (app, split[PK_PACKAGE_ID_VERSION]); + gs_app_add_source (app, split[PK_PACKAGE_ID_NAME]); + gs_app_add_source_id (app, package_id); + gs_app_set_description (app, GS_APP_QUALITY_LOWEST, + pk_details_get_description (item)); + gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, pk_details_get_url (item)); + gs_app_set_size_installed (app, pk_details_get_size (item)); + gs_app_set_size_download (app, 0); + license_spdx = as_utils_license_to_spdx (pk_details_get_license (item)); + gs_app_set_license (app, GS_APP_QUALITY_LOWEST, license_spdx); + add_quirks_from_package_name (app, split[PK_PACKAGE_ID_NAME]); + + /* is already installed? */ + if (!gs_plugin_packagekit_local_check_installed (plugin, + app, + cancellable, + error)) + return FALSE; + + /* look for a desktop file so we can use a valid application id */ + if (!gs_plugin_packagekit_refresh_guess_app_id (plugin, + app, + filename, + cancellable, + error)) + return FALSE; + + gs_app_list_add (list, app); + return TRUE; +} diff --git a/plugins/packagekit/gs-plugin-packagekit-offline.c b/plugins/packagekit/gs-plugin-packagekit-offline.c new file mode 100644 index 0000000..7e15720 --- /dev/null +++ b/plugins/packagekit/gs-plugin-packagekit-offline.c @@ -0,0 +1,181 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015-2017 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <packagekit-glib2/packagekit.h> + +#include <gnome-software.h> + +#include "packagekit-common.h" + +/* + * SECTION: + * This adds historical updates to the application history. + * + * Note: when this is cleared by one user is is unavailable for all + * other users. + */ + +static gboolean +gs_plugin_packagekit_convert_error (GError **error, + PkErrorEnum error_enum, + const gchar *details) +{ + switch (error_enum) { + case PK_ERROR_ENUM_PACKAGE_DOWNLOAD_FAILED: + case PK_ERROR_ENUM_NO_CACHE: + case PK_ERROR_ENUM_NO_NETWORK: + case PK_ERROR_ENUM_NO_MORE_MIRRORS_TO_TRY: + case PK_ERROR_ENUM_CANNOT_FETCH_SOURCES: + case PK_ERROR_ENUM_UNFINISHED_TRANSACTION: + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NO_NETWORK, + details); + break; + case PK_ERROR_ENUM_BAD_GPG_SIGNATURE: + case PK_ERROR_ENUM_CANNOT_UPDATE_REPO_UNSIGNED: + case PK_ERROR_ENUM_GPG_FAILURE: + case PK_ERROR_ENUM_MISSING_GPG_SIGNATURE: + case PK_ERROR_ENUM_PACKAGE_CORRUPT: + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NO_SECURITY, + details); + break; + case PK_ERROR_ENUM_TRANSACTION_CANCELLED: + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_CANCELLED, + details); + break; + case PK_ERROR_ENUM_NO_PACKAGES_TO_UPDATE: + case PK_ERROR_ENUM_UPDATE_NOT_FOUND: + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + details); + break; + case PK_ERROR_ENUM_NO_SPACE_ON_DEVICE: + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NO_SPACE, + details); + break; + default: + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED, + details); + break; + } + return FALSE; +} + +gboolean +gs_plugin_add_updates_historical (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + guint64 mtime; + guint i; + g_autoptr(GPtrArray) package_array = NULL; + g_autoptr(GError) error_local = NULL; + g_autoptr(PkResults) results = NULL; + PkExitEnum exit_code; + + /* get the results */ + results = pk_offline_get_results (&error_local); + if (results == NULL) { + /* was any offline update attempted */ + if (g_error_matches (error_local, + PK_OFFLINE_ERROR, + PK_OFFLINE_ERROR_NO_DATA)) { + return TRUE; + } + + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "Failed to get offline update results: %s", + error_local->message); + return FALSE; + } + + /* get the mtime of the results */ + mtime = pk_offline_get_results_mtime (error); + if (mtime == 0) + return FALSE; + + /* only return results if successful */ + exit_code = pk_results_get_exit_code (results); + if (exit_code != PK_EXIT_ENUM_SUCCESS) { + g_autoptr(PkError) error_code = NULL; + + error_code = pk_results_get_error_code (results); + if (error_code == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED, + "Offline update failed without error_code set"); + return FALSE; + } + + return gs_plugin_packagekit_convert_error (error, + pk_error_get_code (error_code), + pk_error_get_details (error_code)); + } + + /* distro upgrade? */ + if (pk_results_get_role (results) == PK_ROLE_ENUM_UPGRADE_SYSTEM) { + g_autoptr(GsApp) app = NULL; + + app = gs_app_new (NULL); + gs_app_set_from_unique_id (app, "*/*/*/*/system/*"); + gs_app_set_management_plugin (app, "packagekit"); + gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD); + gs_app_set_state (app, AS_APP_STATE_UNKNOWN); + gs_app_set_kind (app, AS_APP_KIND_OS_UPGRADE); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_install_date (app, mtime); + gs_app_set_metadata (app, "GnomeSoftware::Creator", + gs_plugin_get_name (plugin)); + gs_app_list_add (list, app); + + return TRUE; + } + + /* get list of package-ids */ + package_array = pk_results_get_package_array (results); + for (i = 0; i < package_array->len; i++) { + PkPackage *pkg = g_ptr_array_index (package_array, i); + const gchar *package_id; + g_autoptr(GsApp) app = NULL; + g_auto(GStrv) split = NULL; + + app = gs_app_new (NULL); + package_id = pk_package_get_id (pkg); + split = g_strsplit (package_id, ";", 4); + gs_plugin_packagekit_set_packaging_format (plugin, app); + gs_app_add_source (app, split[0]); + gs_app_set_update_version (app, split[1]); + gs_app_set_management_plugin (app, "packagekit"); + gs_app_add_source_id (app, package_id); + gs_app_set_state (app, AS_APP_STATE_UPDATABLE); + gs_app_set_kind (app, AS_APP_KIND_GENERIC); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_install_date (app, mtime); + gs_app_set_metadata (app, "GnomeSoftware::Creator", + gs_plugin_get_name (plugin)); + gs_app_list_add (list, app); + } + return TRUE; +} diff --git a/plugins/packagekit/gs-plugin-packagekit-proxy.c b/plugins/packagekit/gs-plugin-packagekit-proxy.c new file mode 100644 index 0000000..a5bbcc7 --- /dev/null +++ b/plugins/packagekit/gs-plugin-packagekit-proxy.c @@ -0,0 +1,316 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <packagekit-glib2/packagekit.h> +#include <string.h> +#include <gsettings-desktop-schemas/gdesktop-enums.h> +#include <gnome-software.h> + +/* + * SECTION: + * Sets the session proxy on the system PackageKit instance + */ + +struct GsPluginData { + PkControl *control; + GSettings *settings; + GSettings *settings_http; + GSettings *settings_https; + GSettings *settings_ftp; + GSettings *settings_socks; +}; + +static gchar * +get_proxy_http (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + gboolean ret; + GString *string = NULL; + gint port; + GDesktopProxyMode proxy_mode; + g_autofree gchar *host = NULL; + g_autofree gchar *password = NULL; + g_autofree gchar *username = NULL; + + proxy_mode = g_settings_get_enum (priv->settings, "mode"); + if (proxy_mode != G_DESKTOP_PROXY_MODE_MANUAL) + return NULL; + + host = g_settings_get_string (priv->settings_http, "host"); + if (host == NULL) + return NULL; + + port = g_settings_get_int (priv->settings_http, "port"); + + ret = g_settings_get_boolean (priv->settings_http, + "use-authentication"); + if (ret) { + username = g_settings_get_string (priv->settings_http, + "authentication-user"); + password = g_settings_get_string (priv->settings_http, + "authentication-password"); + } + + /* make PackageKit proxy string */ + string = g_string_new (""); + if (username != NULL || password != NULL) { + if (username != NULL) + g_string_append_printf (string, "%s", username); + if (password != NULL) + g_string_append_printf (string, ":%s", password); + g_string_append (string, "@"); + } + g_string_append (string, host); + if (port > 0) + g_string_append_printf (string, ":%i", port); + return g_string_free (string, FALSE); +} + +static gchar * +get_proxy_https (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + GString *string = NULL; + gint port; + GDesktopProxyMode proxy_mode; + g_autofree gchar *host = NULL; + + proxy_mode = g_settings_get_enum (priv->settings, "mode"); + if (proxy_mode != G_DESKTOP_PROXY_MODE_MANUAL) + return NULL; + + host = g_settings_get_string (priv->settings_https, "host"); + if (host == NULL) + return NULL; + port = g_settings_get_int (priv->settings_https, "port"); + if (port == 0) + return NULL; + + /* make PackageKit proxy string */ + string = g_string_new (host); + if (port > 0) + g_string_append_printf (string, ":%i", port); + return g_string_free (string, FALSE); +} + +static gchar * +get_proxy_ftp (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + GString *string = NULL; + gint port; + GDesktopProxyMode proxy_mode; + g_autofree gchar *host = NULL; + + proxy_mode = g_settings_get_enum (priv->settings, "mode"); + if (proxy_mode != G_DESKTOP_PROXY_MODE_MANUAL) + return NULL; + + host = g_settings_get_string (priv->settings_ftp, "host"); + if (host == NULL) + return NULL; + port = g_settings_get_int (priv->settings_ftp, "port"); + if (port == 0) + return NULL; + + /* make PackageKit proxy string */ + string = g_string_new (host); + if (port > 0) + g_string_append_printf (string, ":%i", port); + return g_string_free (string, FALSE); +} + +static gchar * +get_proxy_socks (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + GString *string = NULL; + gint port; + GDesktopProxyMode proxy_mode; + g_autofree gchar *host = NULL; + + proxy_mode = g_settings_get_enum (priv->settings, "mode"); + if (proxy_mode != G_DESKTOP_PROXY_MODE_MANUAL) + return NULL; + + host = g_settings_get_string (priv->settings_socks, "host"); + if (host == NULL) + return NULL; + port = g_settings_get_int (priv->settings_socks, "port"); + if (port == 0) + return NULL; + + /* make PackageKit proxy string */ + string = g_string_new (host); + if (port > 0) + g_string_append_printf (string, ":%i", port); + return g_string_free (string, FALSE); +} + +static gchar * +get_no_proxy (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + GString *string = NULL; + GDesktopProxyMode proxy_mode; + g_autofree gchar **hosts = NULL; + guint i; + + proxy_mode = g_settings_get_enum (priv->settings, "mode"); + if (proxy_mode != G_DESKTOP_PROXY_MODE_MANUAL) + return NULL; + + hosts = g_settings_get_strv (priv->settings, "ignore-hosts"); + if (hosts == NULL) + return NULL; + + /* make PackageKit proxy string */ + string = g_string_new (""); + for (i = 0; hosts[i] != NULL; i++) { + if (i == 0) + g_string_assign (string, hosts[i]); + else + g_string_append_printf (string, ",%s", hosts[i]); + g_free (hosts[i]); + } + + return g_string_free (string, FALSE); +} + +static gchar * +get_pac (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + GDesktopProxyMode proxy_mode; + gchar *url = NULL; + + proxy_mode = g_settings_get_enum (priv->settings, "mode"); + if (proxy_mode != G_DESKTOP_PROXY_MODE_AUTO) + return NULL; + + url = g_settings_get_string (priv->settings, "autoconfig-url"); + if (url == NULL) + return NULL; + + return url; +} + +static void +set_proxy_cb (GObject *object, GAsyncResult *res, gpointer user_data) +{ + g_autoptr(GError) error = NULL; + if (!pk_control_set_proxy_finish (PK_CONTROL (object), res, &error)) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("failed to set proxies: %s", error->message); + } +} + +static void +reload_proxy_settings (GsPlugin *plugin, GCancellable *cancellable) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autofree gchar *proxy_http = NULL; + g_autofree gchar *proxy_https = NULL; + g_autofree gchar *proxy_ftp = NULL; + g_autofree gchar *proxy_socks = NULL; + g_autofree gchar *no_proxy = NULL; + g_autofree gchar *pac = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GPermission) permission = NULL; + + /* only if we can achieve the action *without* an auth dialog */ + permission = gs_utils_get_permission ("org.freedesktop.packagekit." + "system-network-proxy-configure", + cancellable, &error); + if (permission == NULL) { + g_debug ("not setting proxy as no permission: %s", error->message); + return; + } + if (!g_permission_get_allowed (permission)) { + g_debug ("not setting proxy as no auth requested"); + return; + } + + proxy_http = get_proxy_http (plugin); + proxy_https = get_proxy_https (plugin); + proxy_ftp = get_proxy_ftp (plugin); + proxy_socks = get_proxy_socks (plugin); + no_proxy = get_no_proxy (plugin); + pac = get_pac (plugin); + + g_debug ("Setting proxies (http: %s, https: %s, ftp: %s, socks: %s, " + "no_proxy: %s, pac: %s)", + proxy_http, proxy_https, proxy_ftp, proxy_socks, + no_proxy, pac); + + pk_control_set_proxy2_async (priv->control, + proxy_http, + proxy_https, + proxy_ftp, + proxy_socks, + no_proxy, + pac, + cancellable, + set_proxy_cb, + plugin); +} + +static void +gs_plugin_packagekit_proxy_changed_cb (GSettings *settings, + const gchar *key, + GsPlugin *plugin) +{ + if (!gs_plugin_get_enabled (plugin)) + return; + reload_proxy_settings (plugin, NULL); +} + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData)); + priv->control = pk_control_new (); + priv->settings = g_settings_new ("org.gnome.system.proxy"); + g_signal_connect (priv->settings, "changed", + G_CALLBACK (gs_plugin_packagekit_proxy_changed_cb), plugin); + + priv->settings_http = g_settings_new ("org.gnome.system.proxy.http"); + priv->settings_https = g_settings_new ("org.gnome.system.proxy.https"); + priv->settings_ftp = g_settings_new ("org.gnome.system.proxy.ftp"); + priv->settings_socks = g_settings_new ("org.gnome.system.proxy.socks"); + g_signal_connect (priv->settings_http, "changed", + G_CALLBACK (gs_plugin_packagekit_proxy_changed_cb), plugin); + g_signal_connect (priv->settings_https, "changed", + G_CALLBACK (gs_plugin_packagekit_proxy_changed_cb), plugin); + g_signal_connect (priv->settings_ftp, "changed", + G_CALLBACK (gs_plugin_packagekit_proxy_changed_cb), plugin); + g_signal_connect (priv->settings_socks, "changed", + G_CALLBACK (gs_plugin_packagekit_proxy_changed_cb), plugin); +} + +gboolean +gs_plugin_setup (GsPlugin *plugin, GCancellable *cancellable, GError **error) +{ + reload_proxy_settings (plugin, cancellable); + return TRUE; +} + +void +gs_plugin_destroy (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_object_unref (priv->control); + g_object_unref (priv->settings); + g_object_unref (priv->settings_http); + g_object_unref (priv->settings_https); + g_object_unref (priv->settings_ftp); + g_object_unref (priv->settings_socks); +} diff --git a/plugins/packagekit/gs-plugin-packagekit-refine-repos.c b/plugins/packagekit/gs-plugin-packagekit-refine-repos.c new file mode 100644 index 0000000..4b41c44 --- /dev/null +++ b/plugins/packagekit/gs-plugin-packagekit-refine-repos.c @@ -0,0 +1,123 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <packagekit-glib2/packagekit.h> +#include <gnome-software.h> + +#include "gs-packagekit-helper.h" +#include "packagekit-common.h" + +/* + * SECTION: + * Uses the system PackageKit instance to return convert repo filenames to + * package-ids. + * + * Requires: | [repos::repo-filename] + * Refines: | [source-id] + */ + +struct GsPluginData { + PkClient *client; + GMutex client_mutex; +}; + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData)); + + g_mutex_init (&priv->client_mutex); + priv->client = pk_client_new (); + pk_client_set_background (priv->client, FALSE); + pk_client_set_cache_age (priv->client, G_MAXUINT); + + /* need repos::repo-filename */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "repos"); +} + +void +gs_plugin_destroy (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_mutex_clear (&priv->client_mutex); + g_object_unref (priv->client); +} + +static gboolean +gs_plugin_packagekit_refine_repo_from_filename (GsPlugin *plugin, + GsApp *app, + const gchar *filename, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + const gchar *to_array[] = { NULL, NULL }; + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + g_autoptr(PkResults) results = NULL; + g_autoptr(GPtrArray) packages = NULL; + + to_array[0] = filename; + gs_packagekit_helper_add_app (helper, app); + g_mutex_lock (&priv->client_mutex); + results = pk_client_search_files (priv->client, + pk_bitfield_from_enums (PK_FILTER_ENUM_INSTALLED, -1), + (gchar **) to_array, + cancellable, + gs_packagekit_helper_cb, helper, + error); + g_mutex_unlock (&priv->client_mutex); + if (!gs_plugin_packagekit_results_valid (results, error)) { + g_prefix_error (error, "failed to search file %s: ", filename); + return FALSE; + } + + /* get results */ + packages = pk_results_get_package_array (results); + if (packages->len == 1) { + PkPackage *package = g_ptr_array_index (packages, 0); + gs_app_add_source_id (app, pk_package_get_id (package)); + } else { + g_debug ("failed to find one package for repo %s, %s, [%u]", + gs_app_get_id (app), filename, packages->len); + } + 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); + const gchar *fn; + if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD)) + continue; + if (gs_app_get_kind (app) != AS_APP_KIND_SOURCE) + continue; + if (g_strcmp0 (gs_app_get_management_plugin (app), "packagekit") != 0) + continue; + fn = gs_app_get_metadata_item (app, "repos::repo-filename"); + if (fn == NULL) + continue; + /* set the source package name for an installed .repo file */ + if (!gs_plugin_packagekit_refine_repo_from_filename (plugin, + app, + fn, + cancellable, + error)) + return FALSE; + } + + /* success */ + return TRUE; +} diff --git a/plugins/packagekit/gs-plugin-packagekit-refine.c b/plugins/packagekit/gs-plugin-packagekit-refine.c new file mode 100644 index 0000000..68f7eb6 --- /dev/null +++ b/plugins/packagekit/gs-plugin-packagekit-refine.c @@ -0,0 +1,822 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <packagekit-glib2/packagekit.h> +#include <gnome-software.h> + +#include "gs-markdown.h" +#include "gs-packagekit-helper.h" +#include "packagekit-common.h" + +/* + * SECTION: + * Uses the system PackageKit instance to return convert filenames to + * package-ids and to also discover update details about a package. + * + * Requires: | [id] + * Refines: | [source-id], [installed] + */ + +struct GsPluginData { + PkControl *control; + PkClient *client; + GMutex client_mutex; +}; + +static void +gs_plugin_packagekit_updates_changed_cb (PkControl *control, GsPlugin *plugin) +{ + gs_plugin_updates_changed (plugin); +} + +static void +gs_plugin_packagekit_repo_list_changed_cb (PkControl *control, GsPlugin *plugin) +{ + gs_plugin_reload (plugin); +} + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData)); + + g_mutex_init (&priv->client_mutex); + priv->client = pk_client_new (); + priv->control = pk_control_new (); + g_signal_connect (priv->control, "updates-changed", + G_CALLBACK (gs_plugin_packagekit_updates_changed_cb), plugin); + g_signal_connect (priv->control, "repo-list-changed", + G_CALLBACK (gs_plugin_packagekit_repo_list_changed_cb), plugin); + pk_client_set_background (priv->client, FALSE); + pk_client_set_cache_age (priv->client, G_MAXUINT); + + /* need pkgname and ID */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "appstream"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "packagekit"); +} + +void +gs_plugin_destroy (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_mutex_clear (&priv->client_mutex); + g_object_unref (priv->client); + g_object_unref (priv->control); +} + +void +gs_plugin_adopt_app (GsPlugin *plugin, GsApp *app) +{ + if (gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_PACKAGE && + gs_app_get_scope (app) == AS_APP_SCOPE_SYSTEM) { + gs_app_set_management_plugin (app, "packagekit"); + gs_plugin_packagekit_set_packaging_format (plugin, app); + return; + } +} + +static gboolean +gs_plugin_packagekit_resolve_packages_with_filter (GsPlugin *plugin, + GsAppList *list, + PkBitfield filter, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + GPtrArray *sources; + GsApp *app; + const gchar *pkgname; + guint i; + guint j; + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + g_autoptr(PkResults) results = NULL; + g_autoptr(GPtrArray) package_ids = NULL; + g_autoptr(GPtrArray) packages = NULL; + + package_ids = g_ptr_array_new_with_free_func (g_free); + for (i = 0; i < gs_app_list_length (list); i++) { + app = gs_app_list_index (list, i); + sources = gs_app_get_sources (app); + for (j = 0; j < sources->len; j++) { + pkgname = g_ptr_array_index (sources, j); + if (pkgname == NULL || pkgname[0] == '\0') { + g_warning ("invalid pkgname '%s' for %s", + pkgname, + gs_app_get_unique_id (app)); + continue; + } + g_ptr_array_add (package_ids, g_strdup (pkgname)); + } + } + if (package_ids->len == 0) + return TRUE; + g_ptr_array_add (package_ids, NULL); + + /* resolve them all at once */ + g_mutex_lock (&priv->client_mutex); + results = pk_client_resolve (priv->client, + filter, + (gchar **) package_ids->pdata, + cancellable, + gs_packagekit_helper_cb, helper, + error); + g_mutex_unlock (&priv->client_mutex); + if (!gs_plugin_packagekit_results_valid (results, error)) { + g_prefix_error (error, "failed to resolve package_ids: "); + return FALSE; + } + + /* get results */ + packages = pk_results_get_package_array (results); + + /* if the user types more characters we'll get cancelled - don't go on + * to mark apps as unavailable because packages->len = 0 */ + if (g_cancellable_set_error_if_cancelled (cancellable, error)) { + gs_utils_error_convert_gio (error); + return FALSE; + } + + for (i = 0; i < gs_app_list_length (list); i++) { + app = gs_app_list_index (list, i); + if (gs_app_get_local_file (app) != NULL) + continue; + gs_plugin_packagekit_resolve_packages_app (plugin, packages, app); + } + return TRUE; +} + +static gboolean +gs_plugin_packagekit_resolve_packages (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + PkBitfield filter; + g_autoptr(GsAppList) resolve2_list = NULL; + + /* first, try to resolve packages with ARCH filter */ + filter = pk_bitfield_from_enums (PK_FILTER_ENUM_NEWEST, + PK_FILTER_ENUM_ARCH, + -1); + if (!gs_plugin_packagekit_resolve_packages_with_filter (plugin, + list, + filter, + cancellable, + error)) { + return FALSE; + } + + /* if any packages remaining in UNKNOWN state, try to resolve them again, + * but this time without ARCH filter */ + resolve2_list = gs_app_list_new (); + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + if (gs_app_get_state (app) == AS_APP_STATE_UNKNOWN) + gs_app_list_add (resolve2_list, app); + } + filter = pk_bitfield_from_enums (PK_FILTER_ENUM_NEWEST, + PK_FILTER_ENUM_NOT_ARCH, + PK_FILTER_ENUM_NOT_SOURCE, + -1); + if (!gs_plugin_packagekit_resolve_packages_with_filter (plugin, + resolve2_list, + filter, + cancellable, + error)) { + return FALSE; + } + + return TRUE; +} + +static gboolean +gs_plugin_packagekit_refine_from_desktop (GsPlugin *plugin, + GsApp *app, + const gchar *filename, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + const gchar *to_array[] = { NULL, NULL }; + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + g_autoptr(PkResults) results = NULL; + g_autoptr(GPtrArray) packages = NULL; + + to_array[0] = filename; + gs_packagekit_helper_add_app (helper, app); + g_mutex_lock (&priv->client_mutex); + results = pk_client_search_files (priv->client, + pk_bitfield_from_enums (PK_FILTER_ENUM_INSTALLED, -1), + (gchar **) to_array, + cancellable, + gs_packagekit_helper_cb, helper, + error); + g_mutex_unlock (&priv->client_mutex); + if (!gs_plugin_packagekit_results_valid (results, error)) { + g_prefix_error (error, "failed to search file %s: ", filename); + return FALSE; + } + + /* get results */ + packages = pk_results_get_package_array (results); + if (packages->len == 1) { + PkPackage *package; + package = g_ptr_array_index (packages, 0); + gs_plugin_packagekit_set_metadata_from_package (plugin, app, package); + } else { + g_warning ("Failed to find one package for %s, %s, [%u]", + gs_app_get_id (app), filename, packages->len); + } + return TRUE; +} + +/* + * gs_plugin_packagekit_fixup_update_description: + * + * Lets assume Fedora is sending us valid markdown, but fall back to + * plain text if this fails. + */ +static gchar * +gs_plugin_packagekit_fixup_update_description (const gchar *text) +{ + gchar *tmp; + g_autoptr(GsMarkdown) markdown = NULL; + + /* nothing to do */ + if (text == NULL) + return NULL; + + /* try to parse */ + markdown = gs_markdown_new (GS_MARKDOWN_OUTPUT_TEXT); + gs_markdown_set_smart_quoting (markdown, FALSE); + gs_markdown_set_autocode (markdown, FALSE); + gs_markdown_set_autolinkify (markdown, FALSE); + tmp = gs_markdown_parse (markdown, text); + if (tmp != NULL) + return tmp; + return g_strdup (text); +} + +static gboolean +gs_plugin_packagekit_refine_updatedetails (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + const gchar *package_id; + guint j; + GsApp *app; + guint cnt = 0; + PkUpdateDetail *update_detail; + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + g_autofree const gchar **package_ids = NULL; + g_autoptr(PkResults) results = NULL; + g_autoptr(GPtrArray) array = NULL; + + package_ids = g_new0 (const gchar *, gs_app_list_length (list) + 1); + for (guint i = 0; i < gs_app_list_length (list); i++) { + app = gs_app_list_index (list, i); + package_id = gs_app_get_source_id_default (app); + if (package_id != NULL) + package_ids[cnt++] = package_id; + } + + /* nothing to do */ + if (cnt == 0) + return TRUE; + + /* get any update details */ + g_mutex_lock (&priv->client_mutex); + results = pk_client_get_update_detail (priv->client, + (gchar **) package_ids, + cancellable, + gs_packagekit_helper_cb, helper, + error); + g_mutex_unlock (&priv->client_mutex); + if (!gs_plugin_packagekit_results_valid (results, error)) { + g_prefix_error (error, "failed to get update details for %s: ", + package_ids[0]); + return FALSE; + } + + /* set the update details for the update */ + array = pk_results_get_update_detail_array (results); + for (j = 0; j < gs_app_list_length (list); j++) { + app = gs_app_list_index (list, j); + package_id = gs_app_get_source_id_default (app); + for (guint i = 0; i < array->len; i++) { + const gchar *tmp; + g_autofree gchar *desc = NULL; + /* right package? */ + update_detail = g_ptr_array_index (array, i); + if (g_strcmp0 (package_id, pk_update_detail_get_package_id (update_detail)) != 0) + continue; + tmp = pk_update_detail_get_update_text (update_detail); + desc = gs_plugin_packagekit_fixup_update_description (tmp); + if (desc != NULL) + gs_app_set_update_details (app, desc); + break; + } + } + return TRUE; +} + +static gboolean +gs_plugin_packagekit_refine_details2 (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + GPtrArray *source_ids; + GsApp *app; + const gchar *package_id; + guint i, j; + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + g_autoptr(GPtrArray) array = NULL; + g_autoptr(GPtrArray) package_ids = NULL; + g_autoptr(PkResults) results = NULL; + g_autoptr(GHashTable) details_collection = NULL; + + package_ids = g_ptr_array_new_with_free_func (g_free); + for (i = 0; i < gs_app_list_length (list); i++) { + app = gs_app_list_index (list, i); + source_ids = gs_app_get_source_ids (app); + for (j = 0; j < source_ids->len; j++) { + package_id = g_ptr_array_index (source_ids, j); + g_ptr_array_add (package_ids, g_strdup (package_id)); + } + } + if (package_ids->len == 0) + return TRUE; + g_ptr_array_add (package_ids, NULL); + + /* get any details */ + g_mutex_lock (&priv->client_mutex); + results = pk_client_get_details (priv->client, + (gchar **) package_ids->pdata, + cancellable, + gs_packagekit_helper_cb, helper, + error); + g_mutex_unlock (&priv->client_mutex); + if (!gs_plugin_packagekit_results_valid (results, error)) { + g_autofree gchar *package_ids_str = g_strjoinv (",", (gchar **) package_ids->pdata); + g_prefix_error (error, "failed to get details for %s: ", + package_ids_str); + return FALSE; + } + + /* get the results and copy them into a hash table for fast lookups: + * there are typically 400 to 700 elements in @array, and 100 to 200 + * elements in @list, each with 1 or 2 source IDs to look up (but + * sometimes 200) */ + array = pk_results_get_details_array (results); + details_collection = gs_plugin_packagekit_details_array_to_hash (array); + + /* set the update details for the update */ + for (i = 0; i < gs_app_list_length (list); i++) { + app = gs_app_list_index (list, i); + gs_plugin_packagekit_refine_details_app (plugin, details_collection, app); + } + + return TRUE; +} + +static gboolean +gs_plugin_packagekit_refine_update_urgency (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + guint i; + GsApp *app; + const gchar *package_id; + PkBitfield filter; + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + g_autoptr(PkPackageSack) sack = NULL; + g_autoptr(PkResults) results = NULL; + + /* not required */ + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_SEVERITY) == 0) + return TRUE; + + /* get the list of updates */ + filter = pk_bitfield_value (PK_FILTER_ENUM_NONE); + g_mutex_lock (&priv->client_mutex); + results = pk_client_get_updates (priv->client, + filter, + cancellable, + gs_packagekit_helper_cb, helper, + error); + g_mutex_unlock (&priv->client_mutex); + if (!gs_plugin_packagekit_results_valid (results, error)) { + g_prefix_error (error, "failed to get updates for urgency: "); + return FALSE; + } + + /* set the update severity for the app */ + sack = pk_results_get_package_sack (results); + for (i = 0; i < gs_app_list_length (list); i++) { + g_autoptr (PkPackage) pkg = NULL; + app = gs_app_list_index (list, i); + if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD)) + continue; + package_id = gs_app_get_source_id_default (app); + if (package_id == NULL) + continue; + pkg = pk_package_sack_find_by_id (sack, package_id); + if (pkg == NULL) + continue; + switch (pk_package_get_info (pkg)) { + case PK_INFO_ENUM_AVAILABLE: + case PK_INFO_ENUM_NORMAL: + case PK_INFO_ENUM_LOW: + case PK_INFO_ENUM_ENHANCEMENT: + gs_app_set_update_urgency (app, AS_URGENCY_KIND_LOW); + break; + case PK_INFO_ENUM_BUGFIX: + gs_app_set_update_urgency (app, AS_URGENCY_KIND_MEDIUM); + break; + case PK_INFO_ENUM_SECURITY: + gs_app_set_update_urgency (app, AS_URGENCY_KIND_CRITICAL); + break; + case PK_INFO_ENUM_IMPORTANT: + gs_app_set_update_urgency (app, AS_URGENCY_KIND_HIGH); + break; + default: + gs_app_set_update_urgency (app, AS_URGENCY_KIND_UNKNOWN); + g_warning ("unhandled info state %s", + pk_info_enum_to_string (pk_package_get_info (pkg))); + break; + } + } + return TRUE; +} + +static gboolean +gs_plugin_refine_app_needs_details (GsPlugin *plugin, GsPluginRefineFlags flags, GsApp *app) +{ + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE) > 0 && + gs_app_get_license (app) == NULL) + return TRUE; + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL) > 0 && + gs_app_get_url (app, AS_URL_KIND_HOMEPAGE) == NULL) + return TRUE; + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE) > 0 && + gs_app_get_size_installed (app) == 0) + return TRUE; + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE) > 0 && + gs_app_get_size_download (app) == 0) + return TRUE; + return FALSE; +} + +static gboolean +gs_plugin_packagekit_refine_details (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + gboolean ret = TRUE; + g_autoptr(GsAppList) list_tmp = NULL; + + list_tmp = gs_app_list_new (); + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD)) + continue; + if (g_strcmp0 (gs_app_get_management_plugin (app), "packagekit") != 0) + continue; + if (gs_app_get_source_id_default (app) == NULL) + continue; + if (!gs_plugin_refine_app_needs_details (plugin, flags, app)) + continue; + gs_app_list_add (list_tmp, app); + } + if (gs_app_list_length (list_tmp) == 0) + return TRUE; + ret = gs_plugin_packagekit_refine_details2 (plugin, + list_tmp, + cancellable, + error); + if (!ret) + return FALSE; + return TRUE; +} + +static gboolean +gs_plugin_refine_requires_version (GsApp *app, GsPluginRefineFlags flags) +{ + const gchar *tmp; + tmp = gs_app_get_version (app); + if (tmp != NULL) + return FALSE; + return (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION) > 0; +} + +static gboolean +gs_plugin_refine_requires_update_details (GsApp *app, GsPluginRefineFlags flags) +{ + const gchar *tmp; + tmp = gs_app_get_update_details (app); + if (tmp != NULL) + return FALSE; + return (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS) > 0; +} + +static gboolean +gs_plugin_refine_requires_origin (GsApp *app, GsPluginRefineFlags flags) +{ + const gchar *tmp; + tmp = gs_app_get_origin (app); + if (tmp != NULL) + return FALSE; + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN) > 0) + return TRUE; + return FALSE; +} + +static gboolean +gs_plugin_refine_requires_package_id (GsApp *app, GsPluginRefineFlags flags) +{ + const gchar *tmp; + tmp = gs_app_get_source_id_default (app); + if (tmp != NULL) + return FALSE; + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION) > 0) + return TRUE; + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE) > 0) + return TRUE; + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL) > 0) + return TRUE; + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE) > 0) + return TRUE; + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION) > 0) + return TRUE; + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION) > 0) + return TRUE; + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS) > 0) + return TRUE; + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE) > 0) + return TRUE; + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION) > 0) + return TRUE; + return FALSE; +} + +static gboolean +gs_plugin_packagekit_refine_distro_upgrade (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + guint i; + GsApp *app2; + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + g_autoptr(PkResults) results = NULL; + g_autoptr(GsAppList) list = NULL; + guint cache_age_save; + + gs_packagekit_helper_add_app (helper, app); + + /* ask PK to simulate upgrading the system */ + g_mutex_lock (&priv->client_mutex); + cache_age_save = pk_client_get_cache_age (priv->client); + pk_client_set_cache_age (priv->client, 60 * 60 * 24 * 7); /* once per week */ + results = pk_client_upgrade_system (priv->client, + pk_bitfield_from_enums (PK_TRANSACTION_FLAG_ENUM_SIMULATE, -1), + gs_app_get_version (app), + PK_UPGRADE_KIND_ENUM_COMPLETE, + cancellable, + gs_packagekit_helper_cb, helper, + error); + pk_client_set_cache_age (priv->client, cache_age_save); + g_mutex_unlock (&priv->client_mutex); + + if (!gs_plugin_packagekit_results_valid (results, error)) { + g_prefix_error (error, "failed to refine distro upgrade: "); + return FALSE; + } + list = gs_app_list_new (); + if (!gs_plugin_packagekit_add_results (plugin, list, results, error)) + return FALSE; + + /* add each of these as related applications */ + for (i = 0; i < gs_app_list_length (list); i++) { + app2 = gs_app_list_index (list, i); + if (gs_app_get_state (app2) != AS_APP_STATE_UNAVAILABLE) + continue; + gs_app_add_related (app, app2); + } + return TRUE; +} + +static gboolean +gs_plugin_packagekit_refine_valid_package_name (const gchar *source) +{ + if (g_strstr_len (source, -1, "/") != NULL) + return FALSE; + return TRUE; +} + +static gboolean +gs_plugin_packagekit_refine_name_to_id (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GsAppList) resolve_all = gs_app_list_new (); + for (guint i = 0; i < gs_app_list_length (list); i++) { + GPtrArray *sources; + GsApp *app = gs_app_list_index (list, i); + const gchar *tmp; + if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD)) + continue; + tmp = gs_app_get_management_plugin (app); + if (tmp != NULL && g_strcmp0 (tmp, "packagekit") != 0) + continue; + sources = gs_app_get_sources (app); + if (sources->len == 0) + continue; + tmp = g_ptr_array_index (sources, 0); + if (!gs_plugin_packagekit_refine_valid_package_name (tmp)) + continue; + if (gs_app_get_state (app) == AS_APP_STATE_UNKNOWN || + gs_plugin_refine_requires_package_id (app, flags) || + gs_plugin_refine_requires_origin (app, flags) || + gs_plugin_refine_requires_version (app, flags)) { + gs_app_list_add (resolve_all, app); + } + } + if (gs_app_list_length (resolve_all) > 0) { + if (!gs_plugin_packagekit_resolve_packages (plugin, + resolve_all, + cancellable, + error)) + return FALSE; + } + return TRUE; +} + +static gboolean +gs_plugin_packagekit_refine_filename_to_id (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + /* not now */ + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION) == 0) + return TRUE; + + for (guint i = 0; i < gs_app_list_length (list); i++) { + g_autofree gchar *fn = NULL; + GsApp *app = gs_app_list_index (list, i); + const gchar *tmp; + if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD)) + continue; + if (gs_app_get_source_id_default (app) != NULL) + continue; + tmp = gs_app_get_management_plugin (app); + if (tmp != NULL && g_strcmp0 (tmp, "packagekit") != 0) + continue; + tmp = gs_app_get_id (app); + if (tmp == NULL) + continue; + switch (gs_app_get_kind (app)) { + case AS_APP_KIND_DESKTOP: + fn = g_strdup_printf ("/usr/share/applications/%s", tmp); + break; + case AS_APP_KIND_ADDON: + fn = g_strdup_printf ("/usr/share/metainfo/%s.metainfo.xml", tmp); + if (!g_file_test (fn, G_FILE_TEST_EXISTS)) { + g_free (fn); + fn = g_strdup_printf ("/usr/share/appdata/%s.metainfo.xml", tmp); + } + break; + default: + break; + } + if (fn == NULL) + continue; + if (!g_file_test (fn, G_FILE_TEST_EXISTS)) { + g_debug ("ignoring %s as does not exist", fn); + continue; + } + if (!gs_plugin_packagekit_refine_from_desktop (plugin, + app, + fn, + cancellable, + error)) + return FALSE; + } + return TRUE; +} + +static gboolean +gs_plugin_packagekit_refine_update_details (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GsAppList) updatedetails_all = gs_app_list_new (); + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + const gchar *tmp; + if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD)) + continue; + if (gs_app_get_state (app) != AS_APP_STATE_UPDATABLE) + continue; + if (gs_app_get_source_id_default (app) == NULL) + continue; + tmp = gs_app_get_management_plugin (app); + if (tmp != NULL && g_strcmp0 (tmp, "packagekit") != 0) + continue; + if (gs_plugin_refine_requires_update_details (app, flags)) + gs_app_list_add (updatedetails_all, app); + } + if (gs_app_list_length (updatedetails_all) > 0) { + if (!gs_plugin_packagekit_refine_updatedetails (plugin, + updatedetails_all, + cancellable, + error)) + return FALSE; + } + return TRUE; +} + +gboolean +gs_plugin_refine (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + /* when we need the cannot-be-upgraded applications, we implement this + * by doing a UpgradeSystem(SIMULATE) which adds the removed packages + * to the related-apps list with a state of %AS_APP_STATE_UNAVAILABLE */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPGRADE_REMOVED) { + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + if (gs_app_get_kind (app) != AS_APP_KIND_OS_UPGRADE) + continue; + if (!gs_plugin_packagekit_refine_distro_upgrade (plugin, + app, + cancellable, + error)) + return FALSE; + } + } + + /* can we resolve in one go? */ + if (!gs_plugin_packagekit_refine_name_to_id (plugin, list, flags, cancellable, error)) + return FALSE; + + /* set the package-id for an installed desktop file */ + if (!gs_plugin_packagekit_refine_filename_to_id (plugin, list, flags, cancellable, error)) + return FALSE; + + /* any update details missing? */ + if (!gs_plugin_packagekit_refine_update_details (plugin, list, flags, cancellable, error)) + return FALSE; + + /* any package details missing? */ + if (!gs_plugin_packagekit_refine_details (plugin, list, flags, cancellable, error)) + return FALSE; + + /* get the update severity */ + if (!gs_plugin_packagekit_refine_update_urgency (plugin, list, flags, cancellable, error)) + return FALSE; + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + + /* only process this app if was created by this plugin */ + if (g_strcmp0 (gs_app_get_management_plugin (app), "packagekit") != 0) + continue; + + /* the scope is always system-wide */ + if (gs_app_get_scope (app) == AS_APP_SCOPE_UNKNOWN) + gs_app_set_scope (app, AS_APP_SCOPE_SYSTEM); + if (gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_UNKNOWN) + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + } + + /* success */ + return TRUE; +} diff --git a/plugins/packagekit/gs-plugin-packagekit-refresh.c b/plugins/packagekit/gs-plugin-packagekit-refresh.c new file mode 100644 index 0000000..2d9a7e4 --- /dev/null +++ b/plugins/packagekit/gs-plugin-packagekit-refresh.c @@ -0,0 +1,182 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2014-2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <packagekit-glib2/packagekit.h> +#include <gnome-software.h> + +#include "gs-metered.h" +#include "gs-packagekit-helper.h" +#include "packagekit-common.h" + +/* + * SECTION: + * Do a PackageKit UpdatePackages(ONLY_DOWNLOAD) method on refresh and + * also convert any package files to applications the best we can. + */ + +struct GsPluginData { + PkTask *task; + GMutex task_mutex; +}; + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData)); + + g_mutex_init (&priv->task_mutex); + priv->task = pk_task_new (); + pk_task_set_only_download (priv->task, TRUE); + pk_client_set_background (PK_CLIENT (priv->task), TRUE); + + /* we can return better results than dpkg directly */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_CONFLICTS, "dpkg"); +} + +void +gs_plugin_destroy (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_mutex_clear (&priv->task_mutex); + g_object_unref (priv->task); +} + +static gboolean +_download_only (GsPlugin *plugin, GsAppList *list, + GCancellable *cancellable, GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_auto(GStrv) package_ids = NULL; + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + g_autoptr(PkPackageSack) sack = NULL; + g_autoptr(PkResults) results2 = NULL; + g_autoptr(PkResults) results = NULL; + + /* get the list of packages to update */ + gs_plugin_status_update (plugin, NULL, GS_PLUGIN_STATUS_WAITING); + + g_mutex_lock (&priv->task_mutex); + /* never refresh the metadata here as this can surprise the frontend if + * we end up downloading a different set of packages than what was + * shown to the user */ + pk_client_set_cache_age (PK_CLIENT (priv->task), G_MAXUINT); + results = pk_client_get_updates (PK_CLIENT (priv->task), + pk_bitfield_value (PK_FILTER_ENUM_NONE), + cancellable, + gs_packagekit_helper_cb, helper, + error); + g_mutex_unlock (&priv->task_mutex); + if (!gs_plugin_packagekit_results_valid (results, error)) { + return FALSE; + } + + /* download all the packages */ + sack = pk_results_get_package_sack (results); + if (pk_package_sack_get_size (sack) == 0) + return TRUE; + package_ids = pk_package_sack_get_ids (sack); + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + gs_packagekit_helper_add_app (helper, app); + } + g_mutex_lock (&priv->task_mutex); + /* never refresh the metadata here as this can surprise the frontend if + * we end up downloading a different set of packages than what was + * shown to the user */ + pk_client_set_cache_age (PK_CLIENT (priv->task), G_MAXUINT); + results2 = pk_task_update_packages_sync (priv->task, + package_ids, + cancellable, + gs_packagekit_helper_cb, helper, + error); + g_mutex_unlock (&priv->task_mutex); + if (results2 == NULL) { + gs_plugin_packagekit_error_convert (error); + return FALSE; + } + return TRUE; +} + +gboolean +gs_plugin_download (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GsAppList) list_tmp = gs_app_list_new (); + + /* add any packages */ + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + GsAppList *related = gs_app_get_related (app); + + /* add this app */ + if (!gs_app_has_quirk (app, GS_APP_QUIRK_IS_PROXY)) + if (g_strcmp0 (gs_app_get_management_plugin (app), "packagekit") == 0) { + gs_app_list_add (list_tmp, app); + continue; + } + + /* add each related app */ + for (guint j = 0; j < gs_app_list_length (related); j++) { + GsApp *app_tmp = gs_app_list_index (related, j); + if (g_strcmp0 (gs_app_get_management_plugin (app_tmp), "packagekit") == 0) + gs_app_list_add (list_tmp, app_tmp); + } + } + + if (gs_app_list_length (list_tmp) == 0) + return TRUE; + + if (!gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)) { + g_autoptr(GError) error_local = NULL; + + if (!gs_metered_block_app_list_on_download_scheduler (list_tmp, cancellable, &error_local)) { + g_warning ("Failed to block on download scheduler: %s", + error_local->message); + g_clear_error (&error_local); + } + } + + return _download_only (plugin, list_tmp, cancellable, error); +} + +gboolean +gs_plugin_refresh (GsPlugin *plugin, + guint cache_age, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + g_autoptr(GsApp) app_dl = gs_app_new (gs_plugin_get_name (plugin)); + g_autoptr(PkResults) results = NULL; + + gs_plugin_status_update (plugin, NULL, GS_PLUGIN_STATUS_WAITING); + gs_packagekit_helper_set_progress_app (helper, app_dl); + + g_mutex_lock (&priv->task_mutex); + /* cache age of 1 is user-initiated */ + pk_client_set_background (PK_CLIENT (priv->task), cache_age > 1); + pk_client_set_cache_age (PK_CLIENT (priv->task), cache_age); + /* refresh the metadata */ + results = pk_client_refresh_cache (PK_CLIENT (priv->task), + FALSE /* force */, + cancellable, + gs_packagekit_helper_cb, helper, + error); + g_mutex_unlock (&priv->task_mutex); + if (!gs_plugin_packagekit_results_valid (results, error)) { + return FALSE; + } + + return TRUE; +} diff --git a/plugins/packagekit/gs-plugin-packagekit-upgrade.c b/plugins/packagekit/gs-plugin-packagekit-upgrade.c new file mode 100644 index 0000000..6442dd7 --- /dev/null +++ b/plugins/packagekit/gs-plugin-packagekit-upgrade.c @@ -0,0 +1,88 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2016 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <packagekit-glib2/packagekit.h> + +#include <gnome-software.h> + +#include "gs-packagekit-helper.h" +#include "packagekit-common.h" + +struct GsPluginData { + PkTask *task; + GMutex task_mutex; +}; + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData)); + + g_mutex_init (&priv->task_mutex); + priv->task = pk_task_new (); + pk_task_set_only_download (priv->task, TRUE); + pk_client_set_background (PK_CLIENT (priv->task), TRUE); + pk_client_set_cache_age (PK_CLIENT (priv->task), 60 * 60 * 24); +} + +void +gs_plugin_destroy (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_mutex_clear (&priv->task_mutex); + g_object_unref (priv->task); +} + +void +gs_plugin_adopt_app (GsPlugin *plugin, GsApp *app) +{ + if (gs_app_get_kind (app) == AS_APP_KIND_OS_UPGRADE) + gs_app_set_management_plugin (app, "packagekit"); +} + +gboolean +gs_plugin_app_upgrade_download (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + g_autoptr(PkResults) results = NULL; + + /* only process this app if was created by this plugin */ + if (g_strcmp0 (gs_app_get_management_plugin (app), "packagekit") != 0) + return TRUE; + + /* check is distro-upgrade */ + if (gs_app_get_kind (app) != AS_APP_KIND_OS_UPGRADE) + return TRUE; + + /* ask PK to download enough packages to upgrade the system */ + gs_app_set_state (app, AS_APP_STATE_INSTALLING); + gs_packagekit_helper_set_progress_app (helper, app); + g_mutex_lock (&priv->task_mutex); + results = pk_task_upgrade_system_sync (priv->task, + gs_app_get_version (app), + PK_UPGRADE_KIND_ENUM_COMPLETE, + cancellable, + gs_packagekit_helper_cb, helper, + error); + g_mutex_unlock (&priv->task_mutex); + if (!gs_plugin_packagekit_results_valid (results, error)) { + gs_app_set_state_recover (app); + return FALSE; + } + + /* state is known */ + gs_app_set_state (app, AS_APP_STATE_UPDATABLE); + return TRUE; +} diff --git a/plugins/packagekit/gs-plugin-packagekit-url-to-app.c b/plugins/packagekit/gs-plugin-packagekit-url-to-app.c new file mode 100644 index 0000000..0418920 --- /dev/null +++ b/plugins/packagekit/gs-plugin-packagekit-url-to-app.c @@ -0,0 +1,125 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2017 Canonical Ltd + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <packagekit-glib2/packagekit.h> +#include <gnome-software.h> + +#include "gs-packagekit-helper.h" +#include "packagekit-common.h" + +struct GsPluginData { + PkClient *client; + GMutex client_mutex; +}; + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData)); + + g_mutex_init (&priv->client_mutex); + priv->client = pk_client_new (); + + pk_client_set_background (priv->client, FALSE); + pk_client_set_cache_age (priv->client, G_MAXUINT); +} + +void +gs_plugin_destroy (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_mutex_clear (&priv->client_mutex); + g_object_unref (priv->client); +} + +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 *scheme = NULL; + g_autofree gchar *path = NULL; + const gchar *id = NULL; + const gchar * const *id_like = NULL; + g_auto(GStrv) package_ids = NULL; + g_autoptr(PkResults) results = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsOsRelease) os_release = NULL; + g_autoptr(GPtrArray) packages = NULL; + g_autoptr(GPtrArray) details = NULL; + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + + path = gs_utils_get_url_path (url); + + /* only do this for apt:// on debian or debian-like distros */ + os_release = gs_os_release_new (error); + if (os_release == NULL) { + g_prefix_error (error, "failed to determine OS information:"); + return FALSE; + } else { + id = gs_os_release_get_id (os_release); + id_like = gs_os_release_get_id_like (os_release); + scheme = gs_utils_get_url_scheme (url); + if (!(g_strcmp0 (scheme, "apt") == 0 && + (g_strcmp0 (id, "debian") == 0 || + g_strv_contains (id_like, "debian")))) { + return TRUE; + } + } + + app = gs_app_new (NULL); + gs_plugin_packagekit_set_packaging_format (plugin, app); + gs_app_add_source (app, path); + gs_app_set_kind (app, AS_APP_KIND_GENERIC); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + + package_ids = g_new0 (gchar *, 2); + package_ids[0] = g_strdup (path); + + g_mutex_lock (&priv->client_mutex); + results = pk_client_resolve (priv->client, + pk_bitfield_from_enums (PK_FILTER_ENUM_NEWEST, PK_FILTER_ENUM_ARCH, -1), + package_ids, + cancellable, + gs_packagekit_helper_cb, helper, + error); + g_mutex_unlock (&priv->client_mutex); + + if (!gs_plugin_packagekit_results_valid (results, error)) { + g_prefix_error (error, "failed to resolve package_ids: "); + return FALSE; + } + + /* get results */ + packages = pk_results_get_package_array (results); + details = pk_results_get_details_array (results); + + if (packages->len >= 1) { + g_autoptr(GHashTable) details_collection = NULL; + + if (gs_app_get_local_file (app) != NULL) + return TRUE; + + details_collection = gs_plugin_packagekit_details_array_to_hash (details); + + gs_plugin_packagekit_resolve_packages_app (plugin, packages, app); + gs_plugin_packagekit_refine_details_app (plugin, details_collection, app); + + gs_app_list_add (list, app); + } else { + g_warning ("no results returned"); + } + + return TRUE; +} diff --git a/plugins/packagekit/gs-plugin-packagekit.c b/plugins/packagekit/gs-plugin-packagekit.c new file mode 100644 index 0000000..c379f94 --- /dev/null +++ b/plugins/packagekit/gs-plugin-packagekit.c @@ -0,0 +1,693 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2014-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <packagekit-glib2/packagekit.h> + +#include <gnome-software.h> + +#include "packagekit-common.h" +#include "gs-packagekit-helper.h" + +/* + * SECTION: + * Uses the system PackageKit instance to return installed packages, + * sources and the ability to add and remove packages. + * + * Requires: | [source-id] + * Refines: | [source-id], [source], [update-details], [management-plugin] + */ + +struct GsPluginData { + PkTask *task; + GMutex task_mutex; +}; + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData)); + + g_mutex_init (&priv->task_mutex); + priv->task = pk_task_new (); + pk_client_set_background (PK_CLIENT (priv->task), FALSE); + pk_client_set_cache_age (PK_CLIENT (priv->task), G_MAXUINT); +} + +void +gs_plugin_destroy (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_mutex_clear (&priv->task_mutex); + g_object_unref (priv->task); +} + +static gboolean +gs_plugin_add_sources_related (GsPlugin *plugin, + GHashTable *hash, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + guint i; + GsApp *app; + GsApp *app_tmp; + PkBitfield filter; + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + const gchar *id; + gboolean ret = TRUE; + g_autoptr(GsAppList) installed = gs_app_list_new (); + g_autoptr(PkResults) results = NULL; + + filter = pk_bitfield_from_enums (PK_FILTER_ENUM_INSTALLED, + PK_FILTER_ENUM_NEWEST, + PK_FILTER_ENUM_ARCH, + PK_FILTER_ENUM_NOT_COLLECTIONS, + -1); + g_mutex_lock (&priv->task_mutex); + results = pk_client_get_packages (PK_CLIENT(priv->task), + filter, + cancellable, + gs_packagekit_helper_cb, helper, + error); + g_mutex_unlock (&priv->task_mutex); + if (!gs_plugin_packagekit_results_valid (results, error)) { + g_prefix_error (error, "failed to get sources related: "); + return FALSE; + } + ret = gs_plugin_packagekit_add_results (plugin, + installed, + results, + error); + if (!ret) + return FALSE; + for (i = 0; i < gs_app_list_length (installed); i++) { + g_auto(GStrv) split = NULL; + app = gs_app_list_index (installed, i); + split = pk_package_id_split (gs_app_get_source_id_default (app)); + if (split == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "invalid package-id: %s", + gs_app_get_source_id_default (app)); + return FALSE; + } + if (g_str_has_prefix (split[PK_PACKAGE_ID_DATA], "installed:")) { + id = split[PK_PACKAGE_ID_DATA] + 10; + app_tmp = g_hash_table_lookup (hash, id); + if (app_tmp != NULL) { + g_debug ("found package %s from %s", + gs_app_get_source_default (app), id); + gs_app_add_related (app_tmp, app); + } + } + } + return TRUE; +} + +gboolean +gs_plugin_add_sources (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + PkBitfield filter; + PkRepoDetail *rd; + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + const gchar *id; + guint i; + g_autoptr(GHashTable) hash = NULL; + g_autoptr(PkResults) results = NULL; + g_autoptr(GPtrArray) array = NULL; + + /* ask PK for the repo details */ + filter = pk_bitfield_from_enums (PK_FILTER_ENUM_NOT_SOURCE, + PK_FILTER_ENUM_NOT_DEVELOPMENT, + PK_FILTER_ENUM_NOT_SUPPORTED, + -1); + g_mutex_lock (&priv->task_mutex); + results = pk_client_get_repo_list (PK_CLIENT(priv->task), + filter, + cancellable, + gs_packagekit_helper_cb, helper, + error); + g_mutex_unlock (&priv->task_mutex); + if (!gs_plugin_packagekit_results_valid (results, error)) + return FALSE; + hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + array = pk_results_get_repo_detail_array (results); + for (i = 0; i < array->len; i++) { + g_autoptr(GsApp) app = NULL; + rd = g_ptr_array_index (array, i); + id = pk_repo_detail_get_id (rd); + app = gs_app_new (id); + gs_app_set_management_plugin (app, gs_plugin_get_name (plugin)); + gs_app_set_kind (app, AS_APP_KIND_SOURCE); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE); + gs_app_set_state (app, pk_repo_detail_get_enabled (rd) ? + AS_APP_STATE_INSTALLED : AS_APP_STATE_AVAILABLE); + gs_app_set_name (app, + GS_APP_QUALITY_LOWEST, + pk_repo_detail_get_description (rd)); + gs_app_set_summary (app, + GS_APP_QUALITY_LOWEST, + pk_repo_detail_get_description (rd)); + gs_app_list_add (list, app); + g_hash_table_insert (hash, + g_strdup (id), + (gpointer) app); + } + + /* get every application on the system and add it as a related package + * if it matches */ + return gs_plugin_add_sources_related (plugin, hash, cancellable, error); +} + +static gboolean +gs_plugin_app_origin_repo_enable (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + g_autoptr(PkResults) results = NULL; + const gchar *repo_id; + + repo_id = gs_app_get_origin (app); + if (repo_id == NULL) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "origin not set"); + return FALSE; + } + + /* do sync call */ + gs_plugin_status_update (plugin, app, GS_PLUGIN_STATUS_WAITING); + g_mutex_lock (&priv->task_mutex); + results = pk_client_repo_enable (PK_CLIENT (priv->task), + repo_id, + TRUE, + cancellable, + gs_packagekit_helper_cb, helper, + error); + g_mutex_unlock (&priv->task_mutex); + if (!gs_plugin_packagekit_results_valid (results, error)) { + gs_utils_error_add_origin_id (error, app); + return FALSE; + } + + /* now that the repo is enabled, the app (not the repo!) moves from + * UNAVAILABLE state to AVAILABLE */ + gs_app_set_state (app, AS_APP_STATE_AVAILABLE); + + return TRUE; +} + +static gboolean +gs_plugin_repo_enable (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + g_autoptr(PkResults) results = NULL; + + /* do sync call */ + gs_plugin_status_update (plugin, app, GS_PLUGIN_STATUS_WAITING); + gs_app_set_state (app, AS_APP_STATE_INSTALLING); + gs_packagekit_helper_add_app (helper, app); + g_mutex_lock (&priv->task_mutex); + results = pk_client_repo_enable (PK_CLIENT (priv->task), + gs_app_get_id (app), + TRUE, + cancellable, + gs_packagekit_helper_cb, helper, + error); + g_mutex_unlock (&priv->task_mutex); + if (!gs_plugin_packagekit_results_valid (results, error)) { + gs_app_set_state_recover (app); + gs_utils_error_add_origin_id (error, app); + return FALSE; + } + + /* state is known */ + gs_app_set_state (app, AS_APP_STATE_INSTALLED); + + return TRUE; +} + +gboolean +gs_plugin_app_install (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + GsAppList *addons; + GPtrArray *source_ids; + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + const gchar *package_id; + guint i, j; + g_autofree gchar *local_filename = NULL; + g_auto(GStrv) package_ids = NULL; + g_autoptr(GPtrArray) array_package_ids = NULL; + g_autoptr(PkResults) results = NULL; + + /* only process this app if was created by this plugin */ + if (g_strcmp0 (gs_app_get_management_plugin (app), + gs_plugin_get_name (plugin)) != 0) + return TRUE; + + /* enable repo */ + if (gs_app_get_kind (app) == AS_APP_KIND_SOURCE) + return gs_plugin_repo_enable (plugin, app, cancellable, error); + + /* queue for install if installation needs the network */ + if (!gs_plugin_get_network_available (plugin)) { + gs_app_set_state (app, AS_APP_STATE_QUEUED_FOR_INSTALL); + return TRUE; + } + + if (gs_app_get_state (app) == AS_APP_STATE_UNAVAILABLE) { + /* get everything up front we need */ + source_ids = gs_app_get_source_ids (app); + if (source_ids->len == 0) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "installing not available"); + return FALSE; + } + package_ids = g_new0 (gchar *, 2); + package_ids[0] = g_strdup (g_ptr_array_index (source_ids, 0)); + + /* enable the repo where the unavailable app is coming from */ + if (!gs_plugin_app_origin_repo_enable (plugin, app, cancellable, error)) + return FALSE; + + gs_app_set_state (app, AS_APP_STATE_INSTALLING); + + /* FIXME: this is a hack, to allow PK time to re-initialize + * everything in order to match an actual result. The root cause + * is probably some kind of hard-to-debug race in the daemon. */ + g_usleep (G_USEC_PER_SEC * 3); + + /* actually install the package */ + gs_packagekit_helper_add_app (helper, app); + g_mutex_lock (&priv->task_mutex); + results = pk_task_install_packages_sync (priv->task, + package_ids, + cancellable, + gs_packagekit_helper_cb, helper, + error); + g_mutex_unlock (&priv->task_mutex); + if (!gs_plugin_packagekit_results_valid (results, error)) { + gs_app_set_state_recover (app); + return FALSE; + } + + /* state is known */ + gs_app_set_state (app, AS_APP_STATE_INSTALLED); + + /* if we remove the app again later, we should be able to + * cancel the installation if we'd never installed it */ + gs_app_set_allow_cancel (app, TRUE); + + /* no longer valid */ + gs_app_clear_source_ids (app); + return TRUE; + } + + /* get the list of available package ids to install */ + switch (gs_app_get_state (app)) { + case AS_APP_STATE_AVAILABLE: + case AS_APP_STATE_UPDATABLE: + source_ids = gs_app_get_source_ids (app); + if (source_ids->len == 0) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "installing not available"); + return FALSE; + } + array_package_ids = g_ptr_array_new_with_free_func (g_free); + for (i = 0; i < source_ids->len; i++) { + package_id = g_ptr_array_index (source_ids, i); + if (g_strstr_len (package_id, -1, ";installed") != NULL) + continue; + g_ptr_array_add (array_package_ids, g_strdup (package_id)); + } + + addons = gs_app_get_addons (app); + for (i = 0; i < gs_app_list_length (addons); i++) { + GsApp *addon = gs_app_list_index (addons, i); + + if (!gs_app_get_to_be_installed (addon)) + continue; + + source_ids = gs_app_get_source_ids (addon); + for (j = 0; j < source_ids->len; j++) { + package_id = g_ptr_array_index (source_ids, j); + if (g_strstr_len (package_id, -1, ";installed") != NULL) + continue; + g_ptr_array_add (array_package_ids, g_strdup (package_id)); + } + } + g_ptr_array_add (array_package_ids, NULL); + + if (array_package_ids->len == 0) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "no packages to install"); + return FALSE; + } + + gs_app_set_state (app, AS_APP_STATE_INSTALLING); + addons = gs_app_get_addons (app); + for (i = 0; i < gs_app_list_length (addons); i++) { + GsApp *addon = gs_app_list_index (addons, i); + if (gs_app_get_to_be_installed (addon)) + gs_app_set_state (addon, AS_APP_STATE_INSTALLING); + } + gs_packagekit_helper_add_app (helper, app); + g_mutex_lock (&priv->task_mutex); + results = pk_task_install_packages_sync (priv->task, + (gchar **) array_package_ids->pdata, + cancellable, + gs_packagekit_helper_cb, helper, + error); + g_mutex_unlock (&priv->task_mutex); + if (!gs_plugin_packagekit_results_valid (results, error)) { + gs_app_set_state_recover (app); + return FALSE; + } + + /* state is known */ + gs_app_set_state (app, AS_APP_STATE_INSTALLED); + + break; + case AS_APP_STATE_AVAILABLE_LOCAL: + if (gs_app_get_local_file (app) == NULL) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "local package, but no filename"); + return FALSE; + } + local_filename = g_file_get_path (gs_app_get_local_file (app)); + package_ids = g_strsplit (local_filename, "\t", -1); + + gs_app_set_state (app, AS_APP_STATE_INSTALLING); + gs_packagekit_helper_add_app (helper, app); + g_mutex_lock (&priv->task_mutex); + results = pk_task_install_files_sync (priv->task, + package_ids, + cancellable, + gs_packagekit_helper_cb, helper, + error); + g_mutex_unlock (&priv->task_mutex); + if (!gs_plugin_packagekit_results_valid (results, error)) { + gs_app_set_state_recover (app); + return FALSE; + } + + /* state is known */ + gs_app_set_state (app, AS_APP_STATE_INSTALLED); + + /* get the new icon from the package */ + gs_app_set_local_file (app, NULL); + gs_app_add_icon (app, NULL); + gs_app_set_pixbuf (app, NULL); + break; + default: + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "do not know how to install app in state %s", + as_app_state_to_string (gs_app_get_state (app))); + return FALSE; + } + + /* no longer valid */ + gs_app_clear_source_ids (app); + + return TRUE; +} + +static gboolean +gs_plugin_repo_disable (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + g_autoptr(PkResults) results = NULL; + + /* do sync call */ + gs_plugin_status_update (plugin, app, GS_PLUGIN_STATUS_WAITING); + gs_app_set_state (app, AS_APP_STATE_REMOVING); + gs_packagekit_helper_add_app (helper, app); + g_mutex_lock (&priv->task_mutex); + results = pk_client_repo_enable (PK_CLIENT (priv->task), + gs_app_get_id (app), + FALSE, + cancellable, + gs_packagekit_helper_cb, helper, + error); + g_mutex_unlock (&priv->task_mutex); + if (!gs_plugin_packagekit_results_valid (results, error)) { + gs_app_set_state_recover (app); + gs_utils_error_add_origin_id (error, app); + return FALSE; + } + + /* state is known */ + gs_app_set_state (app, AS_APP_STATE_AVAILABLE); + + return TRUE; +} + +gboolean +gs_plugin_app_remove (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + const gchar *package_id; + GPtrArray *source_ids; + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + guint i; + guint cnt = 0; + g_autoptr(PkResults) results = NULL; + g_auto(GStrv) package_ids = NULL; + + /* only process this app if was created by this plugin */ + if (g_strcmp0 (gs_app_get_management_plugin (app), + gs_plugin_get_name (plugin)) != 0) + return TRUE; + + /* disable repo */ + if (gs_app_get_kind (app) == AS_APP_KIND_SOURCE) + return gs_plugin_repo_disable (plugin, app, cancellable, error); + + /* get the list of available package ids to install */ + source_ids = gs_app_get_source_ids (app); + if (source_ids->len == 0) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "removing not available"); + return FALSE; + } + package_ids = g_new0 (gchar *, source_ids->len + 1); + for (i = 0; i < source_ids->len; i++) { + package_id = g_ptr_array_index (source_ids, i); + if (g_strstr_len (package_id, -1, ";installed") == NULL) + continue; + package_ids[cnt++] = g_strdup (package_id); + } + if (cnt == 0) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "no packages to remove"); + return FALSE; + } + + /* do the action */ + gs_app_set_state (app, AS_APP_STATE_REMOVING); + gs_packagekit_helper_add_app (helper, app); + g_mutex_lock (&priv->task_mutex); + results = pk_task_remove_packages_sync (priv->task, + package_ids, + TRUE, GS_PACKAGEKIT_AUTOREMOVE, + cancellable, + gs_packagekit_helper_cb, helper, + error); + g_mutex_unlock (&priv->task_mutex); + if (!gs_plugin_packagekit_results_valid (results, error)) { + gs_app_set_state_recover (app); + return FALSE; + } + + /* state is not known: we don't know if we can re-install this app */ + gs_app_set_state (app, AS_APP_STATE_UNKNOWN); + + /* no longer valid */ + gs_app_clear_source_ids (app); + + return TRUE; +} + +static GsApp * +gs_plugin_packagekit_build_update_app (GsPlugin *plugin, PkPackage *package) +{ + GsApp *app = gs_plugin_cache_lookup (plugin, pk_package_get_id (package)); + if (app != NULL) + return app; + app = gs_app_new (NULL); + gs_plugin_packagekit_set_packaging_format (plugin, app); + gs_app_add_source (app, pk_package_get_name (package)); + gs_app_add_source_id (app, pk_package_get_id (package)); + gs_app_set_name (app, GS_APP_QUALITY_LOWEST, + pk_package_get_name (package)); + gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, + pk_package_get_summary (package)); + gs_app_set_metadata (app, "GnomeSoftware::Creator", + gs_plugin_get_name (plugin)); + gs_app_set_management_plugin (app, "packagekit"); + gs_app_set_update_version (app, pk_package_get_version (package)); + gs_app_set_kind (app, AS_APP_KIND_GENERIC); + gs_app_set_scope (app, AS_APP_SCOPE_SYSTEM); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_state (app, AS_APP_STATE_UPDATABLE); + gs_plugin_cache_add (plugin, pk_package_get_id (package), app); + return app; +} + +gboolean +gs_plugin_add_updates (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + g_autoptr(PkResults) results = NULL; + g_autoptr(GPtrArray) array = NULL; + + /* do sync call */ + gs_plugin_status_update (plugin, NULL, GS_PLUGIN_STATUS_WAITING); + g_mutex_lock (&priv->task_mutex); + results = pk_client_get_updates (PK_CLIENT (priv->task), + pk_bitfield_value (PK_FILTER_ENUM_NONE), + cancellable, + gs_packagekit_helper_cb, helper, + error); + g_mutex_unlock (&priv->task_mutex); + if (!gs_plugin_packagekit_results_valid (results, error)) + return FALSE; + + /* add results */ + array = pk_results_get_package_array (results); + for (guint i = 0; i < array->len; i++) { + PkPackage *package = g_ptr_array_index (array, i); + g_autoptr(GsApp) app = NULL; + app = gs_plugin_packagekit_build_update_app (plugin, package); + gs_app_list_add (list, app); + } + return TRUE; +} + +gboolean +gs_plugin_add_search_files (GsPlugin *plugin, + gchar **search, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + PkBitfield filter; + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + g_autoptr(PkResults) results = NULL; + + /* do sync call */ + gs_plugin_status_update (plugin, NULL, GS_PLUGIN_STATUS_WAITING); + filter = pk_bitfield_from_enums (PK_FILTER_ENUM_NEWEST, + PK_FILTER_ENUM_ARCH, + -1); + g_mutex_lock (&priv->task_mutex); + results = pk_client_search_files (PK_CLIENT (priv->task), + filter, + search, + cancellable, + gs_packagekit_helper_cb, helper, + error); + g_mutex_unlock (&priv->task_mutex); + if (!gs_plugin_packagekit_results_valid (results, error)) + return FALSE; + + /* add results */ + return gs_plugin_packagekit_add_results (plugin, list, results, error); +} + +gboolean +gs_plugin_add_search_what_provides (GsPlugin *plugin, + gchar **search, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + PkBitfield filter; + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + g_autoptr(PkResults) results = NULL; + + /* do sync call */ + gs_plugin_status_update (plugin, NULL, GS_PLUGIN_STATUS_WAITING); + filter = pk_bitfield_from_enums (PK_FILTER_ENUM_NEWEST, + PK_FILTER_ENUM_ARCH, + -1); + g_mutex_lock (&priv->task_mutex); + results = pk_client_what_provides (PK_CLIENT (priv->task), + filter, + search, + cancellable, + gs_packagekit_helper_cb, helper, + error); + g_mutex_unlock (&priv->task_mutex); + if (!gs_plugin_packagekit_results_valid (results, error)) + return FALSE; + + /* add results */ + return gs_plugin_packagekit_add_results (plugin, list, results, error); +} + +gboolean +gs_plugin_launch (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + /* only process this app if was created by this plugin */ + if (g_strcmp0 (gs_app_get_management_plugin (app), + gs_plugin_get_name (plugin)) != 0) + return TRUE; + return gs_plugin_app_launch (plugin, app, error); +} diff --git a/plugins/packagekit/gs-plugin-systemd-updates.c b/plugins/packagekit/gs-plugin-systemd-updates.c new file mode 100644 index 0000000..ea8ff1a --- /dev/null +++ b/plugins/packagekit/gs-plugin-systemd-updates.c @@ -0,0 +1,330 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015-2016 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <packagekit-glib2/packagekit.h> + +#include "packagekit-common.h" + +#include <gnome-software.h> + +/* + * Mark previously downloaded packages as zero size, and also allow + * scheduling the offline update. + */ + +struct GsPluginData { + GFileMonitor *monitor; + GFileMonitor *monitor_trigger; + GPermission *permission; + gboolean is_triggered; + GHashTable *hash_prepared; + GMutex hash_prepared_mutex; +}; + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData)); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "packagekit-refresh"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "packagekit-refine"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_BEFORE, "generic-updates"); + g_mutex_init (&priv->hash_prepared_mutex); + priv->hash_prepared = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, NULL); +} + +void +gs_plugin_destroy (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_hash_table_unref (priv->hash_prepared); + g_mutex_clear (&priv->hash_prepared_mutex); + if (priv->monitor != NULL) + g_object_unref (priv->monitor); + if (priv->monitor_trigger != NULL) + g_object_unref (priv->monitor_trigger); +} + +static void +gs_plugin_systemd_updates_permission_cb (GPermission *permission, + GParamSpec *pspec, + gpointer data) +{ + GsPlugin *plugin = GS_PLUGIN (data); + gboolean ret = g_permission_get_allowed (permission) || + g_permission_get_can_acquire (permission); + gs_plugin_set_allow_updates (plugin, ret); +} + +static gboolean +gs_plugin_systemd_update_cache (GsPlugin *plugin, GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GError) error_local = NULL; + g_auto(GStrv) package_ids = NULL; + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->hash_prepared_mutex); + + /* invalidate */ + g_hash_table_remove_all (priv->hash_prepared); + + /* get new list of package-ids */ + package_ids = pk_offline_get_prepared_ids (&error_local); + if (package_ids == NULL) { + if (g_error_matches (error_local, + PK_OFFLINE_ERROR, + PK_OFFLINE_ERROR_NO_DATA)) { + return TRUE; + } + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "Failed to get prepared IDs: %s", + error_local->message); + return FALSE; + } + for (guint i = 0; package_ids[i] != NULL; i++) { + g_hash_table_insert (priv->hash_prepared, + g_strdup (package_ids[i]), + GUINT_TO_POINTER (1)); + } + return TRUE; +} + +static void +gs_plugin_systemd_updates_changed_cb (GFileMonitor *monitor, + GFile *file, GFile *other_file, + GFileMonitorEvent event_type, + gpointer user_data) +{ + GsPlugin *plugin = GS_PLUGIN (user_data); + + /* update UI */ + gs_plugin_systemd_update_cache (plugin, NULL); + gs_plugin_updates_changed (plugin); +} + +static void +gs_plugin_systemd_updates_refresh_is_triggered (GsPlugin *plugin, GCancellable *cancellable) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GFile) file_trigger = NULL; + file_trigger = g_file_new_for_path ("/system-update"); + priv->is_triggered = g_file_query_exists (file_trigger, NULL); + g_debug ("offline trigger is now %s", + priv->is_triggered ? "enabled" : "disabled"); +} + +static void +gs_plugin_systemd_trigger_changed_cb (GFileMonitor *monitor, + GFile *file, GFile *other_file, + GFileMonitorEvent event_type, + gpointer user_data) +{ + GsPlugin *plugin = GS_PLUGIN (user_data); + gs_plugin_systemd_updates_refresh_is_triggered (plugin, NULL); +} + +static void +gs_plugin_systemd_refine_app (GsPlugin *plugin, GsApp *app) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + const gchar *package_id; + g_autoptr(GMutexLocker) locker = NULL; + + /* only process this app if was created by this plugin */ + if (g_strcmp0 (gs_app_get_management_plugin (app), "packagekit") != 0) + return; + + /* the package is already downloaded */ + package_id = gs_app_get_source_id_default (app); + if (package_id == NULL) + return; + locker = g_mutex_locker_new (&priv->hash_prepared_mutex); + if (g_hash_table_lookup (priv->hash_prepared, package_id) != NULL) + gs_app_set_size_download (app, 0); +} + +gboolean +gs_plugin_refine (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + /* not now */ + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE) == 0) + return TRUE; + + /* re-read /var/lib/PackageKit/prepared-update */ + if (!gs_plugin_systemd_update_cache (plugin, error)) + return FALSE; + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + GsAppList *related = gs_app_get_related (app); + /* refine the app itself */ + gs_plugin_systemd_refine_app (plugin, app); + /* and anything related for proxy apps */ + for (guint j = 0; j < gs_app_list_length (related); j++) { + GsApp *app_related = gs_app_list_index (related, j); + gs_plugin_systemd_refine_app (plugin, app_related); + } + } + + return TRUE; +} + +gboolean +gs_plugin_setup (GsPlugin *plugin, GCancellable *cancellable, GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GFile) file_trigger = NULL; + + /* watch the prepared file */ + priv->monitor = pk_offline_get_prepared_monitor (cancellable, error); + if (priv->monitor == NULL) { + gs_utils_error_convert_gio (error); + return FALSE; + } + g_signal_connect (priv->monitor, "changed", + G_CALLBACK (gs_plugin_systemd_updates_changed_cb), + plugin); + + /* watch the trigger file */ + file_trigger = g_file_new_for_path ("/system-update"); + priv->monitor_trigger = g_file_monitor_file (file_trigger, + G_FILE_MONITOR_NONE, + NULL, + error); + if (priv->monitor_trigger == NULL) { + gs_utils_error_convert_gio (error); + return FALSE; + } + g_signal_connect (priv->monitor_trigger, "changed", + G_CALLBACK (gs_plugin_systemd_trigger_changed_cb), + plugin); + + /* check if we have permission to trigger the update */ + priv->permission = gs_utils_get_permission ( + "org.freedesktop.packagekit.trigger-offline-update", + NULL, NULL); + if (priv->permission != NULL) { + g_signal_connect (priv->permission, "notify", + G_CALLBACK (gs_plugin_systemd_updates_permission_cb), + plugin); + } + + /* get the list of currently downloaded packages */ + return gs_plugin_systemd_update_cache (plugin, error); +} + +static gboolean +_systemd_trigger_app (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + + /* if we can process this online do not require a trigger */ + if (gs_app_get_state (app) != AS_APP_STATE_UPDATABLE) + return TRUE; + + /* only process this app if was created by this plugin */ + if (g_strcmp0 (gs_app_get_management_plugin (app), "packagekit") != 0) + return TRUE; + + /* already in correct state */ + if (priv->is_triggered) + return TRUE; + + /* trigger offline update */ + if (!pk_offline_trigger (PK_OFFLINE_ACTION_REBOOT, + cancellable, error)) { + gs_plugin_packagekit_error_convert (error); + return FALSE; + } + + /* don't rely on the file monitor */ + gs_plugin_systemd_updates_refresh_is_triggered (plugin, cancellable); + + /* success */ + return TRUE; +} + +gboolean +gs_plugin_update (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + /* any are us? */ + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + GsAppList *related = gs_app_get_related (app); + + /* try to trigger this app */ + if (!gs_app_has_quirk (app, GS_APP_QUIRK_IS_PROXY)) { + if (!_systemd_trigger_app (plugin, app, cancellable, error)) + return FALSE; + continue; + } + + /* try to trigger each related app */ + for (guint j = 0; j < gs_app_list_length (related); j++) { + GsApp *app_tmp = gs_app_list_index (related, j); + if (!_systemd_trigger_app (plugin, app_tmp, cancellable, error)) + return FALSE; + } + } + + /* success */ + return TRUE; +} + +gboolean +gs_plugin_update_cancel (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + + /* only process this app if was created by this plugin */ + if (g_strcmp0 (gs_app_get_management_plugin (app), "packagekit") != 0) + return TRUE; + + /* already in correct state */ + if (!priv->is_triggered) + return TRUE; + + /* cancel offline update */ + if (!pk_offline_cancel (NULL, error)) + return FALSE; + + /* don't rely on the file monitor */ + gs_plugin_systemd_updates_refresh_is_triggered (plugin, cancellable); + + /* success! */ + return TRUE; +} + +gboolean +gs_plugin_app_upgrade_trigger (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + /* only process this app if was created by this plugin */ + if (g_strcmp0 (gs_app_get_management_plugin (app), "packagekit") != 0) + return TRUE; + return pk_offline_trigger_upgrade (PK_OFFLINE_ACTION_REBOOT, cancellable, error); +} diff --git a/plugins/packagekit/gs-self-test.c b/plugins/packagekit/gs-self-test.c new file mode 100644 index 0000000..5dbaf0a --- /dev/null +++ b/plugins/packagekit/gs-self-test.c @@ -0,0 +1,279 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2017 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include "gnome-software-private.h" + +#include "gs-markdown.h" +#include "gs-test.h" + +static void +gs_markdown_func (void) +{ + gchar *text; + const gchar *markdown; + const gchar *markdown_expected; + g_autoptr(GsMarkdown) md = NULL; + + /* get GsMarkdown object */ + md = gs_markdown_new (GS_MARKDOWN_OUTPUT_PANGO); + g_assert (md); + + markdown = "OEMs\n" + "====\n" + " - Bullett\n"; + markdown_expected = + "<big>OEMs</big>\n" + "• Bullett"; + /* markdown (type2 header) */ + text = gs_markdown_parse (md, markdown); + g_assert_cmpstr (text, ==, markdown_expected); + g_free (text); + + /* markdown (autocode) */ + markdown = "this is http://www.hughsie.com/with_spaces_in_url inline link\n"; + markdown_expected = "this is <tt>http://www.hughsie.com/with_spaces_in_url</tt> inline link"; + gs_markdown_set_autocode (md, TRUE); + text = gs_markdown_parse (md, markdown); + g_assert_cmpstr (text, ==, markdown_expected); + g_free (text); + + /* markdown some invalid header */ + markdown = "*** This software is currently in alpha state ***\n"; + markdown_expected = "<b><i> This software is currently in alpha state </b></i>"; + text = gs_markdown_parse (md, markdown); + g_assert_cmpstr (text, ==, markdown_expected); + g_free (text); + + /* markdown (complex1) */ + markdown = " - This is a *very*\n" + " short paragraph\n" + " that is not usual.\n" + " - Another"; + markdown_expected = + "• This is a <i>very</i> short paragraph that is not usual.\n" + "• Another"; + text = gs_markdown_parse (md, markdown); + g_assert_cmpstr (text, ==, markdown_expected); + g_free (text); + + /* markdown (complex1) */ + markdown = "* This is a *very*\n" + " short paragraph\n" + " that is not usual.\n" + "* This is the second\n" + " bullett point.\n" + "* And the third.\n" + " \n" + "* * *\n" + " \n" + "Paragraph one\n" + "isn't __very__ long at all.\n" + "\n" + "Paragraph two\n" + "isn't much better."; + markdown_expected = + "• This is a <i>very</i> short paragraph that is not usual.\n" + "• This is the second bullett point.\n" + "• And the third.\n" + "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯\n" + "Paragraph one isn't <b>very</b> long at all.\n" + "Paragraph two isn't much better."; + text = gs_markdown_parse (md, markdown); + g_assert_cmpstr (text, ==, markdown_expected); + g_free (text); + + markdown = "This is a spec file description or\n" + "an **update** description in bohdi.\n" + "\n" + "* * *\n" + "# Big title #\n" + "\n" + "The *following* things 'were' fixed:\n" + "- Fix `dave`\n" + "* Fubar update because of \"security\"\n"; + markdown_expected = + "This is a spec file description or an <b>update</b> description in bohdi.\n" + "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯\n" + "<big>Big title</big>\n" + "The <i>following</i> things 'were' fixed:\n" + "• Fix <tt>dave</tt>\n" + "• Fubar update because of \"security\""; + /* markdown (complex2) */ + text = gs_markdown_parse (md, markdown); + if (g_strcmp0 (text, markdown_expected) == 0) + g_assert_cmpstr (text, ==, markdown_expected); + g_free (text); + + /* markdown (list with spaces) */ + markdown = "* list seporated with spaces -\n" + " first item\n" + "\n" + "* second item\n" + "\n" + "* third item\n"; + markdown_expected = + "• list seporated with spaces - first item\n" + "• second item\n" + "• third item"; + text = gs_markdown_parse (md, markdown); + g_assert_cmpstr (text, ==, markdown_expected); + g_free (text); + + gs_markdown_set_max_lines (md, 1); + + /* markdown (one line limit) */ + markdown = "* list seporated with spaces -\n" + " first item\n" + "* second item\n"; + markdown_expected = + "• list seporated with spaces - first item"; + text = gs_markdown_parse (md, markdown); + g_assert_cmpstr (text, ==, markdown_expected); + g_free (text); + + gs_markdown_set_max_lines (md, 1); + + /* markdown (escaping) */ + markdown = "* list & <spaces>"; + markdown_expected = + "• list & <spaces>"; + text = gs_markdown_parse (md, markdown); + g_assert_cmpstr (text, ==, markdown_expected); + g_free (text); + + /* markdown (URLs) */ + markdown = "this is the http://www.hughsie.com/ coolest site"; + markdown_expected = + "this is the " + "<a href=\"http://www.hughsie.com/\">http://www.hughsie.com/</a>" + " coolest site"; + text = gs_markdown_parse (md, markdown); + g_assert_cmpstr (text, ==, markdown_expected); + g_free (text); + + /* markdown (free text) */ + gs_markdown_set_escape (md, FALSE); + text = gs_markdown_parse (md, "This isn't a present"); + g_assert_cmpstr (text, ==, "This isn't a present"); + g_free (text); + + /* markdown (autotext underscore) */ + text = gs_markdown_parse (md, "This isn't CONFIG_UEVENT_HELPER_PATH present"); + g_assert_cmpstr (text, ==, "This isn't <tt>CONFIG_UEVENT_HELPER_PATH</tt> present"); + g_free (text); + + /* markdown (end of bullett) */ + markdown = "*Thu Mar 12 12:00:00 2009* Dan Walsh <dwalsh@redhat.com> - 2.0.79-1\n" + "- Update to upstream \n" + " * Netlink socket handoff patch from Adam Jackson.\n" + " * AVC caching of compute_create results by Eric Paris.\n" + "\n" + "*Tue Mar 10 12:00:00 2009* Dan Walsh <dwalsh@redhat.com> - 2.0.78-5\n" + "- Add patch from ajax to accellerate X SELinux \n" + "- Update eparis patch\n"; + markdown_expected = + "<i>Thu Mar 12 12:00:00 2009</i> Dan Walsh <tt><dwalsh@redhat.com></tt> - 2.0.79-1\n" + "• Update to upstream\n" + "• Netlink socket handoff patch from Adam Jackson.\n" + "• AVC caching of compute_create results by Eric Paris.\n" + "<i>Tue Mar 10 12:00:00 2009</i> Dan Walsh <tt><dwalsh@redhat.com></tt> - 2.0.78-5\n" + "• Add patch from ajax to accellerate X SELinux\n" + "• Update eparis patch"; + gs_markdown_set_escape (md, TRUE); + gs_markdown_set_max_lines (md, 1024); + text = gs_markdown_parse (md, markdown); + g_assert_cmpstr (text, ==, markdown_expected); + g_free (text); +} + +static void +gs_plugins_packagekit_local_func (GsPluginLoader *plugin_loader) +{ + g_autoptr(GsApp) app = NULL; + g_autoptr(GError) error = NULL; + g_autofree gchar *fn = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* no packagekit, abort */ + if (!gs_plugin_loader_get_enabled (plugin_loader, "packagekit-local")) { + g_test_skip ("not enabled"); + return; + } + + /* load local file */ + fn = gs_test_get_filename (TESTDATADIR, "chiron-1.1-1.fc24.x86_64.rpm"); + g_assert (fn != NULL); + file = g_file_new_for_path (fn); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP, + "file", file, + NULL); + app = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NOT_SUPPORTED)) { + g_test_skip ("rpm files not supported"); + return; + } + g_assert_no_error (error); + g_assert (app != NULL); + g_assert_cmpstr (gs_app_get_source_default (app), ==, "chiron"); + g_assert_cmpstr (gs_app_get_url (app, AS_URL_KIND_HOMEPAGE), ==, "http://127.0.0.1/"); + g_assert_cmpstr (gs_app_get_name (app), ==, "chiron"); + g_assert_cmpstr (gs_app_get_version (app), ==, "1.1-1.fc24"); + g_assert_cmpstr (gs_app_get_summary (app), ==, "Single line synopsis"); + g_assert_cmpstr (gs_app_get_description (app), ==, + "This is the first paragraph in the example " + "package spec file.\n\nThis is the second paragraph."); +} + +int +main (int argc, char **argv) +{ + gboolean ret; + g_autoptr(GError) error = NULL; + g_autoptr(GsPluginLoader) plugin_loader = NULL; + const gchar *allowlist[] = { + "packagekit-local", + NULL + }; + + 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); + + /* only critical and error are fatal */ + g_log_set_fatal_mask (NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); + + /* generic tests go here */ + g_test_add_func ("/gnome-software/markdown", gs_markdown_func); + + /* we can only load this once per process */ + plugin_loader = gs_plugin_loader_new (); + 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 */ + if (!g_file_test ("/run/ostree-booted", G_FILE_TEST_EXISTS)) { + g_test_add_data_func ("/gnome-software/plugins/packagekit/local", + plugin_loader, + (GTestDataFunc) gs_plugins_packagekit_local_func); + } + + return g_test_run (); +} diff --git a/plugins/packagekit/meson.build b/plugins/packagekit/meson.build new file mode 100644 index 0000000..05ae4d4 --- /dev/null +++ b/plugins/packagekit/meson.build @@ -0,0 +1,249 @@ +cargs = ['-DG_LOG_DOMAIN="GsPluginPackageKit"'] +cargs += ['-DLOCALPLUGINDIR="' + meson.current_build_dir() + '"'] +deps = [ + plugin_libs, + packagekit, +] + +if get_option('mogwai') + deps += [mogwai_schedule_client] +endif + +shared_module( + 'gs_plugin_systemd-updates', + sources : [ + 'gs-plugin-systemd-updates.c', + 'gs-packagekit-helper.c', + 'packagekit-common.c', + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : deps, + link_with : [ + libgnomesoftware + ] +) + +shared_module( + 'gs_plugin_packagekit', + sources : [ + 'gs-plugin-packagekit.c', + 'gs-packagekit-helper.c', + 'packagekit-common.c', + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : deps, + link_with : [ + libgnomesoftware + ] +) + +shared_module( + 'gs_plugin_packagekit-refine', + sources : [ + 'gs-plugin-packagekit-refine.c', + 'gs-markdown.c', + 'gs-packagekit-helper.c', + 'packagekit-common.c', + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : [ plugin_libs, packagekit ], + link_with : [ + libgnomesoftware + ] +) + +shared_module( + 'gs_plugin_packagekit-refine-repos', + sources : [ + 'gs-plugin-packagekit-refine-repos.c', + 'gs-packagekit-helper.c', + 'packagekit-common.c', + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : [ plugin_libs, packagekit ], + link_with : [ + libgnomesoftware + ] +) + +shared_module( + 'gs_plugin_packagekit-refresh', + sources : [ + 'gs-plugin-packagekit-refresh.c', + 'gs-packagekit-helper.c', + 'packagekit-common.c', + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : [ plugin_libs, packagekit ], + link_with : [ + libgnomesoftware + ] +) + +shared_module( + 'gs_plugin_packagekit-local', + sources : [ + 'gs-plugin-packagekit-local.c', + 'gs-packagekit-helper.c', + 'packagekit-common.c', + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : [ plugin_libs, packagekit ], + link_with : [ + libgnomesoftware + ] +) + +shared_module( + 'gs_plugin_packagekit-history', + sources : [ + 'gs-plugin-packagekit-history.c', + 'packagekit-common.c', + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : [ plugin_libs, packagekit ], + link_with : [ + libgnomesoftware + ] +) + +shared_module( + 'gs_plugin_packagekit-upgrade', + sources : [ + 'gs-plugin-packagekit-upgrade.c', + 'gs-packagekit-helper.c', + 'packagekit-common.c', + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : [ plugin_libs, packagekit ], + link_with : [ + libgnomesoftware + ] +) + +shared_module( + 'gs_plugin_packagekit-offline', +sources : [ + 'gs-plugin-packagekit-offline.c', + 'packagekit-common.c', + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : [ plugin_libs, packagekit ], + link_with : [ + libgnomesoftware + ] +) + +shared_module( + 'gs_plugin_packagekit-proxy', +sources : 'gs-plugin-packagekit-proxy.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : [ plugin_libs, packagekit ], + link_with : [ + libgnomesoftware + ] +) + +shared_module( + 'gs_plugin_packagekit-url-to-app', + sources : [ + 'gs-plugin-packagekit-url-to-app.c', + 'gs-packagekit-helper.c', + 'packagekit-common.c', + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : [ plugin_libs, packagekit ], + link_with : [ + libgnomesoftware + ] +) + +if get_option('tests') + cargs += ['-DTESTDATADIR="' + join_paths(meson.current_source_dir(), 'tests') + '"'] + e = executable( + 'gs-self-test-packagekit', + compiled_schemas, + sources : [ + 'gs-markdown.c', + 'gs-self-test.c' + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + dependencies : [ + plugin_libs, + ], + link_with : [ + libgnomesoftware + ], + c_args : cargs, + ) + test('gs-self-test-packagekit', e, suite: ['plugins', 'packagekit'], env: test_env) +endif diff --git a/plugins/packagekit/packagekit-common.c b/plugins/packagekit/packagekit-common.c new file mode 100644 index 0000000..ed77b34 --- /dev/null +++ b/plugins/packagekit/packagekit-common.c @@ -0,0 +1,543 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2014-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <packagekit-glib2/packagekit.h> + +#include <gnome-software.h> + +#include "packagekit-common.h" + +GsPluginStatus +packagekit_status_enum_to_plugin_status (PkStatusEnum status) +{ + GsPluginStatus plugin_status = GS_PLUGIN_STATUS_UNKNOWN; + + switch (status) { + case PK_STATUS_ENUM_SETUP: + case PK_STATUS_ENUM_CANCEL: + case PK_STATUS_ENUM_FINISHED: + case PK_STATUS_ENUM_UNKNOWN: + break; + case PK_STATUS_ENUM_WAIT: + case PK_STATUS_ENUM_WAITING_FOR_LOCK: + case PK_STATUS_ENUM_WAITING_FOR_AUTH: + plugin_status = GS_PLUGIN_STATUS_WAITING; + break; + case PK_STATUS_ENUM_LOADING_CACHE: + case PK_STATUS_ENUM_TEST_COMMIT: + case PK_STATUS_ENUM_RUNNING: + case PK_STATUS_ENUM_SIG_CHECK: + case PK_STATUS_ENUM_REFRESH_CACHE: + plugin_status = GS_PLUGIN_STATUS_SETUP; + break; + case PK_STATUS_ENUM_DOWNLOAD: + case PK_STATUS_ENUM_DOWNLOAD_REPOSITORY: + case PK_STATUS_ENUM_DOWNLOAD_PACKAGELIST: + case PK_STATUS_ENUM_DOWNLOAD_FILELIST: + case PK_STATUS_ENUM_DOWNLOAD_CHANGELOG: + case PK_STATUS_ENUM_DOWNLOAD_GROUP: + case PK_STATUS_ENUM_DOWNLOAD_UPDATEINFO: + plugin_status = GS_PLUGIN_STATUS_DOWNLOADING; + break; + case PK_STATUS_ENUM_INSTALL: + case PK_STATUS_ENUM_UPDATE: + plugin_status = GS_PLUGIN_STATUS_INSTALLING; + break; + case PK_STATUS_ENUM_CLEANUP: + case PK_STATUS_ENUM_REMOVE: + plugin_status = GS_PLUGIN_STATUS_REMOVING; + break; + case PK_STATUS_ENUM_REQUEST: + case PK_STATUS_ENUM_QUERY: + case PK_STATUS_ENUM_INFO: + case PK_STATUS_ENUM_DEP_RESOLVE: + plugin_status = GS_PLUGIN_STATUS_QUERYING; + break; + default: + g_warning ("no mapping for %s", + pk_status_enum_to_string (status)); + break; + } + return plugin_status; +} + +gboolean +gs_plugin_packagekit_error_convert (GError **error) +{ + GError *error_tmp; + + if (error == NULL) + return FALSE; + + /* this are allowed for low-level errors */ + if (gs_utils_error_convert_gio (error)) + return TRUE; + + /* not set */ + error_tmp = *error; + if (error_tmp == NULL) + return FALSE; + + /* already correct */ + if (error_tmp->domain == GS_PLUGIN_ERROR) + return TRUE; + + /* get a local version */ + if (error_tmp->domain != PK_CLIENT_ERROR) + return FALSE; + + /* daemon errors */ + if (error_tmp->code <= 0xff) { + switch (error_tmp->code) { + case PK_CLIENT_ERROR_CANNOT_START_DAEMON: + case PK_CLIENT_ERROR_INVALID_FILE: + case PK_CLIENT_ERROR_NOT_SUPPORTED: + error_tmp->code = GS_PLUGIN_ERROR_NOT_SUPPORTED; + break; + /* this is working around a bug in libpackagekit-glib */ + case PK_ERROR_ENUM_TRANSACTION_CANCELLED: + error_tmp->code = GS_PLUGIN_ERROR_CANCELLED; + break; + default: + error_tmp->code = GS_PLUGIN_ERROR_FAILED; + break; + } + + /* backend errors */ + } else { + switch (error_tmp->code - 0xff) { + case PK_ERROR_ENUM_INVALID_PACKAGE_FILE: + case PK_ERROR_ENUM_NOT_SUPPORTED: + case PK_ERROR_ENUM_PACKAGE_INSTALL_BLOCKED: + error_tmp->code = GS_PLUGIN_ERROR_NOT_SUPPORTED; + break; + case PK_ERROR_ENUM_NO_CACHE: + case PK_ERROR_ENUM_NO_NETWORK: + error_tmp->code = GS_PLUGIN_ERROR_NO_NETWORK; + break; + case PK_ERROR_ENUM_PACKAGE_DOWNLOAD_FAILED: + case PK_ERROR_ENUM_NO_MORE_MIRRORS_TO_TRY: + case PK_ERROR_ENUM_CANNOT_FETCH_SOURCES: + error_tmp->code = GS_PLUGIN_ERROR_DOWNLOAD_FAILED; + break; + case PK_ERROR_ENUM_BAD_GPG_SIGNATURE: + case PK_ERROR_ENUM_CANNOT_INSTALL_REPO_UNSIGNED: + case PK_ERROR_ENUM_CANNOT_UPDATE_REPO_UNSIGNED: + case PK_ERROR_ENUM_GPG_FAILURE: + case PK_ERROR_ENUM_MISSING_GPG_SIGNATURE: + case PK_ERROR_ENUM_NO_LICENSE_AGREEMENT: + case PK_ERROR_ENUM_NOT_AUTHORIZED: + case PK_ERROR_ENUM_RESTRICTED_DOWNLOAD: + error_tmp->code = GS_PLUGIN_ERROR_NO_SECURITY; + break; + case PK_ERROR_ENUM_NO_SPACE_ON_DEVICE: + error_tmp->code = GS_PLUGIN_ERROR_NO_SPACE; + break; + case PK_ERROR_ENUM_CANCELLED_PRIORITY: + case PK_ERROR_ENUM_TRANSACTION_CANCELLED: + error_tmp->code = GS_PLUGIN_ERROR_CANCELLED; + break; + default: + error_tmp->code = GS_PLUGIN_ERROR_FAILED; + break; + } + } + error_tmp->domain = GS_PLUGIN_ERROR; + return TRUE; +} + +gboolean +gs_plugin_packagekit_results_valid (PkResults *results, GError **error) +{ + g_autoptr(PkError) error_code = NULL; + + /* method failed? */ + if (results == NULL) { + gs_plugin_packagekit_error_convert (error); + return FALSE; + } + + /* check error code */ + error_code = pk_results_get_error_code (results); + if (error_code != NULL) { + g_set_error_literal (error, + PK_CLIENT_ERROR, + pk_error_get_code (error_code), + pk_error_get_details (error_code)); + gs_plugin_packagekit_error_convert (error); + return FALSE; + } + + /* all good */ + return TRUE; +} + +gboolean +gs_plugin_packagekit_add_results (GsPlugin *plugin, + GsAppList *list, + PkResults *results, + GError **error) +{ + const gchar *package_id; + guint i; + PkPackage *package; + g_autoptr(GHashTable) installed = NULL; + g_autoptr(PkError) error_code = NULL; + g_autoptr(GPtrArray) array_filtered = NULL; + g_autoptr(GPtrArray) array = NULL; + + g_return_val_if_fail (GS_IS_PLUGIN (plugin), FALSE); + g_return_val_if_fail (GS_IS_APP_LIST (list), FALSE); + + /* check error code */ + error_code = pk_results_get_error_code (results); + if (error_code != NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "failed to get-packages: %s, %s", + pk_error_enum_to_string (pk_error_get_code (error_code)), + pk_error_get_details (error_code)); + return FALSE; + } + + /* add all installed packages to a hash */ + installed = g_hash_table_new (g_str_hash, g_str_equal); + array = pk_results_get_package_array (results); + for (i = 0; i < array->len; i++) { + package = g_ptr_array_index (array, i); + if (pk_package_get_info (package) != PK_INFO_ENUM_INSTALLED) + continue; + g_hash_table_insert (installed, + (const gpointer) pk_package_get_name (package), + (const gpointer) pk_package_get_id (package)); + } + + /* if the search returns more than one package with the same name, + * ignore everything with that name except the installed package */ + array_filtered = g_ptr_array_new (); + for (i = 0; i < array->len; i++) { + package = g_ptr_array_index (array, i); + package_id = g_hash_table_lookup (installed, pk_package_get_name (package)); + if (pk_package_get_info (package) == PK_INFO_ENUM_INSTALLED || package_id == NULL) { + g_ptr_array_add (array_filtered, package); + } else { + g_debug ("ignoring available %s as installed %s also reported", + pk_package_get_id (package), package_id); + } + } + + /* process packages */ + for (i = 0; i < array_filtered->len; i++) { + g_autoptr(GsApp) app = NULL; + package = g_ptr_array_index (array_filtered, i); + + app = gs_plugin_cache_lookup (plugin, pk_package_get_id (package)); + if (app == NULL) { + app = gs_app_new (NULL); + gs_plugin_packagekit_set_packaging_format (plugin, app); + gs_app_add_source (app, pk_package_get_name (package)); + gs_app_add_source_id (app, pk_package_get_id (package)); + gs_plugin_cache_add (plugin, pk_package_get_id (package), app); + } + gs_app_set_name (app, + GS_APP_QUALITY_LOWEST, + pk_package_get_name (package)); + gs_app_set_summary (app, + GS_APP_QUALITY_LOWEST, + pk_package_get_summary (package)); + gs_app_set_metadata (app, "GnomeSoftware::Creator", + gs_plugin_get_name (plugin)); + gs_app_set_management_plugin (app, "packagekit"); + gs_app_set_version (app, pk_package_get_version (package)); + switch (pk_package_get_info (package)) { + case PK_INFO_ENUM_INSTALLED: + gs_app_set_state (app, AS_APP_STATE_INSTALLED); + break; + case PK_INFO_ENUM_AVAILABLE: + gs_app_set_state (app, AS_APP_STATE_AVAILABLE); + break; + case PK_INFO_ENUM_INSTALLING: + case PK_INFO_ENUM_UPDATING: + case PK_INFO_ENUM_DOWNGRADING: + case PK_INFO_ENUM_OBSOLETING: + case PK_INFO_ENUM_UNTRUSTED: + break; + case PK_INFO_ENUM_UNAVAILABLE: + case PK_INFO_ENUM_REMOVING: + gs_app_set_state (app, AS_APP_STATE_UNAVAILABLE); + break; + default: + gs_app_set_state (app, AS_APP_STATE_UNKNOWN); + g_warning ("unknown info state of %s", + pk_info_enum_to_string (pk_package_get_info (package))); + } + gs_app_set_kind (app, AS_APP_KIND_GENERIC); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_list_add (list, app); + } + return TRUE; +} + +void +gs_plugin_packagekit_resolve_packages_app (GsPlugin *plugin, + GPtrArray *packages, + GsApp *app) +{ + GPtrArray *sources; + PkPackage *package; + const gchar *pkgname; + guint i, j; + guint number_available = 0; + guint number_installed = 0; + + /* find any packages that match the package name */ + number_installed = 0; + number_available = 0; + sources = gs_app_get_sources (app); + for (j = 0; j < sources->len; j++) { + pkgname = g_ptr_array_index (sources, j); + for (i = 0; i < packages->len; i++) { + package = g_ptr_array_index (packages, i); + if (g_strcmp0 (pk_package_get_name (package), pkgname) == 0) { + gs_plugin_packagekit_set_metadata_from_package (plugin, app, package); + switch (pk_package_get_info (package)) { + case PK_INFO_ENUM_INSTALLED: + number_installed++; + break; + case PK_INFO_ENUM_AVAILABLE: + number_available++; + break; + case PK_INFO_ENUM_UNAVAILABLE: + number_available++; + break; + default: + /* should we expect anything else? */ + break; + } + } + } + } + + /* if *all* the source packages for the app are installed then the + * application is considered completely installed */ + if (number_installed == sources->len && number_available == 0) { + if (gs_app_get_state (app) == AS_APP_STATE_UNKNOWN) + gs_app_set_state (app, AS_APP_STATE_INSTALLED); + } else if (number_installed + number_available == sources->len) { + /* if all the source packages are installed and all the rest + * of the packages are available then the app is available */ + if (gs_app_get_state (app) == AS_APP_STATE_UNKNOWN) + gs_app_set_state (app, AS_APP_STATE_AVAILABLE); + } else if (number_installed + number_available > sources->len) { + /* we have more packages returned than source packages */ + gs_app_set_state (app, AS_APP_STATE_UNKNOWN); + gs_app_set_state (app, AS_APP_STATE_UPDATABLE); + } else if (number_installed + number_available < sources->len) { + g_autofree gchar *tmp = NULL; + /* we have less packages returned than source packages */ + tmp = gs_app_to_string (app); + g_debug ("Failed to find all packages for:\n%s", tmp); + gs_app_set_state (app, AS_APP_STATE_UNKNOWN); + } +} + +void +gs_plugin_packagekit_set_metadata_from_package (GsPlugin *plugin, + GsApp *app, + PkPackage *package) +{ + const gchar *data; + + gs_plugin_packagekit_set_packaging_format (plugin, app); + gs_app_set_management_plugin (app, "packagekit"); + gs_app_add_source (app, pk_package_get_name (package)); + gs_app_add_source_id (app, pk_package_get_id (package)); + + /* set origin */ + if (gs_app_get_origin (app) == NULL) { + data = pk_package_get_data (package); + if (g_str_has_prefix (data, "installed:")) + data += 10; + gs_app_set_origin (app, data); + } + + /* set unavailable state */ + if (pk_package_get_info (package) == PK_INFO_ENUM_UNAVAILABLE) { + gs_app_set_state (app, AS_APP_STATE_UNAVAILABLE); + if (gs_app_get_size_installed (app) == 0) + gs_app_set_size_installed (app, GS_APP_SIZE_UNKNOWABLE); + if (gs_app_get_size_download (app) == 0) + gs_app_set_size_download (app, GS_APP_SIZE_UNKNOWABLE); + } + if (gs_app_get_version (app) == NULL) + gs_app_set_version (app, pk_package_get_version (package)); + gs_app_set_name (app, + GS_APP_QUALITY_LOWEST, + pk_package_get_name (package)); + gs_app_set_summary (app, + GS_APP_QUALITY_LOWEST, + pk_package_get_summary (package)); +} + +/* Hash functions which compare PkPackageIds on NAME, VERSION and ARCH, but not DATA. + * This is because some backends do not append the origin. + * + * Borrowing some implementation details from pk-package-id.c, a package + * ID is a semicolon-separated list of NAME;[VERSION];[ARCH];[DATA], + * so a comparison which ignores DATA is just a strncmp() up to and + * including the final semicolon. + * + * Doing it this way means zero allocations, which allows the hash and + * equality functions to be fast. This is important when dealing with + * large refine() package lists. + * + * The hash and equality functions assume that the IDs they are passed are + * valid. */ +static guint +package_id_hash (gconstpointer key) +{ + const gchar *package_id = key; + gchar *no_data; + gsize i, last_semicolon = 0; + + /* find the last semicolon, which starts the DATA section */ + for (i = 0; package_id[i] != '\0'; i++) { + if (package_id[i] == ';') + last_semicolon = i; + } + + /* exit early if the DATA section was empty */ + if (last_semicolon + 1 == i) + return g_str_hash (package_id); + + /* extract up to (and including) the last semicolon into a local string */ + no_data = g_alloca (last_semicolon + 2); + memcpy (no_data, package_id, last_semicolon + 1); + no_data[last_semicolon + 1] = '\0'; + + return g_str_hash (no_data); +} + +static gboolean +package_id_equal (gconstpointer a, + gconstpointer b) +{ + const gchar *package_id_a = a; + const gchar *package_id_b = b; + gsize i, n_semicolons = 0; + + /* compare up to and including the last semicolon */ + for (i = 0; package_id_a[i] != '\0' && package_id_b[i] != '\0'; i++) { + if (package_id_a[i] != package_id_b[i]) + return FALSE; + if (package_id_a[i] == ';') + n_semicolons++; + if (n_semicolons == 4) + return TRUE; + } + + return package_id_a[i] == package_id_b[i]; +} + +GHashTable * +gs_plugin_packagekit_details_array_to_hash (GPtrArray *array) +{ + g_autoptr(GHashTable) details_collection = NULL; + + details_collection = g_hash_table_new_full (package_id_hash, package_id_equal, + NULL, NULL); + + for (gsize i = 0; i < array->len; i++) { + PkDetails *details = g_ptr_array_index (array, i); + g_hash_table_insert (details_collection, + pk_details_get_package_id (details), + details); + } + + return g_steal_pointer (&details_collection); +} + +void +gs_plugin_packagekit_refine_details_app (GsPlugin *plugin, + GHashTable *details_collection, + GsApp *app) +{ + GPtrArray *source_ids; + PkDetails *details; + const gchar *package_id; + guint j; + guint64 size = 0; + + /* @source_ids can have as many as 200 elements (google-noto); typically + * it has 1 or 2 + * + * @details_collection is typically a large list of apps in the + * repository, on the order of 400 or 700 apps */ + source_ids = gs_app_get_source_ids (app); + for (j = 0; j < source_ids->len; j++) { + package_id = g_ptr_array_index (source_ids, j); + details = g_hash_table_lookup (details_collection, package_id); + if (details == NULL) + continue; + + if (gs_app_get_license (app) == NULL) { + g_autofree gchar *license_spdx = NULL; + license_spdx = as_utils_license_to_spdx (pk_details_get_license (details)); + if (license_spdx != NULL) { + gs_app_set_license (app, + GS_APP_QUALITY_LOWEST, + license_spdx); + } + } + if (gs_app_get_url (app, AS_URL_KIND_HOMEPAGE) == NULL) { + gs_app_set_url (app, + AS_URL_KIND_HOMEPAGE, + pk_details_get_url (details)); + } + if (gs_app_get_description (app) == NULL) { + gs_app_set_description (app, + GS_APP_QUALITY_LOWEST, + pk_details_get_description (details)); + } + size += pk_details_get_size (details); + } + + /* the size is the size of all sources */ + if (gs_app_get_state (app) == AS_APP_STATE_UPDATABLE) { + if (size > 0 && gs_app_get_size_installed (app) == 0) + gs_app_set_size_installed (app, size); + if (size > 0 && gs_app_get_size_download (app) == 0) + gs_app_set_size_download (app, size); + } else if (gs_app_is_installed (app)) { + if (gs_app_get_size_download (app) == 0) + gs_app_set_size_download (app, GS_APP_SIZE_UNKNOWABLE); + if (size > 0 && gs_app_get_size_installed (app) == 0) + gs_app_set_size_installed (app, size); + } else { + if (gs_app_get_size_installed (app) == 0) + gs_app_set_size_installed (app, GS_APP_SIZE_UNKNOWABLE); + if (size > 0 && gs_app_get_size_download (app) == 0) + gs_app_set_size_download (app, size); + } +} + +void +gs_plugin_packagekit_set_packaging_format (GsPlugin *plugin, GsApp *app) +{ + if (gs_plugin_check_distro_id (plugin, "fedora") || + gs_plugin_check_distro_id (plugin, "rhel")) { + gs_app_set_metadata (app, "GnomeSoftware::PackagingFormat", "RPM"); + } else if (gs_plugin_check_distro_id (plugin, "debian") || + gs_plugin_check_distro_id (plugin, "ubuntu")) { + gs_app_set_metadata (app, "GnomeSoftware::PackagingFormat", "deb"); + } +} diff --git a/plugins/packagekit/packagekit-common.h b/plugins/packagekit/packagekit-common.h new file mode 100644 index 0000000..9f52368 --- /dev/null +++ b/plugins/packagekit/packagekit-common.h @@ -0,0 +1,40 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib.h> +#include <gnome-software.h> + +#include <packagekit-glib2/packagekit.h> + +G_BEGIN_DECLS + +GsPluginStatus packagekit_status_enum_to_plugin_status (PkStatusEnum status); + +gboolean gs_plugin_packagekit_add_results (GsPlugin *plugin, + GsAppList *list, + PkResults *results, + GError **error); +gboolean gs_plugin_packagekit_error_convert (GError **error); +gboolean gs_plugin_packagekit_results_valid (PkResults *results, + GError **error); +void gs_plugin_packagekit_resolve_packages_app (GsPlugin *plugin, + GPtrArray *packages, + GsApp *app); +void gs_plugin_packagekit_set_metadata_from_package (GsPlugin *plugin, + GsApp *app, + PkPackage *package); +GHashTable * gs_plugin_packagekit_details_array_to_hash (GPtrArray *array); +void gs_plugin_packagekit_refine_details_app (GsPlugin *plugin, + GHashTable *details_collection, + GsApp *app); +void gs_plugin_packagekit_set_packaging_format (GsPlugin *plugin, + GsApp *app); + +G_END_DECLS diff --git a/plugins/packagekit/tests/build-rpm.sh b/plugins/packagekit/tests/build-rpm.sh new file mode 100755 index 0000000..90a4163 --- /dev/null +++ b/plugins/packagekit/tests/build-rpm.sh @@ -0,0 +1,2 @@ +rpmbuild -ba chiron.spec +cp ~/rpmbuild/RPMS/*/chiron*.rpm . diff --git a/plugins/packagekit/tests/chiron-1.1-1.fc24.x86_64.rpm b/plugins/packagekit/tests/chiron-1.1-1.fc24.x86_64.rpm Binary files differnew file mode 100644 index 0000000..1453f48 --- /dev/null +++ b/plugins/packagekit/tests/chiron-1.1-1.fc24.x86_64.rpm diff --git a/plugins/packagekit/tests/chiron.spec b/plugins/packagekit/tests/chiron.spec new file mode 100644 index 0000000..6cbba7e --- /dev/null +++ b/plugins/packagekit/tests/chiron.spec @@ -0,0 +1,22 @@ +Summary: Single line synopsis +Name: chiron +Version: 1.1 +Release: 1%{?dist} +URL: http://127.0.0.1/ +License: GPLv2+ + +%description +This is the first paragraph in the example package spec file. + +This is the second paragraph. + +%install +mkdir -p $RPM_BUILD_ROOT/%{_bindir} +touch $RPM_BUILD_ROOT/%{_bindir}/chiron + +%files +%{_bindir}/chiron + +%changelog +* Tue Apr 26 2016 Richard Hughes <richard@hughsie.com> - 1.1-1 +- Initial version diff --git a/plugins/repos/gs-plugin-repos.c b/plugins/repos/gs-plugin-repos.c new file mode 100644 index 0000000..a35fc80 --- /dev/null +++ b/plugins/repos/gs-plugin-repos.c @@ -0,0 +1,247 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2017-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <gnome-software.h> + +struct GsPluginData { + GHashTable *fns; /* origin : filename */ + GHashTable *urls; /* origin : url */ + GFileMonitor *monitor; + GMutex mutex; + gchar *reposdir; + gboolean valid; +}; + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData)); + + g_mutex_init (&priv->mutex); + + /* for debugging and the self tests */ + priv->reposdir = g_strdup (g_getenv ("GS_SELF_TEST_REPOS_DIR")); + if (priv->reposdir == NULL) + priv->reposdir = g_strdup ("/etc/yum.repos.d"); + + /* plugin only makes sense if this exists at startup */ + if (!g_file_test (priv->reposdir, G_FILE_TEST_EXISTS)) { + gs_plugin_set_enabled (plugin, FALSE); + return; + } + + /* we also watch this for changes */ + priv->fns = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + priv->urls = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + + /* need application IDs */ + 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_free (priv->reposdir); + if (priv->fns != NULL) + g_hash_table_unref (priv->fns); + if (priv->urls != NULL) + g_hash_table_unref (priv->urls); + if (priv->monitor != NULL) + g_object_unref (priv->monitor); + g_mutex_clear (&priv->mutex); +} + +/* mutex must be held */ +static gboolean +gs_plugin_repos_setup (GsPlugin *plugin, GCancellable *cancellable, GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GDir) dir = NULL; + const gchar *fn; + + /* already valid */ + if (priv->valid) + return TRUE; + + /* clear existing */ + g_hash_table_remove_all (priv->fns); + g_hash_table_remove_all (priv->urls); + + /* search all files */ + dir = g_dir_open (priv->reposdir, 0, error); + if (dir == NULL) { + gs_utils_error_convert_gio (error); + return FALSE; + } + while ((fn = g_dir_read_name (dir)) != NULL) { + g_autofree gchar *filename = NULL; + g_auto(GStrv) groups = NULL; + g_autoptr(GKeyFile) kf = g_key_file_new (); + guint i; + + /* not a repo */ + if (!g_str_has_suffix (fn, ".repo")) + continue; + + /* load file */ + filename = g_build_filename (priv->reposdir, fn, NULL); + if (!g_key_file_load_from_file (kf, filename, + G_KEY_FILE_NONE, + error)) { + gs_utils_error_convert_gio (error); + return FALSE; + } + + /* we can have multiple repos in one file */ + groups = g_key_file_get_groups (kf, NULL); + for (i = 0; groups[i] != NULL; i++) { + g_autofree gchar *tmp = NULL; + + g_hash_table_insert (priv->fns, + g_strdup (groups[i]), + g_strdup (filename)); + + tmp = g_key_file_get_string (kf, groups[i], "baseurl", NULL); + if (tmp != NULL) { + g_hash_table_insert (priv->urls, + g_strdup (groups[i]), + g_strdup (tmp)); + continue; + } + + tmp = g_key_file_get_string (kf, groups[i], "metalink", NULL); + if (tmp != NULL) { + g_hash_table_insert (priv->urls, + g_strdup (groups[i]), + g_strdup (tmp)); + continue; + } + } + } + + /* success */ + priv->valid = TRUE; + return TRUE; +} + +static void +gs_plugin_repos_changed_cb (GFileMonitor *monitor, + GFile *file, + GFile *other_file, + GFileMonitorEvent event_type, + GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + priv->valid = FALSE; +} + +gboolean +gs_plugin_setup (GsPlugin *plugin, GCancellable *cancellable, GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GFile) file = g_file_new_for_path (priv->reposdir); + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex); + + /* watch for changes */ + priv->monitor = g_file_monitor_directory (file, G_FILE_MONITOR_NONE, cancellable, error); + if (priv->monitor == NULL) { + gs_utils_error_convert_gio (error); + return FALSE; + } + g_signal_connect (priv->monitor, "changed", + G_CALLBACK (gs_plugin_repos_changed_cb), plugin); + + /* unconditionally at startup */ + return gs_plugin_repos_setup (plugin, cancellable, error); +} + +static gboolean +refine_app_locked (GsPlugin *plugin, + GsApp *app, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + const gchar *tmp; + + /* not required */ + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME) == 0) + return TRUE; + if (gs_app_get_origin_hostname (app) != NULL) + return TRUE; + + /* make sure we don't end up refining flatpak repos */ + if (gs_app_get_bundle_kind (app) != AS_BUNDLE_KIND_PACKAGE) + return TRUE; + + /* ensure valid */ + if (!gs_plugin_repos_setup (plugin, cancellable, error)) + return FALSE; + + /* find hostname */ + switch (gs_app_get_kind (app)) { + case AS_APP_KIND_SOURCE: + if (gs_app_get_id (app) == NULL) + return TRUE; + tmp = g_hash_table_lookup (priv->urls, gs_app_get_id (app)); + if (tmp != NULL) + gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, tmp); + break; + default: + if (gs_app_get_origin (app) == NULL) + return TRUE; + tmp = g_hash_table_lookup (priv->urls, gs_app_get_origin (app)); + if (tmp != NULL) + gs_app_set_origin_hostname (app, tmp); + break; + } + + /* find filename */ + switch (gs_app_get_kind (app)) { + case AS_APP_KIND_SOURCE: + if (gs_app_get_id (app) == NULL) + return TRUE; + tmp = g_hash_table_lookup (priv->fns, gs_app_get_id (app)); + if (tmp != NULL) + gs_app_set_metadata (app, "repos::repo-filename", tmp); + break; + default: + break; + } + + return TRUE; +} + +gboolean +gs_plugin_refine (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex); + + /* nothing to do here */ + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME) == 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_locked (plugin, app, flags, cancellable, error)) + return FALSE; + } + + return TRUE; +} diff --git a/plugins/repos/gs-self-test.c b/plugins/repos/gs-self-test.c new file mode 100644 index 0000000..ff5b3e7 --- /dev/null +++ b/plugins/repos/gs-self-test.c @@ -0,0 +1,82 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2017 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include "gnome-software-private.h" + +#include "gs-test.h" + +static void +gs_plugins_repos_func (GsPluginLoader *plugin_loader) +{ + gboolean ret; + g_autoptr(GsApp) app = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* get the extra bits */ + app = gs_app_new ("testrepos.desktop"); + gs_app_set_origin (app, "utopia"); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFINE, + "app", app, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (ret); + g_assert_cmpstr (gs_app_get_origin_hostname (app), ==, "people.freedesktop.org"); +} + +int +main (int argc, char **argv) +{ + gboolean ret; + g_autofree gchar *reposdir = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsPluginLoader) plugin_loader = NULL; + const gchar *allowlist[] = { + "repos", + NULL + }; + + 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); + + /* dummy data */ + reposdir = gs_test_get_filename (TESTDATADIR, "yum.repos.d"); + g_assert (reposdir != NULL); + g_setenv ("GS_SELF_TEST_REPOS_DIR", reposdir, 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/repos", + plugin_loader, + (GTestDataFunc) gs_plugins_repos_func); + + return g_test_run (); +} diff --git a/plugins/repos/meson.build b/plugins/repos/meson.build new file mode 100644 index 0000000..201b137 --- /dev/null +++ b/plugins/repos/meson.build @@ -0,0 +1,41 @@ +cargs = ['-DG_LOG_DOMAIN="GsPluginRepos"'] +cargs += ['-DLOCALPLUGINDIR="' + meson.current_build_dir() + '"'] + +shared_module( + 'gs_plugin_repos', + sources : 'gs-plugin-repos.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : plugin_libs, + link_with : [ + libgnomesoftware + ] +) + +if get_option('tests') + cargs += ['-DTESTDATADIR="' + join_paths(meson.current_source_dir(), 'tests') + '"'] + e = executable( + 'gs-self-test-repos', + compiled_schemas, + sources : [ + 'gs-self-test.c' + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + dependencies : [ + plugin_libs, + ], + link_with : [ + libgnomesoftware + ], + c_args : cargs, + ) + test('gs-self-test-repos', e, suite: ['plugins', 'repos'], env: test_env) +endif diff --git a/plugins/repos/tests/yum.repos.d/utopia.repo b/plugins/repos/tests/yum.repos.d/utopia.repo new file mode 100644 index 0000000..e912ec4 --- /dev/null +++ b/plugins/repos/tests/yum.repos.d/utopia.repo @@ -0,0 +1,5 @@ +[utopia] +name=utopia for Fedora $releasever +baseurl=http://people.freedesktop.org/~hughsient/fedora/$releasever/x86_64/ +enabled=1 +gpgcheck=0 diff --git a/plugins/rpm-ostree/gs-plugin-rpm-ostree.c b/plugins/rpm-ostree/gs-plugin-rpm-ostree.c new file mode 100644 index 0000000..e4977bb --- /dev/null +++ b/plugins/rpm-ostree/gs-plugin-rpm-ostree.c @@ -0,0 +1,1829 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2017-2020 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <gnome-software.h> + +#include <fcntl.h> +#include <gio/gio.h> +#include <gio/gunixfdlist.h> +#include <glib/gstdio.h> +#include <libdnf/libdnf.h> +#include <ostree.h> +#include <rpm/rpmdb.h> +#include <rpm/rpmlib.h> +#include <rpm/rpmts.h> +#include <rpmostree.h> + +#include "gs-rpmostree-generated.h" + +/* This shows up in the `rpm-ostree status` as the software that + * initiated the update. + */ +#define GS_RPMOSTREE_CLIENT_ID PACKAGE_NAME + +G_DEFINE_AUTO_CLEANUP_FREE_FUNC(Header, headerFree, NULL) +G_DEFINE_AUTO_CLEANUP_FREE_FUNC(rpmts, rpmtsFree, NULL); +G_DEFINE_AUTO_CLEANUP_FREE_FUNC(rpmdbMatchIterator, rpmdbFreeIterator, NULL); + +struct GsPluginData { + GMutex mutex; + GsRPMOSTreeOS *os_proxy; + GsRPMOSTreeSysroot *sysroot_proxy; + OstreeRepo *ot_repo; + OstreeSysroot *ot_sysroot; + DnfContext *dnf_context; + gboolean update_triggered; +}; + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData)); + + /* only works on OSTree */ + if (!g_file_test ("/run/ostree-booted", G_FILE_TEST_EXISTS)) { + gs_plugin_set_enabled (plugin, FALSE); + return; + } + + g_mutex_init (&priv->mutex); + + /* open transaction */ + rpmReadConfigFiles (NULL, NULL); + + /* rpm-ostree is already a daemon with a DBus API; hence it makes + * more sense to use a custom plugin instead of using PackageKit. + */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_CONFLICTS, "packagekit"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_CONFLICTS, "packagekit-history"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_CONFLICTS, "packagekit-local"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_CONFLICTS, "packagekit-offline"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_CONFLICTS, "packagekit-proxy"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_CONFLICTS, "packagekit-refine"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_CONFLICTS, "packagekit-refine-repos"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_CONFLICTS, "packagekit-refresh"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_CONFLICTS, "packagekit-upgrade"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_CONFLICTS, "packagekit-url-to-app"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_CONFLICTS, "systemd-updates"); + + /* need pkgname */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "appstream"); +} + +void +gs_plugin_destroy (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + if (priv->os_proxy != NULL) + g_object_unref (priv->os_proxy); + if (priv->sysroot_proxy != NULL) + g_object_unref (priv->sysroot_proxy); + if (priv->ot_sysroot != NULL) + g_object_unref (priv->ot_sysroot); + if (priv->ot_repo != NULL) + g_object_unref (priv->ot_repo); + if (priv->dnf_context != NULL) + g_object_unref (priv->dnf_context); + g_mutex_clear (&priv->mutex); +} + +static void +gs_rpmostree_error_convert (GError **perror) +{ + GError *error = perror != NULL ? *perror : NULL; + + /* not set */ + if (error == NULL) + return; + + /* parse remote RPM_OSTREED_ERROR */ + if (g_dbus_error_is_remote_error (error)) { + g_autofree gchar *remote_error = g_dbus_error_get_remote_error (error); + + g_dbus_error_strip_remote_error (error); + + if (g_strcmp0 (remote_error, "org.projectatomic.rpmostreed.Error.NotAuthorized") == 0) { + error->code = GS_PLUGIN_ERROR_NO_SECURITY; + } else if (g_str_has_prefix (remote_error, "org.projectatomic.rpmostreed.Error")) { + error->code = GS_PLUGIN_ERROR_FAILED; + } else { + g_warning ("can't reliably fixup remote error %s", remote_error); + error->code = GS_PLUGIN_ERROR_FAILED; + } + error->domain = GS_PLUGIN_ERROR; + return; + } + + /* this are allowed for low-level errors */ + if (gs_utils_error_convert_gio (perror)) + return; + + /* this are allowed for low-level errors */ + if (gs_utils_error_convert_gdbus (perror)) + return; +} + +gboolean +gs_plugin_setup (GsPlugin *plugin, GCancellable *cancellable, GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GVariantBuilder) options_builder = NULL; + + /* Create a proxy for sysroot */ + if (priv->sysroot_proxy == NULL) { + priv->sysroot_proxy = gs_rpmostree_sysroot_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_NONE, + "org.projectatomic.rpmostree1", + "/org/projectatomic/rpmostree1/Sysroot", + cancellable, + error); + if (priv->sysroot_proxy == NULL) { + gs_rpmostree_error_convert (error); + return FALSE; + } + } + + /* Create a proxy for currently booted OS */ + if (priv->os_proxy == NULL) { + g_autofree gchar *os_object_path = NULL; + + os_object_path = gs_rpmostree_sysroot_dup_booted (priv->sysroot_proxy); + if (os_object_path == NULL && + !gs_rpmostree_sysroot_call_get_os_sync (priv->sysroot_proxy, + "", + &os_object_path, + cancellable, + error)) { + gs_rpmostree_error_convert (error); + return FALSE; + } + + priv->os_proxy = gs_rpmostree_os_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_NONE, + "org.projectatomic.rpmostree1", + os_object_path, + cancellable, + error); + if (priv->os_proxy == NULL) { + gs_rpmostree_error_convert (error); + return FALSE; + } + } + + options_builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}")); + g_variant_builder_add (options_builder, "{sv}", "id", + g_variant_new_string (GS_RPMOSTREE_CLIENT_ID)); + /* Register as a client so that the rpm-ostree daemon doesn't exit */ + if (!gs_rpmostree_sysroot_call_register_client_sync (priv->sysroot_proxy, + g_variant_builder_end (options_builder), + cancellable, + error)) { + gs_rpmostree_error_convert (error); + return FALSE; + } + + /* Load ostree sysroot and repo */ + if (priv->ot_sysroot == NULL) { + g_autofree gchar *sysroot_path = NULL; + g_autoptr(GFile) sysroot_file = NULL; + + sysroot_path = gs_rpmostree_sysroot_dup_path (priv->sysroot_proxy); + sysroot_file = g_file_new_for_path (sysroot_path); + + priv->ot_sysroot = ostree_sysroot_new (sysroot_file); + if (!ostree_sysroot_load (priv->ot_sysroot, cancellable, error)) { + gs_rpmostree_error_convert (error); + return FALSE; + } + + if (!ostree_sysroot_get_repo (priv->ot_sysroot, &priv->ot_repo, cancellable, error)) { + gs_rpmostree_error_convert (error); + return FALSE; + } + } + + return TRUE; +} + +static void +app_set_rpm_ostree_packaging_format (GsApp *app) +{ + gs_app_set_metadata (app, "GnomeSoftware::PackagingFormat", "RPM"); +} + +void +gs_plugin_adopt_app (GsPlugin *plugin, GsApp *app) +{ + if (gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_PACKAGE && + gs_app_get_scope (app) == AS_APP_SCOPE_SYSTEM) { + gs_app_set_management_plugin (app, gs_plugin_get_name (plugin)); + gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT); + app_set_rpm_ostree_packaging_format (app); + } + + if (gs_app_get_kind (app) == AS_APP_KIND_OS_UPGRADE) { + gs_app_set_management_plugin (app, gs_plugin_get_name (plugin)); + gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT); + } +} + +typedef struct { + GsPlugin *plugin; + GError *error; + GMainLoop *loop; + GsApp *app; + gboolean complete; +} TransactionProgress; + +static TransactionProgress * +transaction_progress_new (void) +{ + TransactionProgress *self; + + self = g_slice_new0 (TransactionProgress); + self->loop = g_main_loop_new (NULL, FALSE); + + return self; +} + +static void +transaction_progress_free (TransactionProgress *self) +{ + g_clear_object (&self->plugin); + g_clear_error (&self->error); + g_main_loop_unref (self->loop); + g_clear_object (&self->app); + g_slice_free (TransactionProgress, self); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(TransactionProgress, transaction_progress_free); + +static void +transaction_progress_end (TransactionProgress *self) +{ + g_main_loop_quit (self->loop); +} + +static void +on_transaction_progress (GDBusProxy *proxy, + gchar *sender_name, + gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + TransactionProgress *tp = user_data; + + if (g_strcmp0 (signal_name, "PercentProgress") == 0) { + const gchar *message = NULL; + guint32 percentage; + + g_variant_get_child (parameters, 0, "&s", &message); + g_variant_get_child (parameters, 1, "u", &percentage); + g_debug ("PercentProgress: %u, %s\n", percentage, message); + + if (tp->app != NULL) + gs_app_set_progress (tp->app, (guint) percentage); + + if (tp->app != NULL && tp->plugin != NULL) { + GsPluginStatus plugin_status; + + switch (gs_app_get_state (tp->app)) { + case AS_APP_STATE_INSTALLING: + plugin_status = GS_PLUGIN_STATUS_INSTALLING; + break; + case AS_APP_STATE_REMOVING: + plugin_status = GS_PLUGIN_STATUS_REMOVING; + break; + default: + plugin_status = GS_PLUGIN_STATUS_DOWNLOADING; + break; + } + gs_plugin_status_update (tp->plugin, tp->app, plugin_status); + } + } else if (g_strcmp0 (signal_name, "Finished") == 0) { + if (tp->error == NULL) { + g_autofree gchar *error_message = NULL; + gboolean success = FALSE; + + g_variant_get (parameters, "(bs)", &success, &error_message); + + if (!success) { + tp->error = g_dbus_error_new_for_dbus_error ("org.projectatomic.rpmostreed.Error.Failed", + error_message); + } + } + + transaction_progress_end (tp); + } +} + +static void +cancelled_handler (GCancellable *cancellable, + gpointer user_data) +{ + GsRPMOSTreeTransaction *transaction = user_data; + gs_rpmostree_transaction_call_cancel_sync (transaction, NULL, NULL); +} + +static gboolean +gs_rpmostree_transaction_get_response_sync (GsRPMOSTreeSysroot *sysroot_proxy, + const gchar *transaction_address, + TransactionProgress *tp, + GCancellable *cancellable, + GError **error) +{ + GsRPMOSTreeTransaction *transaction = NULL; + g_autoptr(GDBusConnection) peer_connection = NULL; + gint cancel_handler; + gulong signal_handler = 0; + gboolean success = FALSE; + gboolean just_started = FALSE; + + peer_connection = g_dbus_connection_new_for_address_sync (transaction_address, + G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT, + NULL, + cancellable, + error); + + if (peer_connection == NULL) + goto out; + + transaction = gs_rpmostree_transaction_proxy_new_sync (peer_connection, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + "/", + cancellable, + error); + if (transaction == NULL) + goto out; + + /* setup cancel handler */ + cancel_handler = g_cancellable_connect (cancellable, + G_CALLBACK (cancelled_handler), + transaction, NULL); + + signal_handler = g_signal_connect (transaction, "g-signal", + G_CALLBACK (on_transaction_progress), + tp); + + /* Tell the server we're ready to receive signals. */ + if (!gs_rpmostree_transaction_call_start_sync (transaction, + &just_started, + cancellable, + error)) + goto out; + + g_main_loop_run (tp->loop); + + g_cancellable_disconnect (cancellable, cancel_handler); + + if (!g_cancellable_set_error_if_cancelled (cancellable, error)) { + if (tp->error) { + g_propagate_error (error, g_steal_pointer (&tp->error)); + } else { + success = TRUE; + } + } + +out: + if (signal_handler) + g_signal_handler_disconnect (transaction, signal_handler); + if (transaction != NULL) + g_object_unref (transaction); + + return success; +} + +static GsApp * +app_from_modified_pkg_variant (GsPlugin *plugin, GVariant *variant) +{ + g_autoptr(GsApp) app = NULL; + const char *name; + const char *old_evr, *old_arch; + const char *new_evr, *new_arch; + g_autofree char *old_nevra = NULL; + g_autofree char *new_nevra = NULL; + + g_variant_get (variant, "(us(ss)(ss))", NULL /* type*/, &name, &old_evr, &old_arch, &new_evr, &new_arch); + old_nevra = g_strdup_printf ("%s-%s-%s", name, old_evr, old_arch); + new_nevra = g_strdup_printf ("%s-%s-%s", name, new_evr, new_arch); + + app = gs_plugin_cache_lookup (plugin, old_nevra); + if (app != NULL) + return g_steal_pointer (&app); + + /* create new app */ + app = gs_app_new (NULL); + gs_app_set_management_plugin (app, "rpm-ostree"); + gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT); + app_set_rpm_ostree_packaging_format (app); + gs_app_set_size_download (app, 0); + gs_app_set_kind (app, AS_APP_KIND_GENERIC); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_scope (app, AS_APP_SCOPE_SYSTEM); + + /* update or downgrade */ + gs_app_add_source (app, name); + gs_app_set_version (app, old_evr); + gs_app_set_update_version (app, new_evr); + gs_app_set_state (app, AS_APP_STATE_UPDATABLE); + + g_debug ("!%s\n", old_nevra); + g_debug ("=%s\n", new_nevra); + + gs_plugin_cache_add (plugin, old_nevra, app); + return g_steal_pointer (&app); +} + +static GsApp * +app_from_single_pkg_variant (GsPlugin *plugin, GVariant *variant, gboolean addition) +{ + g_autoptr(GsApp) app = NULL; + const char *name; + const char *evr; + const char *arch; + g_autofree char *nevra = NULL; + + g_variant_get (variant, "(usss)", NULL /* type*/, &name, &evr, &arch); + nevra = g_strdup_printf ("%s-%s-%s", name, evr, arch); + + app = gs_plugin_cache_lookup (plugin, nevra); + if (app != NULL) + return g_steal_pointer (&app); + + /* create new app */ + app = gs_app_new (NULL); + gs_app_set_management_plugin (app, "rpm-ostree"); + gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT); + app_set_rpm_ostree_packaging_format (app); + gs_app_set_size_download (app, 0); + gs_app_set_kind (app, AS_APP_KIND_GENERIC); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_scope (app, AS_APP_SCOPE_SYSTEM); + + if (addition) { + /* addition */ + gs_app_add_source (app, name); + gs_app_set_version (app, evr); + gs_app_set_state (app, AS_APP_STATE_AVAILABLE); + + g_debug ("+%s\n", nevra); + } else { + /* removal */ + gs_app_add_source (app, name); + gs_app_set_version (app, evr); + gs_app_set_state (app, AS_APP_STATE_UNAVAILABLE); + + g_debug ("-%s\n", nevra); + } + + gs_plugin_cache_add (plugin, nevra, app); + return g_steal_pointer (&app); +} + +static GVariant * +make_rpmostree_options_variant (gboolean reboot, + gboolean allow_downgrade, + gboolean cache_only, + gboolean download_only, + gboolean skip_purge, + gboolean no_pull_base, + gboolean dry_run, + gboolean no_overrides) +{ + GVariantDict dict; + g_variant_dict_init (&dict, NULL); + g_variant_dict_insert (&dict, "reboot", "b", reboot); + g_variant_dict_insert (&dict, "allow-downgrade", "b", allow_downgrade); + g_variant_dict_insert (&dict, "cache-only", "b", cache_only); + g_variant_dict_insert (&dict, "download-only", "b", download_only); + g_variant_dict_insert (&dict, "skip-purge", "b", skip_purge); + g_variant_dict_insert (&dict, "no-pull-base", "b", no_pull_base); + g_variant_dict_insert (&dict, "dry-run", "b", dry_run); + g_variant_dict_insert (&dict, "no-overrides", "b", no_overrides); + return g_variant_ref_sink (g_variant_dict_end (&dict)); +} + +static GVariant * +make_refresh_md_options_variant (gboolean force) +{ + GVariantDict dict; + g_variant_dict_init (&dict, NULL); + g_variant_dict_insert (&dict, "force", "b", force); + return g_variant_ref_sink (g_variant_dict_end (&dict)); +} + +static gboolean +make_rpmostree_modifiers_variant (const char *install_package, + const char *uninstall_package, + const char *install_local_package, + GVariant **out_modifiers, + GUnixFDList **out_fd_list, + GError **error) +{ + GVariantDict dict; + g_autoptr(GUnixFDList) fd_list = g_unix_fd_list_new (); + + g_variant_dict_init (&dict, NULL); + + if (install_package != NULL) { + g_autoptr(GPtrArray) repo_pkgs = g_ptr_array_new (); + + g_ptr_array_add (repo_pkgs, install_package); + + g_variant_dict_insert_value (&dict, "install-packages", + g_variant_new_strv ((const char *const*)repo_pkgs->pdata, + repo_pkgs->len)); + + } + + if (uninstall_package != NULL) { + g_autoptr(GPtrArray) repo_pkgs = g_ptr_array_new (); + + g_ptr_array_add (repo_pkgs, uninstall_package); + + g_variant_dict_insert_value (&dict, "uninstall-packages", + g_variant_new_strv ((const char *const*)repo_pkgs->pdata, + repo_pkgs->len)); + + } + + if (install_local_package != NULL) { + g_auto(GVariantBuilder) builder; + int fd; + int idx; + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("ah")); + + fd = openat (AT_FDCWD, install_local_package, O_RDONLY | O_CLOEXEC | O_NOCTTY); + if (fd == -1) { + g_set_error (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED, + "Failed to open %s", install_local_package); + return FALSE; + } + + idx = g_unix_fd_list_append (fd_list, fd, error); + if (idx < 0) { + close (fd); + return FALSE; + } + + g_variant_builder_add (&builder, "h", idx); + g_variant_dict_insert_value (&dict, "install-local-packages", + g_variant_new ("ah", &builder)); + close (fd); + } + + *out_fd_list = g_steal_pointer (&fd_list); + *out_modifiers = g_variant_ref_sink (g_variant_dict_end (&dict)); + return TRUE; +} + +static gboolean +rpmostree_update_deployment (GsRPMOSTreeOS *os_proxy, + const char *install_package, + const char *uninstall_package, + const char *install_local_package, + GVariant *options, + char **out_transaction_address, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GUnixFDList) fd_list = NULL; + g_autoptr(GVariant) modifiers = NULL; + + if (!make_rpmostree_modifiers_variant (install_package, + uninstall_package, + install_local_package, + &modifiers, &fd_list, error)) + return FALSE; + + return gs_rpmostree_os_call_update_deployment_sync (os_proxy, + modifiers, + options, + fd_list, + out_transaction_address, + NULL, + cancellable, + error); +} + +#define RPMOSTREE_CORE_CACHEDIR "/var/cache/rpm-ostree/" +#define RPMOSTREE_DIR_CACHE_REPOMD "repomd" +#define RPMOSTREE_DIR_CACHE_SOLV "solv" + +static gboolean +ensure_rpmostree_dnf_context (GsPlugin *plugin, GCancellable *cancellable, GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autofree gchar *transaction_address = NULL; + g_autoptr(GsApp) progress_app = gs_app_new (gs_plugin_get_name (plugin)); + g_autoptr(DnfContext) context = dnf_context_new (); + g_autoptr(DnfState) state = dnf_state_new (); + g_autoptr(GVariant) options = NULL; + g_autoptr(TransactionProgress) tp = transaction_progress_new (); + + if (priv->dnf_context != NULL) + return TRUE; + + tp->app = g_object_ref (progress_app); + tp->plugin = g_object_ref (plugin); + + dnf_context_set_repo_dir (context, "/etc/yum.repos.d"); + dnf_context_set_cache_dir (context, RPMOSTREE_CORE_CACHEDIR RPMOSTREE_DIR_CACHE_REPOMD); + dnf_context_set_solv_dir (context, RPMOSTREE_CORE_CACHEDIR RPMOSTREE_DIR_CACHE_SOLV); + dnf_context_set_cache_age (context, G_MAXUINT); + dnf_context_set_enable_filelists (context, FALSE); + + options = make_refresh_md_options_variant (FALSE /* force */); + if (!gs_rpmostree_os_call_refresh_md_sync (priv->os_proxy, + options, + &transaction_address, + cancellable, + error)) { + gs_rpmostree_error_convert (error); + return FALSE; + } + + if (!gs_rpmostree_transaction_get_response_sync (priv->sysroot_proxy, + transaction_address, + tp, + cancellable, + error)) { + gs_rpmostree_error_convert (error); + return FALSE; + } + + if (!dnf_context_setup (context, cancellable, error)) { + gs_rpmostree_error_convert (error); + return FALSE; + } + + if (!dnf_context_setup_sack_with_flags (context, state, DNF_CONTEXT_SETUP_SACK_FLAG_SKIP_RPMDB, error)) { + gs_rpmostree_error_convert (error); + return FALSE; + } + + g_set_object (&priv->dnf_context, context); + return TRUE; +} + +gboolean +gs_plugin_refresh (GsPlugin *plugin, + guint cache_age, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GMutexLocker) locker = NULL; + + locker = g_mutex_locker_new (&priv->mutex); + + if (!ensure_rpmostree_dnf_context (plugin, cancellable, error)) + return FALSE; + + if (cache_age == G_MAXUINT) + return TRUE; + + { + g_autofree gchar *transaction_address = NULL; + g_autoptr(GsApp) progress_app = gs_app_new (gs_plugin_get_name (plugin)); + g_autoptr(GVariant) options = NULL; + g_autoptr(TransactionProgress) tp = transaction_progress_new (); + + tp->app = g_object_ref (progress_app); + tp->plugin = g_object_ref (plugin); + + options = make_rpmostree_options_variant (FALSE, /* reboot */ + FALSE, /* allow-downgrade */ + FALSE, /* cache-only */ + TRUE, /* download-only */ + FALSE, /* skip-purge */ + FALSE, /* no-pull-base */ + FALSE, /* dry-run */ + FALSE); /* no-overrides */ + if (!gs_rpmostree_os_call_upgrade_sync (priv->os_proxy, + options, + NULL /* fd list */, + &transaction_address, + NULL /* fd list out */, + cancellable, + error)) { + gs_rpmostree_error_convert (error); + return FALSE; + } + + if (!gs_rpmostree_transaction_get_response_sync (priv->sysroot_proxy, + transaction_address, + tp, + cancellable, + error)) { + gs_rpmostree_error_convert (error); + return FALSE; + } + } + + { + g_autofree gchar *transaction_address = NULL; + g_autoptr(GsApp) progress_app = gs_app_new (gs_plugin_get_name (plugin)); + g_autoptr(GVariant) options = NULL; + GVariantDict dict; + g_autoptr(TransactionProgress) tp = transaction_progress_new (); + + tp->app = g_object_ref (progress_app); + tp->plugin = g_object_ref (plugin); + + g_variant_dict_init (&dict, NULL); + g_variant_dict_insert (&dict, "mode", "s", "check"); + options = g_variant_ref_sink (g_variant_dict_end (&dict)); + + if (!gs_rpmostree_os_call_automatic_update_trigger_sync (priv->os_proxy, + options, + NULL, + &transaction_address, + cancellable, + error)) { + gs_rpmostree_error_convert (error); + return FALSE; + } + + if (!gs_rpmostree_transaction_get_response_sync (priv->sysroot_proxy, + transaction_address, + tp, + cancellable, + error)) { + gs_rpmostree_error_convert (error); + return FALSE; + } + } + + /* update UI */ + gs_plugin_updates_changed (plugin); + + return TRUE; +} + +gboolean +gs_plugin_add_updates (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GVariant) cached_update = NULL; + g_autoptr(GVariant) rpm_diff = NULL; + const gchar *checksum = NULL; + const gchar *version = NULL; + g_auto(GVariantDict) cached_update_dict; + + /* ensure D-Bus properties are updated before reading them */ + if (!gs_rpmostree_sysroot_call_reload_sync (priv->sysroot_proxy, cancellable, error)) { + gs_rpmostree_error_convert (error); + return FALSE; + } + + cached_update = gs_rpmostree_os_dup_cached_update (priv->os_proxy); + g_variant_dict_init (&cached_update_dict, cached_update); + + if (!g_variant_dict_lookup (&cached_update_dict, "checksum", "&s", &checksum)) + return TRUE; + if (!g_variant_dict_lookup (&cached_update_dict, "version", "&s", &version)) + return TRUE; + + g_debug ("got CachedUpdate version '%s', checksum '%s'", version, checksum); + + rpm_diff = g_variant_dict_lookup_value (&cached_update_dict, "rpm-diff", G_VARIANT_TYPE ("a{sv}")); + if (rpm_diff != NULL) { + GVariantIter iter; + GVariant *child; + g_autoptr(GVariant) upgraded = NULL; + g_autoptr(GVariant) downgraded = NULL; + g_autoptr(GVariant) removed = NULL; + g_autoptr(GVariant) added = NULL; + g_auto(GVariantDict) rpm_diff_dict; + g_variant_dict_init (&rpm_diff_dict, rpm_diff); + + upgraded = g_variant_dict_lookup_value (&rpm_diff_dict, "upgraded", G_VARIANT_TYPE ("a(us(ss)(ss))")); + if (upgraded == NULL) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "no 'upgraded' in rpm-diff dict"); + return FALSE; + } + downgraded = g_variant_dict_lookup_value (&rpm_diff_dict, "downgraded", G_VARIANT_TYPE ("a(us(ss)(ss))")); + if (downgraded == NULL) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "no 'downgraded' in rpm-diff dict"); + return FALSE; + } + removed = g_variant_dict_lookup_value (&rpm_diff_dict, "removed", G_VARIANT_TYPE ("a(usss)")); + if (removed == NULL) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "no 'removed' in rpm-diff dict"); + return FALSE; + } + added = g_variant_dict_lookup_value (&rpm_diff_dict, "added", G_VARIANT_TYPE ("a(usss)")); + if (added == NULL) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "no 'added' in rpm-diff dict"); + return FALSE; + } + + /* iterate over all upgraded packages and add them */ + g_variant_iter_init (&iter, upgraded); + while ((child = g_variant_iter_next_value (&iter)) != NULL) { + g_autoptr(GsApp) app = app_from_modified_pkg_variant (plugin, child); + if (app != NULL) + gs_app_list_add (list, app); + g_variant_unref (child); + } + + /* iterate over all downgraded packages and add them */ + g_variant_iter_init (&iter, downgraded); + while ((child = g_variant_iter_next_value (&iter)) != NULL) { + g_autoptr(GsApp) app = app_from_modified_pkg_variant (plugin, child); + if (app != NULL) + gs_app_list_add (list, app); + g_variant_unref (child); + } + + /* iterate over all removed packages and add them */ + g_variant_iter_init (&iter, removed); + while ((child = g_variant_iter_next_value (&iter)) != NULL) { + g_autoptr(GsApp) app = app_from_single_pkg_variant (plugin, child, FALSE); + if (app != NULL) + gs_app_list_add (list, app); + g_variant_unref (child); + } + + /* iterate over all added packages and add them */ + g_variant_iter_init (&iter, added); + while ((child = g_variant_iter_next_value (&iter)) != NULL) { + g_autoptr(GsApp) app = app_from_single_pkg_variant (plugin, child, TRUE); + if (app != NULL) + gs_app_list_add (list, app); + g_variant_unref (child); + } + } + + return TRUE; +} + +static gboolean +trigger_rpmostree_update (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autofree gchar *transaction_address = NULL; + g_autoptr(GVariant) options = NULL; + g_autoptr(TransactionProgress) tp = transaction_progress_new (); + + /* if we can process this online do not require a trigger */ + if (gs_app_get_state (app) != AS_APP_STATE_UPDATABLE) + return TRUE; + + /* only process this app if was created by this plugin */ + if (g_strcmp0 (gs_app_get_management_plugin (app), gs_plugin_get_name (plugin)) != 0) + return TRUE; + + /* already in correct state */ + if (priv->update_triggered) + return TRUE; + + /* trigger the update */ + options = make_rpmostree_options_variant (FALSE, /* reboot */ + FALSE, /* allow-downgrade */ + TRUE, /* cache-only */ + FALSE, /* download-only */ + FALSE, /* skip-purge */ + FALSE, /* no-pull-base */ + FALSE, /* dry-run */ + FALSE); /* no-overrides */ + if (!gs_rpmostree_os_call_upgrade_sync (priv->os_proxy, + options, + NULL /* fd list */, + &transaction_address, + NULL /* fd list out */, + cancellable, + error)) { + gs_rpmostree_error_convert (error); + return FALSE; + } + + if (!gs_rpmostree_transaction_get_response_sync (priv->sysroot_proxy, + transaction_address, + tp, + cancellable, + error)) { + gs_rpmostree_error_convert (error); + return FALSE; + } + + priv->update_triggered = TRUE; + + /* success */ + return TRUE; +} + +gboolean +gs_plugin_update_app (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsAppList *related = gs_app_get_related (app); + + /* we don't currently don't put all updates in the OsUpdate proxy app */ + if (!gs_app_has_quirk (app, GS_APP_QUIRK_IS_PROXY)) + return trigger_rpmostree_update (plugin, app, cancellable, error); + + /* try to trigger each related app */ + for (guint i = 0; i < gs_app_list_length (related); i++) { + GsApp *app_tmp = gs_app_list_index (related, i); + if (!trigger_rpmostree_update (plugin, app_tmp, cancellable, error)) + return FALSE; + } + + /* success */ + return TRUE; +} + +gboolean +gs_plugin_app_upgrade_trigger (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + const char *packages[] = { NULL }; + g_autofree gchar *new_refspec = NULL; + g_autofree gchar *transaction_address = NULL; + g_autoptr(GVariant) options = NULL; + g_autoptr(TransactionProgress) tp = transaction_progress_new (); + + /* only process this app if was created by this plugin */ + if (g_strcmp0 (gs_app_get_management_plugin (app), gs_plugin_get_name (plugin)) != 0) + return TRUE; + + /* check is distro-upgrade */ + if (gs_app_get_kind (app) != AS_APP_KIND_OS_UPGRADE) + return TRUE; + + /* construct new refspec based on the distro version we're upgrading to */ + new_refspec = g_strdup_printf ("ostree://fedora/%s/x86_64/silverblue", + gs_app_get_version (app)); + + /* trigger the upgrade */ + options = make_rpmostree_options_variant (FALSE, /* reboot */ + TRUE, /* allow-downgrade */ + TRUE, /* cache-only */ + FALSE, /* download-only */ + FALSE, /* skip-purge */ + FALSE, /* no-pull-base */ + FALSE, /* dry-run */ + FALSE); /* no-overrides */ + + if (!gs_rpmostree_os_call_rebase_sync (priv->os_proxy, + options, + new_refspec, + packages, + NULL /* fd list */, + &transaction_address, + NULL /* fd list out */, + cancellable, + error)) { + gs_rpmostree_error_convert (error); + return FALSE; + } + + if (!gs_rpmostree_transaction_get_response_sync (priv->sysroot_proxy, + transaction_address, + tp, + cancellable, + error)) { + gs_rpmostree_error_convert (error); + + if (g_strrstr ((*error)->message, "Old and new refs are equal")) { + /* don't error out if the correct tree is already deployed */ + g_debug ("ignoring rpm-ostree error: %s", (*error)->message); + g_clear_error (error); + } else { + return FALSE; + } + } + + /* success */ + return TRUE; +} + +static gboolean +gs_plugin_repo_enable (GsPlugin *plugin, + GsApp *app, + gboolean enable, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autofree gchar *transaction_address = NULL; + g_autoptr(GVariantBuilder) options_builder = NULL; + g_autoptr(TransactionProgress) tp = NULL; + + if (enable) + gs_app_set_state (app, AS_APP_STATE_INSTALLING); + else + gs_app_set_state (app, AS_APP_STATE_REMOVING); + + options_builder = g_variant_builder_new (G_VARIANT_TYPE ("a{ss}")); + g_variant_builder_add (options_builder, "{ss}", "enabled", enable ? "1" : "0"); + if (!gs_rpmostree_os_call_modify_yum_repo_sync (priv->os_proxy, + gs_app_get_id (app), + g_variant_builder_end (options_builder), + &transaction_address, + cancellable, + error)) { + gs_rpmostree_error_convert (error); + gs_app_set_state_recover (app); + gs_utils_error_add_origin_id (error, app); + return FALSE; + } + + tp = transaction_progress_new (); + tp->app = g_object_ref (app); + if (!gs_rpmostree_transaction_get_response_sync (priv->sysroot_proxy, + transaction_address, + tp, + cancellable, + error)) { + gs_rpmostree_error_convert (error); + gs_app_set_state_recover (app); + gs_utils_error_add_origin_id (error, app); + return FALSE; + } + + + /* state is known */ + if (enable) + gs_app_set_state (app, AS_APP_STATE_INSTALLED); + else + gs_app_set_state (app, AS_APP_STATE_AVAILABLE); + + return TRUE; +} + +gboolean +gs_plugin_app_install (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + const gchar *install_package = NULL; + g_autofree gchar *local_filename = NULL; + g_autofree gchar *transaction_address = NULL; + g_autoptr(GVariant) options = NULL; + g_autoptr(TransactionProgress) tp = transaction_progress_new (); + + /* only process this app if was created by this plugin */ + if (g_strcmp0 (gs_app_get_management_plugin (app), gs_plugin_get_name (plugin)) != 0) + return TRUE; + + /* enable repo */ + if (gs_app_get_kind (app) == AS_APP_KIND_SOURCE) + return gs_plugin_repo_enable (plugin, app, TRUE, cancellable, error); + + switch (gs_app_get_state (app)) { + case AS_APP_STATE_AVAILABLE: + if (gs_app_get_source_default (app) == NULL) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "no source set"); + return FALSE; + } + + install_package = gs_app_get_source_default (app); + break; + case AS_APP_STATE_AVAILABLE_LOCAL: + if (gs_app_get_local_file (app) == NULL) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "local package, but no filename"); + return FALSE; + } + + local_filename = g_file_get_path (gs_app_get_local_file (app)); + break; + default: + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "do not know how to install app in state %s", + as_app_state_to_string (gs_app_get_state (app))); + return FALSE; + } + + gs_app_set_state (app, AS_APP_STATE_INSTALLING); + tp->app = g_object_ref (app); + + options = make_rpmostree_options_variant (FALSE, /* reboot */ + FALSE, /* allow-downgrade */ + FALSE, /* cache-only */ + FALSE, /* download-only */ + FALSE, /* skip-purge */ + TRUE, /* no-pull-base */ + FALSE, /* dry-run */ + FALSE); /* no-overrides */ + + if (!rpmostree_update_deployment (priv->os_proxy, + install_package, + NULL /* remove package */, + local_filename, + options, + &transaction_address, + cancellable, + error)) { + gs_rpmostree_error_convert (error); + gs_app_set_state_recover (app); + return FALSE; + } + + if (!gs_rpmostree_transaction_get_response_sync (priv->sysroot_proxy, + transaction_address, + tp, + cancellable, + error)) { + gs_rpmostree_error_convert (error); + gs_app_set_state_recover (app); + return FALSE; + } + + /* state is known */ + gs_app_set_state (app, AS_APP_STATE_INSTALLED); + + /* get the new icon from the package */ + gs_app_set_local_file (app, NULL); + gs_app_add_icon (app, NULL); + gs_app_set_pixbuf (app, NULL); + + /* no longer valid */ + gs_app_clear_source_ids (app); + + return TRUE; +} + +gboolean +gs_plugin_app_remove (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autofree gchar *transaction_address = NULL; + g_autoptr(GVariant) options = NULL; + g_autoptr(TransactionProgress) tp = transaction_progress_new (); + + /* only process this app if was created by this plugin */ + if (g_strcmp0 (gs_app_get_management_plugin (app), gs_plugin_get_name (plugin)) != 0) + return TRUE; + + /* disable repo */ + if (gs_app_get_kind (app) == AS_APP_KIND_SOURCE) + return gs_plugin_repo_enable (plugin, app, FALSE, cancellable, error); + + gs_app_set_state (app, AS_APP_STATE_REMOVING); + tp->app = g_object_ref (app); + + options = make_rpmostree_options_variant (FALSE, /* reboot */ + FALSE, /* allow-downgrade */ + TRUE, /* cache-only */ + FALSE, /* download-only */ + FALSE, /* skip-purge */ + TRUE, /* no-pull-base */ + FALSE, /* dry-run */ + FALSE); /* no-overrides */ + + if (!rpmostree_update_deployment (priv->os_proxy, + NULL /* install package */, + gs_app_get_source_default (app), + NULL /* install local package */, + options, + &transaction_address, + cancellable, + error)) { + gs_rpmostree_error_convert (error); + gs_app_set_state_recover (app); + return FALSE; + } + + if (!gs_rpmostree_transaction_get_response_sync (priv->sysroot_proxy, + transaction_address, + tp, + cancellable, + error)) { + gs_rpmostree_error_convert (error); + gs_app_set_state_recover (app); + return FALSE; + } + + /* state is not known: we don't know if we can re-install this app */ + gs_app_set_state (app, AS_APP_STATE_UNKNOWN); + + return TRUE; +} + +static DnfPackage * +find_package_by_name (DnfSack *sack, + const char *pkgname) +{ + g_autoptr(GPtrArray) pkgs = NULL; + hy_autoquery HyQuery query = hy_query_create (sack); + + hy_query_filter (query, HY_PKG_NAME, HY_EQ, pkgname); + hy_query_filter_latest_per_arch (query, TRUE); + + pkgs = hy_query_run (query); + if (pkgs->len == 0) + return NULL; + + return g_object_ref (pkgs->pdata[pkgs->len-1]); +} + +static GPtrArray * +find_packages_by_provides (DnfSack *sack, + gchar **search) +{ + g_autoptr(GPtrArray) pkgs = NULL; + hy_autoquery HyQuery query = hy_query_create (sack); + + hy_query_filter_provides_in (query, search); + hy_query_filter_latest_per_arch (query, TRUE); + + pkgs = hy_query_run (query); + + return g_steal_pointer (&pkgs); +} + +static gboolean +resolve_installed_packages_app (GsPlugin *plugin, + GPtrArray *pkglist, + gchar **layered_packages, + gchar **layered_local_packages, + GsApp *app) +{ + for (guint i = 0; i < pkglist->len; i++) { + RpmOstreePackage *pkg = g_ptr_array_index (pkglist, i); + if (g_strcmp0 (rpm_ostree_package_get_name (pkg), gs_app_get_source_default (app)) == 0) { + gs_app_set_version (app, rpm_ostree_package_get_evr (pkg)); + if (gs_app_get_state (app) == AS_APP_STATE_UNKNOWN) + gs_app_set_state (app, AS_APP_STATE_INSTALLED); + if (g_strv_contains ((const gchar * const *) layered_packages, + rpm_ostree_package_get_name (pkg)) || + g_strv_contains ((const gchar * const *) layered_local_packages, + rpm_ostree_package_get_nevra (pkg))) { + /* layered packages can always be removed */ + gs_app_remove_quirk (app, GS_APP_QUIRK_COMPULSORY); + } else { + /* can't remove packages that are part of the base system */ + gs_app_add_quirk (app, GS_APP_QUIRK_COMPULSORY); + } + if (gs_app_get_origin (app) == NULL) + gs_app_set_origin (app, "rpm-ostree"); + return TRUE /* found */; + } + } + + return FALSE /* not found */; +} + +static gboolean +resolve_available_packages_app (GsPlugin *plugin, + DnfSack *sack, + GsApp *app) +{ + g_autoptr(DnfPackage) pkg = NULL; + + pkg = find_package_by_name (sack, gs_app_get_source_default (app)); + if (pkg != NULL) { + gs_app_set_version (app, dnf_package_get_evr (pkg)); + if (gs_app_get_state (app) == AS_APP_STATE_UNKNOWN) + gs_app_set_state (app, AS_APP_STATE_AVAILABLE); + + /* anything not part of the base system can be removed */ + gs_app_remove_quirk (app, GS_APP_QUIRK_COMPULSORY); + + /* set origin */ + if (gs_app_get_origin (app) == NULL) { + const gchar *reponame = dnf_package_get_reponame (pkg); + gs_app_set_origin (app, reponame); + } + + /* set more metadata for packages that don't have appstream data */ + gs_app_set_name (app, GS_APP_QUALITY_LOWEST, dnf_package_get_name (pkg)); + gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, dnf_package_get_summary (pkg)); + + /* set hide-from-search quirk for available apps we don't want to show */ + if (!gs_app_is_installed (app)) { + switch (gs_app_get_kind (app)) { + case AS_APP_KIND_DESKTOP: + case AS_APP_KIND_WEB_APP: + case AS_APP_KIND_CONSOLE: + gs_app_add_quirk (app, GS_APP_QUIRK_HIDE_FROM_SEARCH); + break; + default: + break; + } + } + + return TRUE /* found */; + } + + return FALSE /* not found */; +} + +static gboolean +resolve_appstream_source_file_to_package_name (GsPlugin *plugin, + GsApp *app, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + Header h; + const gchar *fn; + gint rc; + g_auto(rpmdbMatchIterator) mi = NULL; + g_auto(rpmts) ts = NULL; + + /* open db readonly */ + ts = rpmtsCreate(); + rpmtsSetRootDir (ts, NULL); + rc = rpmtsOpenDB (ts, O_RDONLY); + if (rc != 0) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "Failed to open rpmdb: %i", rc); + return FALSE; + } + + /* look for a specific file */ + fn = gs_app_get_metadata_item (app, "appstream::source-file"); + if (fn == NULL) + return TRUE; + + mi = rpmtsInitIterator (ts, RPMDBI_INSTFILENAMES, fn, 0); + if (mi == NULL) { + g_debug ("rpm: no search results for %s", fn); + return TRUE; + } + + /* process any results */ + g_debug ("rpm: querying for %s with %s", gs_app_get_id (app), fn); + while ((h = rpmdbNextIterator (mi)) != NULL) { + const gchar *name; + + /* add default source */ + name = headerGetString (h, RPMTAG_NAME); + if (gs_app_get_source_default (app) == NULL) { + g_debug ("rpm: setting source to %s", name); + gs_app_add_source (app, name); + gs_app_set_management_plugin (app, gs_plugin_get_name (plugin)); + gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT); + app_set_rpm_ostree_packaging_format (app); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + } + } + + return TRUE; +} + +gboolean +gs_plugin_refine (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GMutexLocker) locker = NULL; + g_autoptr(GPtrArray) pkglist = NULL; + g_autoptr(GVariant) default_deployment = NULL; + g_auto(GStrv) layered_packages = NULL; + g_auto(GStrv) layered_local_packages = NULL; + g_autofree gchar *checksum = NULL; + + locker = g_mutex_locker_new (&priv->mutex); + + /* ensure D-Bus properties are updated before reading them */ + if (!gs_rpmostree_sysroot_call_reload_sync (priv->sysroot_proxy, cancellable, error)) { + gs_rpmostree_error_convert (error); + return FALSE; + } + + default_deployment = gs_rpmostree_os_dup_default_deployment (priv->os_proxy); + g_assert (g_variant_lookup (default_deployment, + "packages", "^as", + &layered_packages)); + g_assert (g_variant_lookup (default_deployment, + "requested-local-packages", "^as", + &layered_local_packages)); + g_assert (g_variant_lookup (default_deployment, + "checksum", "s", + &checksum)); + + pkglist = rpm_ostree_db_query_all (priv->ot_repo, checksum, cancellable, error); + if (pkglist == NULL) { + gs_rpmostree_error_convert (error); + return FALSE; + } + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + gboolean found; + + if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD)) + continue; + /* set management plugin for apps where appstream just added the source package name in refine() */ + if (gs_app_get_management_plugin (app) == NULL && + gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_PACKAGE && + gs_app_get_scope (app) == AS_APP_SCOPE_SYSTEM && + gs_app_get_source_default (app) != NULL) { + gs_app_set_management_plugin (app, gs_plugin_get_name (plugin)); + gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT); + app_set_rpm_ostree_packaging_format (app); + } + /* resolve the source package name based on installed appdata/desktop file name */ + if (gs_app_get_management_plugin (app) == NULL && + gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_UNKNOWN && + gs_app_get_scope (app) == AS_APP_SCOPE_SYSTEM && + gs_app_get_source_default (app) == NULL) { + if (!resolve_appstream_source_file_to_package_name (plugin, app, flags, cancellable, error)) + return FALSE; + } + if (g_strcmp0 (gs_app_get_management_plugin (app), gs_plugin_get_name (plugin)) != 0) + continue; + if (gs_app_get_source_default (app) == NULL) + continue; + + /* first try to resolve from installed packages */ + found = resolve_installed_packages_app (plugin, pkglist, layered_packages, layered_local_packages, app); + + /* if we didn't find anything, try resolving from available packages */ + if (!found && priv->dnf_context != NULL) + found = resolve_available_packages_app (plugin, dnf_context_get_sack (priv->dnf_context), app); + + /* if we still didn't find anything then it's likely a package + * that is still in appstream data, but removed from the repos */ + if (!found) + g_debug ("failed to resolve %s", gs_app_get_unique_id (app)); + } + + return TRUE; +} + +gboolean +gs_plugin_app_upgrade_download (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + const char *packages[] = { NULL }; + g_autofree gchar *new_refspec = NULL; + g_autofree gchar *transaction_address = NULL; + g_autoptr(GVariant) options = NULL; + g_autoptr(TransactionProgress) tp = transaction_progress_new (); + + /* only process this app if was created by this plugin */ + if (g_strcmp0 (gs_app_get_management_plugin (app), gs_plugin_get_name (plugin)) != 0) + return TRUE; + + /* check is distro-upgrade */ + if (gs_app_get_kind (app) != AS_APP_KIND_OS_UPGRADE) + return TRUE; + + /* construct new refspec based on the distro version we're upgrading to */ + new_refspec = g_strdup_printf ("ostree://fedora/%s/x86_64/silverblue", + gs_app_get_version (app)); + + options = make_rpmostree_options_variant (FALSE, /* reboot */ + TRUE, /* allow-downgrade */ + FALSE, /* cache-only */ + TRUE, /* download-only */ + FALSE, /* skip-purge */ + FALSE, /* no-pull-base */ + FALSE, /* dry-run */ + FALSE); /* no-overrides */ + + gs_app_set_state (app, AS_APP_STATE_INSTALLING); + tp->app = g_object_ref (app); + + if (!gs_rpmostree_os_call_rebase_sync (priv->os_proxy, + options, + new_refspec, + packages, + NULL /* fd list */, + &transaction_address, + NULL /* fd list out */, + cancellable, + error)) { + gs_rpmostree_error_convert (error); + gs_app_set_state_recover (app); + return FALSE; + } + + if (!gs_rpmostree_transaction_get_response_sync (priv->sysroot_proxy, + transaction_address, + tp, + cancellable, + error)) { + gs_rpmostree_error_convert (error); + + if (g_strrstr ((*error)->message, "Old and new refs are equal")) { + /* don't error out if the correct tree is already deployed */ + g_debug ("ignoring rpm-ostree error: %s", (*error)->message); + g_clear_error (error); + } else { + gs_app_set_state_recover (app); + return FALSE; + } + } + + /* state is known */ + gs_app_set_state (app, AS_APP_STATE_UPDATABLE); + return TRUE; +} + +gboolean +gs_plugin_launch (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + /* only process this app if was created by this plugin */ + if (g_strcmp0 (gs_app_get_management_plugin (app), + gs_plugin_get_name (plugin)) != 0) + return TRUE; + + /* these are handled by the shell extensions plugin */ + if (gs_app_get_kind (app) == AS_APP_KIND_SHELL_EXTENSION) + return TRUE; + + return gs_plugin_app_launch (plugin, app, error); +} + +static void +add_quirks_from_package_name (GsApp *app, const gchar *package_name) +{ + /* these packages don't have a .repo file in their file lists, but + * instead install one through rpm scripts / cron job */ + const gchar *packages_with_repos[] = { + "google-chrome-stable", + "google-earth-pro-stable", + "google-talkplugin", + NULL }; + + if (g_strv_contains (packages_with_repos, package_name)) + gs_app_add_quirk (app, GS_APP_QUIRK_HAS_SOURCE); +} + +gboolean +gs_plugin_file_to_app (GsPlugin *plugin, + GsAppList *list, + GFile *file, + GCancellable *cancellable, + GError **error) +{ + gboolean ret = FALSE; + FD_t rpmfd = NULL; + int r; + guint64 epoch; + guint64 size; + const gchar *name; + const gchar *version; + const gchar *release; + const gchar *license; + g_auto(Header) h = NULL; + g_auto(rpmts) ts = NULL; + g_autofree gchar *evr = NULL; + g_autofree gchar *filename = NULL; + g_autoptr(GsApp) app = NULL; + + filename = g_file_get_path (file); + if (!g_str_has_suffix (filename, ".rpm")) { + ret = TRUE; + goto out; + } + + ts = rpmtsCreate (); + rpmtsSetVSFlags (ts, _RPMVSF_NOSIGNATURES); + + /* librpm needs Fopenfd */ + rpmfd = Fopen (filename, "r.fdio"); + if (rpmfd == NULL) { + g_set_error (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED, + "Opening %s failed", filename); + goto out; + } + if (Ferror (rpmfd)) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED, + "Opening %s failed: %s", + filename, + Fstrerror (rpmfd)); + goto out; + } + + if ((r = rpmReadPackageFile (ts, rpmfd, filename, &h)) != RPMRC_OK) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED, + "Verification of %s failed", + filename); + goto out; + } + + app = gs_app_new (NULL); + gs_app_set_metadata (app, "GnomeSoftware::Creator", gs_plugin_get_name (plugin)); + gs_app_set_management_plugin (app, gs_plugin_get_name (plugin)); + gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT); + app_set_rpm_ostree_packaging_format (app); + gs_app_set_kind (app, AS_APP_KIND_GENERIC); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_scope (app, AS_APP_SCOPE_SYSTEM); + gs_app_set_state (app, AS_APP_STATE_AVAILABLE_LOCAL); + + /* add default source */ + name = headerGetString (h, RPMTAG_NAME); + g_debug ("rpm: setting source to %s", name); + gs_app_add_source (app, name); + + /* add version */ + epoch = headerGetNumber (h, RPMTAG_EPOCH); + version = headerGetString (h, RPMTAG_VERSION); + release = headerGetString (h, RPMTAG_RELEASE); + if (epoch > 0) { + evr = g_strdup_printf ("%" G_GUINT64_FORMAT ":%s-%s", + epoch, version, release); + } else { + evr = g_strdup_printf ("%s-%s", + version, release); + } + g_debug ("rpm: setting version to %s", evr); + gs_app_set_version (app, evr); + + /* set size */ + size = headerGetNumber (h, RPMTAG_SIZE); + gs_app_set_size_installed (app, size); + + /* set license */ + license = headerGetString (h, RPMTAG_LICENSE); + if (license != NULL) { + g_autofree gchar *license_spdx = NULL; + license_spdx = as_utils_license_to_spdx (license); + gs_app_set_license (app, GS_APP_QUALITY_NORMAL, license_spdx); + g_debug ("rpm: setting license to %s", license_spdx); + } + + add_quirks_from_package_name (app, name); + + gs_app_list_add (list, app); + ret = TRUE; + +out: + if (rpmfd != NULL) + (void) Fclose (rpmfd); + return ret; +} + +static gchar ** +what_provides_decompose (gchar **values) +{ + GPtrArray *array = g_ptr_array_new (); + + /* iter on each provide string, and wrap it with the Fedora prefix */ + for (guint i = 0; values[i] != NULL; i++) { + g_ptr_array_add (array, g_strdup (values[i])); + g_ptr_array_add (array, g_strdup_printf ("gstreamer0.10(%s)", values[i])); + g_ptr_array_add (array, g_strdup_printf ("gstreamer1(%s)", values[i])); + g_ptr_array_add (array, g_strdup_printf ("font(%s)", values[i])); + g_ptr_array_add (array, g_strdup_printf ("mimehandler(%s)", values[i])); + g_ptr_array_add (array, g_strdup_printf ("postscriptdriver(%s)", values[i])); + g_ptr_array_add (array, g_strdup_printf ("plasma4(%s)", values[i])); + g_ptr_array_add (array, g_strdup_printf ("plasma5(%s)", values[i])); + } + g_ptr_array_add (array, NULL); + return (gchar **) g_ptr_array_free (array, FALSE); +} + +gboolean +gs_plugin_add_search_what_provides (GsPlugin *plugin, + gchar **search, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GMutexLocker) locker = NULL; + g_autoptr(GPtrArray) pkglist = NULL; + g_auto(GStrv) provides = NULL; + + locker = g_mutex_locker_new (&priv->mutex); + + if (priv->dnf_context == NULL) + return TRUE; + + provides = what_provides_decompose (search); + pkglist = find_packages_by_provides (dnf_context_get_sack (priv->dnf_context), provides); + for (guint i = 0; i < pkglist->len; i++) { + DnfPackage *pkg = g_ptr_array_index (pkglist, i); + g_autoptr(GsApp) app = NULL; + + app = gs_plugin_cache_lookup (plugin, dnf_package_get_nevra (pkg)); + if (app != NULL) { + gs_app_list_add (list, app); + continue; + } + + /* create new app */ + app = gs_app_new (NULL); + gs_app_set_metadata (app, "GnomeSoftware::Creator", gs_plugin_get_name (plugin)); + gs_app_set_management_plugin (app, gs_plugin_get_name (plugin)); + gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT); + app_set_rpm_ostree_packaging_format (app); + gs_app_set_kind (app, AS_APP_KIND_GENERIC); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_scope (app, AS_APP_SCOPE_SYSTEM); + gs_app_add_source (app, dnf_package_get_name (pkg)); + + gs_plugin_cache_add (plugin, dnf_package_get_nevra (pkg), app); + gs_app_list_add (list, app); + } + + return TRUE; +} + +gboolean +gs_plugin_add_sources (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GMutexLocker) locker = NULL; + GPtrArray *repos; + + locker = g_mutex_locker_new (&priv->mutex); + + if (priv->dnf_context == NULL) + return TRUE; + + repos = dnf_context_get_repos (priv->dnf_context); + if (repos == NULL) + return TRUE; + + for (guint i = 0; i < repos->len; i++) { + DnfRepo *repo = g_ptr_array_index (repos, i); + g_autofree gchar *description = NULL; + g_autoptr(GsApp) app = NULL; + gboolean enabled; + + /* hide these from the user */ + if (dnf_repo_is_devel (repo) || dnf_repo_is_source (repo)) + continue; + + app = gs_app_new (dnf_repo_get_id (repo)); + gs_app_set_management_plugin (app, gs_plugin_get_name (plugin)); + gs_app_set_kind (app, AS_APP_KIND_SOURCE); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE); + + enabled = (dnf_repo_get_enabled (repo) & DNF_REPO_ENABLED_PACKAGES) > 0; + gs_app_set_state (app, enabled ? AS_APP_STATE_INSTALLED : AS_APP_STATE_AVAILABLE); + + description = dnf_repo_get_description (repo); + gs_app_set_name (app, GS_APP_QUALITY_LOWEST, description); + gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, description); + + gs_app_list_add (list, app); + } + + return TRUE; +} diff --git a/plugins/rpm-ostree/meson.build b/plugins/rpm-ostree/meson.build new file mode 100644 index 0000000..840f9e7 --- /dev/null +++ b/plugins/rpm-ostree/meson.build @@ -0,0 +1,26 @@ +cargs = ['-DG_LOG_DOMAIN="GsPluginRpmOstree"'] + +rpmostree_generated = gnome.gdbus_codegen( + 'gs-rpmostree-generated', + 'org.projectatomic.rpmostree1.xml', + interface_prefix : 'org.projectatomic.rpmostree1', + namespace : 'GsRPMOSTree' +) + +shared_module( + 'gs_plugin_rpm-ostree', + rpmostree_generated, + sources : 'gs-plugin-rpm-ostree.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + install_rpath: join_paths(rpm_ostree.get_pkgconfig_variable('libdir'), 'rpm-ostree'), + c_args : cargs, + dependencies : [ plugin_libs, libdnf, ostree, rpm, rpm_ostree ], + link_with : [ + libgnomesoftware + ] +) diff --git a/plugins/rpm-ostree/org.projectatomic.rpmostree1.xml b/plugins/rpm-ostree/org.projectatomic.rpmostree1.xml new file mode 100644 index 0000000..6ae04a4 --- /dev/null +++ b/plugins/rpm-ostree/org.projectatomic.rpmostree1.xml @@ -0,0 +1,458 @@ +<!DOCTYPE node PUBLIC +"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" +"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"> +<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd"> + + <!-- Deployment dictionary keys: + 'id' (type 's') + 'osname' (type 's') + 'serial' (type 'i') + 'checksum' (type 's') + 'version' (type 's') + 'timestamp' (type 't') + 'origin' (type 's') + 'signatures' (type 'av') + 'packages' (type 'as') + 'booted' (type 'b') + --> + + <interface name="org.projectatomic.rpmostree1.Sysroot"> + <!-- The booted OSName --> + <property name="Booted" type="o" access="read"/> + + <!-- The system root path --> + <property name="Path" type="s" access="read"/> + + <!-- The values are (method-name, sender-name, object path) --> + <property name="ActiveTransaction" type="(sss)" access="read"/> + <!-- A DBus address - connect to it to access its methods --> + <property name="ActiveTransactionPath" type="s" access="read"/> + + <!-- (Currently) optional method to denote the client plans + to either invoke methods on the daemon, or monitor status. + If no clients are registered, the daemon may exit. + + 'id (type 's') - Package/component name (e.g. `cockpit`, `gnome-software`) + --> + <method name="RegisterClient"> + <arg type="a{sv}" name="options" direction="in"/> + </method> + + <!-- You must call this if your process is no longer interested in talking to + rpm-ostree, but will remain connected to the bus. + + No options are currently defined. + --> + <method name="UnregisterClient"> + <arg type="a{sv}" name="options" direction="in"/> + </method> + + <!-- Reload sysroot if changed. This can also be used as a way to sync with the daemon + to ensure e.g. D-Bus properties are updated before reading them. --> + <method name="Reload"> + </method> + + <!-- Like Reload, but also reload configuration files. --> + <method name="ReloadConfig"> + </method> + + <!-- none, check, stage --> + <property name="AutomaticUpdatePolicy" type="s" access="read"/> + + <method name="CreateOSName"> + <arg type="s" name="name"/> + <arg type="o" name="result" direction="out"/> + </method> + + <method name="GetOS"> + <arg name="name" type="s"/> + <arg name="object_path" type="o" direction="out"/> + </method> + + <!-- Array of all deployments in boot order --> + <property name="Deployments" type="aa{sv}" access="read"/> + </interface> + + <interface name="org.projectatomic.rpmostree1.OS"> + <property name="BootedDeployment" type="a{sv}" access="read"/> + <property name="DefaultDeployment" type="a{sv}" access="read"/> + <property name="RollbackDeployment" type="a{sv}" access="read"/> + + <!-- CachedUpdate dictionary keys: + 'osname' (type 's') + 'checksum' (type 's') + 'version' (type 's') + 'timestamp' (type 't') + 'origin' (type 's') + 'signatures' (type 'av') + 'gpg-enabled' (type 'b') + 'ref-has-new-commit' (type 'b') + TRUE if 'checksum' refers to a new base commit we're not booted in. + 'rpm-diff' (type 'a{sv}') + 'upgraded' (type 'a(us(ss)(ss))') + 'downgraded' (type 'a(us(ss)(ss))') + 'removed' (type 'a(usss)') + 'added' (type 'a(usss)') + 'advisories' (type 'a(suuasa{sv})') + --> + <property name="CachedUpdate" type="a{sv}" access="read"/> + <property name="HasCachedUpdateRpmDiff" type="b" access="read"/> + + <!-- Available options: + "mode" (type 's') + One of auto, none, check. Defaults to auto, which follows configured + policy (available in AutomaticUpdatePolicy property). + "output-to-self" (type 'b') + Whether output should go to the daemon itself rather than the + transaction. Defaults to TRUE. + + If automatic updates are not enabled, @enabled will be FALSE and + @transaction_address will be the empty string. + --> + <method name="AutomaticUpdateTrigger"> + <arg type="a{sv}" name="options" direction="in"/> + <arg type="b" name="enabled" direction="out"/> + <arg type="s" name="transaction_address" direction="out"/> + </method> + + <property name="Name" type="s" access="read"/> + + <method name="GetDeploymentsRpmDiff"> + <arg type="s" name="deployid0"/> + <arg type="s" name="deployid1"/> + <arg type="a(sua{sv})" name="result" direction="out"/> + </method> + + <!-- Revision may be a full checksum or version string. + + Available options: + "reboot" (type 'b') + --> + <method name="Deploy"> + <arg type="s" name="revision" direction="in"/> + <arg type="a{sv}" name="options" direction="in"/> + <arg type="s" name="transaction_address" direction="out"/> + <annotation name="org.gtk.GDBus.C.UnixFD" value="true"/> + </method> + + <!-- details dictionary keys: + 'osname' (type 's') + 'checksum' (type 's') + 'version' (type 's') + 'timestamp' (type 't') + 'origin' (type 's') + 'signatures' (type 'av') + --> + <method name="GetCachedDeployRpmDiff"> + <arg type="s" name="revision"/> + <arg type="as" name="packages"/> + <arg type="a(sua{sv})" name="result" direction="out"/> + <arg type="a{sv}" name="details" direction="out"/> + </method> + + <method name="DownloadDeployRpmDiff"> + <arg type="s" name="revision"/> + <arg type="as" name="packages"/> + <arg type="s" name="transaction_address" direction="out"/> + </method> + + <!-- Available options: + "allow-downgrade" (type 'b') + "reboot" (type 'b') + --> + <method name="Upgrade"> + <arg type="a{sv}" name="options" direction="in"/> + <arg type="s" name="transaction_address" direction="out"/> + <annotation name="org.gtk.GDBus.C.UnixFD" value="true"/> + </method> + + <!-- details dictionary keys: + 'osname' (type 's') + 'checksum' (type 's') + 'version' (type 's') + 'timestamp' (type 't') + 'origin' (type 's') + 'signatures' (type 'av') + --> + <method name="GetCachedUpdateRpmDiff"> + <arg type="s" name="deployid"/> + <arg type="a(sua{sv})" name="result" direction="out"/> + <arg type="a{sv}" name="details" direction="out"/> + </method> + + <method name="DownloadUpdateRpmDiff"> + <arg type="s" name="transaction_address" direction="out"/> + </method> + + <!-- Available options: + "reboot" (type 'b') + --> + <method name="Rollback"> + <arg type="a{sv}" name="options" direction="in"/> + <arg type="s" name="transaction_address" direction="out"/> + </method> + + <!-- Available options: + "reboot" (type 'b') + --> + <method name="ClearRollbackTarget"> + <arg type="a{sv}" name="options" direction="in"/> + <arg type="s" name="transaction_address" direction="out"/> + </method> + + <!-- Available options: + "skip-purge" (type 'b') + "reboot" (type 'b') + "revision" (type 's') + --> + <method name="Rebase"> + <arg type="a{sv}" name="options" direction="in"/> + <arg type="s" name="refspec"/> + <arg type="as" name="packages"/> + <arg type="s" name="transaction_address" direction="out"/> + <annotation name="org.gtk.GDBus.C.UnixFD" value="true"/> + </method> + + <!-- details dictionary keys: + 'osname' (type 's') + 'checksum' (type 's') + 'version' (type 's') + 'timestamp' (type 't') + 'origin' (type 's') + 'signatures' (type 'av') + --> + <method name="GetCachedRebaseRpmDiff"> + <arg type="s" name="refspec"/> + <arg type="as" name="packages"/> + <arg type="a(sua{sv})" name="result" direction="out"/> + <arg type="a{sv}" name="details" direction="out"/> + </method> + + <method name="DownloadRebaseRpmDiff"> + <arg type="s" name="refspec"/> + <arg type="as" name="packages"/> + <arg type="s" name="transaction_address" direction="out"/> + </method> + + <!-- Available options: + "reboot" (type 'b') + "dry-run" (type 'b') + --> + <method name="PkgChange"> + <arg type="a{sv}" name="options" direction="in"/> + <arg type="as" name="packages_added"/> + <arg type="as" name="packages_removed"/> + <arg type="s" name="transaction_address" direction="out"/> + <annotation name="org.gtk.GDBus.C.UnixFD" value="true"/> + </method> + + <method name="SetInitramfsState"> + <arg type="b" name="regenerate" direction="in"/> + <arg type="as" name="args" direction="in"/> + <arg type="a{sv}" name="options" direction="in"/> + <arg type="s" name="transaction_address" direction="out"/> + </method> + + <!-- Available options: + "reboot" (type 'b') + --> + <method name="KernelArgs"> + <arg type="s" name="existing_kernel_arg_string"/> + <arg type="as" name="kernel_args_added" direction="in"/> + <arg type="as" name="kernel_args_replaced" direction="in"/> + <arg type="as" name="kernel_args_removed" direction="in"/> + <arg type="a{sv}" name="options" direction="in"/> + <arg type="s" name="transaction_address" direction="out"/> + </method> + + <method name="GetDeploymentBootConfig"> + <arg type="s" name="deployid" /> + <arg type="b" name="is_pending" direction="in"/> + <arg type="a{sv}" name="bootconfig" direction="out"/> + </method> + + <method name="Cleanup"> + <arg type="as" name="elements" direction="in"/> + <arg type="s" name="transaction_address" direction="out"/> + </method> + + <method name="RefreshMd"> + <arg type="a{sv}" name="options" direction="in"/> + <arg type="s" name="transaction_address" direction="out"/> + </method> + + <!-- Set options in yum .repo files --> + <method name="ModifyYumRepo"> + <arg type="s" name="repo_id" direction="in"/> + <arg type="a{ss}" name="settings" direction="in"/> + <arg type="s" name="transaction_address" direction="out"/> + </method> + + <!-- Available modifiers: + "set-refspec" (type 's') + "set-revision" (type 's') + "install-packages" (type 'as') + "uninstall-packages" (type 'as') + "install-local-packages" (type 'ah') + "override-remove-packages" (type 'as') + "override-reset-packages" (type 'as') + "override-replace-packages" (type 'as') + "override-replace-local-packages" (type 'ah') + "custom-origin" (type '(ss)') + + Available options: + "reboot" (type 'b') + Initiate a reboot after transaction. + "allow-downgrade" (type 'b') + Allow newly pulled bases to have older timestamps than the + current base. Defaults to TRUE if either "set-refspec" or + "set-revision" is specified. + "skip-purge" (type 'b') + Do not purge the old refspec. Only valid if "set-refspec" is + specified. + "no-pull-base" (type 'b') + Do not pull a base layer from the remote. Not valid if + either "set-refspec" or "set-revision" is specified. + "dry-run" (type 'b') + Stop short of deploying the new tree. If layering packages, + the pkg diff is printed but packages are not downloaded or + imported. + "no-layering" (type 'b') + Remove all package requests. Requests in "install-packages" + are still subsequently processed if specified. + "no-overrides" (type 'b') + Remove all active overrides. Not valid if any override + modifiers are specified. + "no-initramfs" (type 'b') + Disable any initramfs regeneration. + "cache-only" (type 'b') + Do not update rpmmd repo metadata cache or ostree refspec. + Not valid if "download-only" is specified. + "download-only" (type 'b') + Update rpmmd repo metadata cache and ostree refspec. Do not + perform any deployments. This is like "dry-run" except that + the latter does not download and import packages. Not valid + if "cache-only" or "dry-run" is specified. + "allow-inactive-requests" (type 'b') + When installing packages, allow package requests which would + not immediately be active. + "idempotent-layering" (type 'b') + Don't error out on requests in install-* or uninstall-* + modifiers that are already satisfied. + --> + <method name="UpdateDeployment"> + <arg type="a{sv}" name="modifiers" direction="in"/> + <arg type="a{sv}" name="options" direction="in"/> + <arg type="s" name="transaction_address" direction="out"/> + <annotation name="org.gtk.GDBus.C.UnixFD" value="true"/> + </method> + + </interface> + + <interface name="org.projectatomic.rpmostree1.OSExperimental"> + + <!-- Just a test method --> + <method name="Moo"> + <arg type="b" name="utf8" direction="in"/> + <arg type="s" name="result" direction="out"/> + </method> + + <method name="LiveFs"> + <arg type="a{sv}" name="options" direction="in"/> + <arg type="s" name="transaction_address" direction="out"/> + </method> + + </interface> + + <interface name="org.projectatomic.rpmostree1.Transaction"> + + <!-- A single-line human-readable string --> + <property name="Title" type="s" access="read"/> + + <!-- Yes, we can. --> + <method name="Cancel"/> + + <!-- For a client to call when ready to receive signals. + The return boolean indicates whether the transaction was + started by this method call (true) or was already started + by another client (false). --> + <method name="Start"> + <arg type="b" name="started" direction="out"/> + </method> + + <signal name="Finished"> + <arg name="success" type="b" direction="out"/> + <arg name="error_message" type="s" direction="out"/> + </signal> + + <!-- For miscellaneous messages; line-buffered. --> + <signal name="Message"> + <arg name="text" type="s" direction="out"/> + </signal> + + <!-- Tasks are notifications that work is being done. --> + <signal name="TaskBegin"> + <arg name="text" type="s" direction="out"/> + </signal> + + <signal name="TaskEnd"> + <arg name="text" type="s" direction="out"/> + </signal> + + <!-- Generic percentage progress. --> + <signal name="PercentProgress"> + <arg name="text" type="s" direction="out"/> + <arg name="percentage" type="u" direction="out"/> + </signal> + + <signal name="DownloadProgress"> + <!-- time data, format is: + start time, elapsed seconds + --> + <arg name="time" type="(tt)" direction="out"/> + + <!-- + outstanding data counts, format is: + (outstanding fetches, outstanding writes) + --> + <arg name="outstanding" type="(uu)" direction="out"/> + + <!-- + metadata counts, format is: + (scanned, fetched, outstanding) + --> + <arg name="metadata" type="(uuu)" direction="out"/> + + <!-- + delta data, format is: + (total parts, fetched parts, total super blocks, total size) + --> + <arg name="delta" type="(uuut)" direction="out"/> + + <!-- + content data, format is: + (fetched, requested) + --> + <arg name="content" type="(uu)" direction="out"/> + + <!-- + transfer data, format is: + (bytes transfered, bytes/s) + --> + <arg name="transfer" type="(tt)" direction="out"/> + </signal> + + <signal name="SignatureProgress"> + <!-- An ostree GVariant containing signature data + see ostree_gpg_verify_result_get_all. + --> + <arg name="signature" type="av" direction="out"/> + <!-- The signed commit --> + <arg name="commit" type="s" direction="out"/> + </signal> + + <!-- Indicates progress signals are done and subsequent + Message signals should be output on separate lines. --> + <signal name="ProgressEnd"/> + </interface> +</node> diff --git a/plugins/snap/gs-plugin-snap.c b/plugins/snap/gs-plugin-snap.c new file mode 100644 index 0000000..05d0294 --- /dev/null +++ b/plugins/snap/gs-plugin-snap.c @@ -0,0 +1,1325 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2015-2018 Canonical Ltd + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <gio/gdesktopappinfo.h> +#include <glib/gi18n.h> +#include <json-glib/json-glib.h> +#include <snapd-glib/snapd-glib.h> +#include <gnome-software.h> + +struct GsPluginData { + gchar *store_name; + gchar *store_hostname; + SnapdSystemConfinement system_confinement; + + GMutex store_snaps_lock; + GHashTable *store_snaps; +}; + +typedef struct { + SnapdSnap *snap; + gboolean full_details; +} CacheEntry; + +static CacheEntry * +cache_entry_new (SnapdSnap *snap, gboolean full_details) +{ + CacheEntry *entry = g_slice_new (CacheEntry); + entry->snap = g_object_ref (snap); + entry->full_details = full_details; + return entry; +} + +static void +cache_entry_free (CacheEntry *entry) +{ + g_object_unref (entry->snap); + g_slice_free (CacheEntry, entry); +} + +static SnapdAuthData * +get_auth_data (GsPlugin *plugin) +{ + g_autofree gchar *path = NULL; + g_autoptr(JsonParser) parser = NULL; + JsonNode *root; + JsonObject *object; + const gchar *macaroon; + g_autoptr(GPtrArray) discharges = NULL; + g_autoptr(GError) error = NULL; + + path = g_build_filename (g_get_home_dir (), ".snap", "auth.json", NULL); + parser = json_parser_new (); + if (!json_parser_load_from_file (parser, path, &error)) { + if (!g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) + g_warning ("Failed to load snap auth data: %s", error->message); + return NULL; + } + + root = json_parser_get_root (parser); + if (root == NULL) + return NULL; + + if (json_node_get_node_type (root) != JSON_NODE_OBJECT) { + g_warning ("Ignoring invalid snap auth data in %s", path); + return NULL; + } + object = json_node_get_object (root); + if (!json_object_has_member (object, "macaroon")) { + g_warning ("Ignoring invalid snap auth data in %s", path); + return NULL; + } + macaroon = json_object_get_string_member (object, "macaroon"); + discharges = g_ptr_array_new (); + if (json_object_has_member (object, "discharges")) { + JsonArray *discharge_array; + + discharge_array = json_object_get_array_member (object, "discharges"); + for (guint i = 0; i < json_array_get_length (discharge_array); i++) + g_ptr_array_add (discharges, json_array_get_string_element (discharge_array, i)); + } + g_ptr_array_add (discharges, NULL); + + return snapd_auth_data_new (macaroon, (GStrv) discharges->pdata); +} + +static SnapdClient * +get_client (GsPlugin *plugin, GError **error) +{ + g_autoptr(SnapdClient) client = NULL; + const gchar *old_user_agent; + g_autofree gchar *user_agent = NULL; + g_autoptr(SnapdAuthData) auth_data = NULL; + + client = snapd_client_new (); + snapd_client_set_allow_interaction (client, TRUE); + old_user_agent = snapd_client_get_user_agent (client); + user_agent = g_strdup_printf ("%s %s", gs_user_agent (), old_user_agent); + snapd_client_set_user_agent (client, user_agent); + + auth_data = get_auth_data (plugin); + snapd_client_set_auth_data (client, auth_data); + + return g_steal_pointer (&client); +} + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData)); + g_autoptr(SnapdClient) client = NULL; + g_autoptr (GError) error = NULL; + + g_mutex_init (&priv->store_snaps_lock); + + client = get_client (plugin, &error); + if (client == NULL) { + gs_plugin_set_enabled (plugin, FALSE); + return; + } + + priv->store_snaps = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, (GDestroyNotify) cache_entry_free); + + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "desktop-categories"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_BETTER_THAN, "packagekit"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_BEFORE, "icons"); + + /* Override hardcoded popular apps */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_BEFORE, "hardcoded-popular"); + + /* set name of MetaInfo file */ + gs_plugin_set_appstream_id (plugin, "org.gnome.Software.Plugin.Snap"); +} + +void +gs_plugin_adopt_app (GsPlugin *plugin, GsApp *app) +{ + if (gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_SNAP) + gs_app_set_management_plugin (app, "snap"); + + if (gs_app_get_id (app) != NULL && g_str_has_prefix (gs_app_get_id (app), "io.snapcraft.")) { + g_autofree gchar *name_and_id = NULL; + gchar *divider, *snap_name;/*, *id;*/ + + name_and_id = g_strdup (gs_app_get_id (app) + strlen ("io.snapcraft.")); + divider = strrchr (name_and_id, '-'); + if (divider != NULL) { + *divider = '\0'; + snap_name = name_and_id; + /*id = divider + 1;*/ /* NOTE: Should probably validate ID */ + + gs_app_set_management_plugin (app, "snap"); + gs_app_set_metadata (app, "snap::name", snap_name); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_SNAP); + } + } +} + +static void +snapd_error_convert (GError **perror) +{ + GError *error = perror != NULL ? *perror : NULL; + + /* not set */ + if (error == NULL) + return; + + /* this are allowed for low-level errors */ + if (gs_utils_error_convert_gio (perror)) + return; + + /* custom to this plugin */ + if (error->domain == SNAPD_ERROR) { + switch (error->code) { + case SNAPD_ERROR_AUTH_DATA_REQUIRED: + error->code = GS_PLUGIN_ERROR_AUTH_REQUIRED; + g_free (error->message); + error->message = g_strdup ("Requires authentication with @snapd"); + break; + case SNAPD_ERROR_AUTH_DATA_INVALID: + case SNAPD_ERROR_TWO_FACTOR_INVALID: + error->code = GS_PLUGIN_ERROR_AUTH_INVALID; + break; + case SNAPD_ERROR_AUTH_CANCELLED: + error->code = GS_PLUGIN_ERROR_CANCELLED; + break; + case SNAPD_ERROR_CONNECTION_FAILED: + case SNAPD_ERROR_WRITE_FAILED: + case SNAPD_ERROR_READ_FAILED: + case SNAPD_ERROR_BAD_REQUEST: + case SNAPD_ERROR_BAD_RESPONSE: + case SNAPD_ERROR_PERMISSION_DENIED: + case SNAPD_ERROR_FAILED: + case SNAPD_ERROR_TERMS_NOT_ACCEPTED: + case SNAPD_ERROR_PAYMENT_NOT_SETUP: + case SNAPD_ERROR_PAYMENT_DECLINED: + case SNAPD_ERROR_ALREADY_INSTALLED: + case SNAPD_ERROR_NOT_INSTALLED: + case SNAPD_ERROR_NO_UPDATE_AVAILABLE: + case SNAPD_ERROR_PASSWORD_POLICY_ERROR: + case SNAPD_ERROR_NEEDS_DEVMODE: + case SNAPD_ERROR_NEEDS_CLASSIC: + case SNAPD_ERROR_NEEDS_CLASSIC_SYSTEM: + default: + error->code = GS_PLUGIN_ERROR_FAILED; + break; + } + } else { + g_warning ("can't reliably fixup error from domain %s", + g_quark_to_string (error->domain)); + error->code = GS_PLUGIN_ERROR_FAILED; + } + error->domain = GS_PLUGIN_ERROR; +} + +gboolean +gs_plugin_setup (GsPlugin *plugin, GCancellable *cancellable, GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(SnapdClient) client = NULL; + g_autoptr(SnapdSystemInformation) system_information = NULL; + + client = get_client (plugin, error); + if (client == NULL) + return FALSE; + + system_information = snapd_client_get_system_information_sync (client, cancellable, error); + if (system_information == NULL) + return FALSE; + priv->store_name = g_strdup (snapd_system_information_get_store (system_information)); + if (priv->store_name == NULL) { + priv->store_name = g_strdup (/* TRANSLATORS: default snap store name */ + _("Snap Store")); + priv->store_hostname = g_strdup ("snapcraft.io"); + } + priv->system_confinement = snapd_system_information_get_confinement (system_information); + + /* success */ + return TRUE; +} + +static SnapdSnap * +store_snap_cache_lookup (GsPlugin *plugin, const gchar *name, gboolean need_details) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + CacheEntry *entry; + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->store_snaps_lock); + + entry = g_hash_table_lookup (priv->store_snaps, name); + if (entry == NULL) + return NULL; + + if (need_details && !entry->full_details) + return NULL; + + return g_object_ref (entry->snap); +} + +static void +store_snap_cache_update (GsPlugin *plugin, GPtrArray *snaps, gboolean full_details) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->store_snaps_lock); + guint i; + + for (i = 0; i < snaps->len; i++) { + SnapdSnap *snap = snaps->pdata[i]; + g_hash_table_insert (priv->store_snaps, g_strdup (snapd_snap_get_name (snap)), cache_entry_new (snap, full_details)); + } +} + +static GPtrArray * +find_snaps (GsPlugin *plugin, SnapdFindFlags flags, const gchar *section, const gchar *query, GCancellable *cancellable, GError **error) +{ + g_autoptr(SnapdClient) client = NULL; + g_autoptr(GPtrArray) snaps = NULL; + + client = get_client (plugin, error); + if (client == NULL) + return NULL; + + snaps = snapd_client_find_section_sync (client, flags, section, query, NULL, cancellable, error); + if (snaps == NULL) { + snapd_error_convert (error); + return NULL; + } + + store_snap_cache_update (plugin, snaps, flags & SNAPD_FIND_FLAGS_MATCH_NAME); + + return g_steal_pointer (&snaps); +} + +static GsApp * +snap_to_app (GsPlugin *plugin, SnapdSnap *snap) +{ + GStrv common_ids; + g_autofree gchar *appstream_id = NULL; + g_autofree gchar *unique_id = NULL; + g_autoptr(GsApp) app = NULL; + SnapdConfinement confinement; + + /* Get the AppStream ID from the snap, or generate a fallback one */ + common_ids = snapd_snap_get_common_ids (snap); + if (g_strv_length (common_ids) == 1) + appstream_id = g_strdup (common_ids[0]); + else + appstream_id = g_strdup_printf ("io.snapcraft.%s-%s", snapd_snap_get_name (snap), snapd_snap_get_id (snap)); + + switch (snapd_snap_get_snap_type (snap)) { + case SNAPD_SNAP_TYPE_APP: + unique_id = g_strdup_printf ("system/snap/*/desktop/%s/*", appstream_id); + break; + case SNAPD_SNAP_TYPE_KERNEL: + case SNAPD_SNAP_TYPE_GADGET: + case SNAPD_SNAP_TYPE_OS: + unique_id = g_strdup_printf ("system/snap/*/runtime/%s/*", appstream_id); + break; + default: + case SNAPD_SNAP_TYPE_UNKNOWN: + unique_id = g_strdup_printf ("system/snap/*/*/%s/*", appstream_id); + break; + } + + app = gs_plugin_cache_lookup (plugin, unique_id); + if (app == NULL) { + app = gs_app_new (NULL); + gs_app_set_from_unique_id (app, unique_id); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_SNAP); + gs_app_set_metadata (app, "snap::name", snapd_snap_get_name (snap)); + gs_plugin_cache_add (plugin, unique_id, app); + } + + gs_app_set_management_plugin (app, "snap"); + gs_app_add_quirk (app, GS_APP_QUIRK_DO_NOT_AUTO_UPDATE); + if (gs_app_get_kind (app) != AS_APP_KIND_DESKTOP) + gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE); + if (gs_plugin_check_distro_id (plugin, "ubuntu")) + gs_app_add_quirk (app, GS_APP_QUIRK_PROVENANCE); + + confinement = snapd_snap_get_confinement (snap); + if (confinement != SNAPD_CONFINEMENT_UNKNOWN) { + GEnumClass *enum_class = g_type_class_ref (SNAPD_TYPE_CONFINEMENT); + gs_app_set_metadata (app, "snap::confinement", g_enum_get_value (enum_class, confinement)->value_nick); + g_type_class_unref (enum_class); + } + + return g_steal_pointer (&app); +} + +gboolean +gs_plugin_url_to_app (GsPlugin *plugin, + GsAppList *list, + const gchar *url, + GCancellable *cancellable, + GError **error) +{ + g_autofree gchar *scheme = NULL; + g_autofree gchar *path = NULL; + g_autoptr(GPtrArray) snaps = NULL; + g_autoptr(GsApp) app = NULL; + + /* not us */ + scheme = gs_utils_get_url_scheme (url); + if (g_strcmp0 (scheme, "snap") != 0) + return TRUE; + + /* create app */ + path = gs_utils_get_url_path (url); + snaps = find_snaps (plugin, SNAPD_FIND_FLAGS_SCOPE_WIDE | SNAPD_FIND_FLAGS_MATCH_NAME, NULL, path, cancellable, NULL); + if (snaps == NULL || snaps->len < 1) + return TRUE; + + app = snap_to_app (plugin, g_ptr_array_index (snaps, 0)); + gs_app_list_add (list, app); + + return TRUE; +} + +void +gs_plugin_destroy (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_free (priv->store_name); + g_free (priv->store_hostname); + g_clear_pointer (&priv->store_snaps, g_hash_table_unref); + g_mutex_clear (&priv->store_snaps_lock); +} + +static gboolean +is_banner_image (const gchar *filename) +{ + /* Check if this screenshot was uploaded as "banner.png" or "banner.jpg". + * The server optionally adds a 7 character suffix onto it if it would collide with + * an existing name, e.g. "banner_MgEy4MI.png" + * See https://forum.snapcraft.io/t/improve-method-for-setting-featured-snap-banner-image-in-store/ + */ + return g_regex_match_simple ("^banner(?:_[a-zA-Z0-9]{7})?\\.(?:png|jpg)$", filename, 0, 0); +} + +static gboolean +is_banner_icon_image (const gchar *filename) +{ + /* Check if this screenshot was uploaded as "banner-icon.png" or "banner-icon.jpg". + * The server optionally adds a 7 character suffix onto it if it would collide with + * an existing name, e.g. "banner-icon_Ugn6pmj.png" + * See https://forum.snapcraft.io/t/improve-method-for-setting-featured-snap-banner-image-in-store/ + */ + return g_regex_match_simple ("^banner-icon(?:_[a-zA-Z0-9]{7})?\\.(?:png|jpg)$", filename, 0, 0); +} + +gboolean +gs_plugin_add_popular (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GPtrArray) snaps = NULL; + guint i; + + snaps = find_snaps (plugin, SNAPD_FIND_FLAGS_SCOPE_WIDE, "featured", NULL, cancellable, error); + if (snaps == NULL) + return FALSE; + + for (i = 0; i < snaps->len; i++) { + g_autoptr(GsApp) app = snap_to_app (plugin, g_ptr_array_index (snaps, i)); + gs_app_list_add (list, app); + } + + return TRUE; +} + +gboolean +gs_plugin_add_category_apps (GsPlugin *plugin, + GsCategory *category, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsCategory *c; + g_autoptr(GString) id = NULL; + const gchar *sections = NULL; + + id = g_string_new (""); + for (c = category; c != NULL; c = gs_category_get_parent (c)) { + if (c != category) + g_string_prepend (id, "/"); + g_string_prepend (id, gs_category_get_id (c)); + } + + /* + * Unused categories: + * + * health-and-fitness + * personalisation + * devices-and-iot + * security + * server-and-cloud + * entertainment + */ + + if (strcmp (id->str, "games/featured") == 0) + sections = "games"; + else if (strcmp (id->str, "audio-video/featured") == 0) + sections = "music-and-audio"; + else if (strcmp (id->str, "graphics/featured") == 0) + sections = "photo-and-video;art-and-design"; + else if (strcmp (id->str, "communication/featured") == 0) + sections = "social;news-and-weather"; + else if (strcmp (id->str, "productivity/featured") == 0) + sections = "productivity;finance"; + else if (strcmp (id->str, "developer-tools/featured") == 0) + sections = "development"; + else if (strcmp (id->str, "utilities/featured") == 0) + sections = "utilities"; + else if (strcmp (id->str, "education-science/featured") == 0) + sections = "education;science"; + else if (strcmp (id->str, "reference/featured") == 0) + sections = "books-and-reference"; + + if (sections != NULL) { + g_auto(GStrv) tokens = NULL; + int i; + + tokens = g_strsplit (sections, ";", -1); + for (i = 0; tokens[i] != NULL; i++) { + g_autoptr(GPtrArray) snaps = NULL; + guint j; + + snaps = find_snaps (plugin, SNAPD_FIND_FLAGS_SCOPE_WIDE, tokens[i], NULL, cancellable, error); + if (snaps == NULL) + return FALSE; + for (j = 0; j < snaps->len; j++) { + g_autoptr(GsApp) app = snap_to_app (plugin, g_ptr_array_index (snaps, j)); + gs_app_list_add (list, app); + } + } + } + return TRUE; +} + +gboolean +gs_plugin_add_installed (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(SnapdClient) client = NULL; + g_autoptr(GPtrArray) snaps = NULL; + guint i; + + client = get_client (plugin, error); + if (client == NULL) + return FALSE; + + snaps = snapd_client_get_snaps_sync (client, SNAPD_GET_SNAPS_FLAGS_NONE, NULL, cancellable, error); + if (snaps == NULL) { + snapd_error_convert (error); + return FALSE; + } + + for (i = 0; i < snaps->len; i++) { + SnapdSnap *snap = g_ptr_array_index (snaps, i); + g_autoptr(GsApp) app = NULL; + + app = snap_to_app (plugin, snap); + gs_app_list_add (list, app); + } + + return TRUE; +} + +gboolean +gs_plugin_add_search (GsPlugin *plugin, + gchar **values, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + g_autofree gchar *query = NULL; + g_autoptr(GPtrArray) snaps = NULL; + guint i; + + query = g_strjoinv (" ", values); + snaps = find_snaps (plugin, SNAPD_FIND_FLAGS_SCOPE_WIDE, NULL, query, cancellable, error); + if (snaps == NULL) + return FALSE; + + for (i = 0; i < snaps->len; i++) { + g_autoptr(GsApp) app = snap_to_app (plugin, g_ptr_array_index (snaps, i)); + gs_app_list_add (list, app); + } + + return TRUE; +} + +static SnapdSnap * +get_store_snap (GsPlugin *plugin, const gchar *name, gboolean need_details, GCancellable *cancellable, GError **error) +{ + SnapdSnap *snap = NULL; + g_autoptr(GPtrArray) snaps = NULL; + + /* use cached version if available */ + snap = store_snap_cache_lookup (plugin, name, need_details); + if (snap != NULL) + return g_object_ref (snap); + + snaps = find_snaps (plugin, SNAPD_FIND_FLAGS_SCOPE_WIDE | SNAPD_FIND_FLAGS_MATCH_NAME, NULL, name, cancellable, error); + if (snaps == NULL || snaps->len < 1) + return NULL; + + return g_object_ref (g_ptr_array_index (snaps, 0)); +} + +static int +track_value (const gchar *track, GStrv tracks) +{ + int r = 0; + while (tracks[r] != NULL && strcmp (track, tracks[r]) != 0) + r++; + return r; +} + +static int +risk_value (const gchar *risk) +{ + if (strcmp (risk, "stable") == 0) + return 0; + else if (strcmp (risk, "candidate") == 0) + return 1; + else if (strcmp (risk, "beta") == 0) + return 2; + else if (strcmp (risk, "edge") == 0) + return 3; + else + return 4; +} + +static int +compare_channel (gconstpointer a, gconstpointer b, gpointer user_data) +{ + SnapdChannel *channel_a = *(SnapdChannel **)a, *channel_b = *(SnapdChannel **)b; + GStrv tracks = user_data; + int r; + + r = track_value (snapd_channel_get_track (channel_a), tracks) - track_value (snapd_channel_get_track (channel_b), tracks); + if (r != 0) + return r; + + r = g_strcmp0 (snapd_channel_get_risk (channel_a), snapd_channel_get_risk (channel_b)); + if (r != 0) { + int r2; + + r2 = risk_value (snapd_channel_get_risk (channel_a)) - risk_value (snapd_channel_get_risk (channel_b)); + if (r2 != 0) + return r2; + else + return r; + } + + return g_strcmp0 (snapd_channel_get_branch (channel_a), snapd_channel_get_branch (channel_b)); +} + +static gchar * +expand_channel_name (const gchar *name) +{ + g_auto(GStrv) tokens = NULL; + const gchar *risks[] = { "stable", "candidate", "beta", "edge", NULL }; + + if (name == NULL) + return NULL; + + tokens = g_strsplit (name, "/", -1); + for (int i = 0; risks[i] != NULL; i++) { + if (strcmp (tokens[0], risks[i]) == 0) + return g_strconcat ("latest/", name, NULL); + } + + return g_strdup (name); +} + +gboolean +gs_plugin_add_alternates (GsPlugin *plugin, + GsApp *app, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + const gchar *snap_name; + g_autoptr(SnapdSnap) snap = NULL; + GStrv tracks; + GPtrArray *channels; + g_autoptr(GPtrArray) sorted_channels = NULL; + + /* not us */ + if (g_strcmp0 (gs_app_get_management_plugin (app), "snap") != 0) + return TRUE; + + snap_name = gs_app_get_metadata_item (app, "snap::name"); + + snap = get_store_snap (plugin, snap_name, TRUE, cancellable, NULL); + if (snap == NULL) { + g_warning ("Failed to get store snap %s\n", snap_name); + return TRUE; + } + + tracks = snapd_snap_get_tracks (snap); + channels = snapd_snap_get_channels (snap); + sorted_channels = g_ptr_array_new (); + for (guint i = 0; i < channels->len; i++) { + SnapdChannel *channel = g_ptr_array_index (channels, i); + g_ptr_array_add (sorted_channels, channel); + } + g_ptr_array_sort_with_data (sorted_channels, compare_channel, tracks); + + for (guint i = 0; i < sorted_channels->len; i++) { + SnapdChannel *channel = g_ptr_array_index (sorted_channels, i); + g_autoptr(GsApp) a; + g_autofree gchar *expanded_name = NULL; + + a = gs_app_new (NULL); + gs_app_set_bundle_kind (a, AS_BUNDLE_KIND_SNAP); + gs_app_set_metadata (a, "snap::name", snap_name); + expanded_name = expand_channel_name (snapd_channel_get_name (channel)); + gs_app_set_branch (a, expanded_name); + gs_app_list_add (list, a); + } + + return TRUE; +} + +static gboolean +load_snap_icon (GsApp *app, SnapdClient *client, SnapdSnap *snap, GCancellable *cancellable) +{ + const gchar *icon_url; + g_autoptr(SnapdIcon) icon = NULL; + g_autoptr(GInputStream) input_stream = NULL; + g_autoptr(GdkPixbuf) pixbuf = NULL; + g_autoptr(GError) error = NULL; + + icon_url = snapd_snap_get_icon (snap); + if (icon_url == NULL || strcmp (icon_url, "") == 0) + return FALSE; + + icon = snapd_client_get_icon_sync (client, gs_app_get_metadata_item (app, "snap::name"), cancellable, &error); + if (icon == NULL) { + if (!g_error_matches (error, SNAPD_ERROR, SNAPD_ERROR_NOT_FOUND)) + g_warning ("Failed to load snap icon: %s", error->message); + return FALSE; + } + + input_stream = g_memory_input_stream_new_from_bytes (snapd_icon_get_data (icon)); + pixbuf = gdk_pixbuf_new_from_stream_at_scale (input_stream, 64, 64, TRUE, cancellable, &error); + if (pixbuf == NULL) { + g_warning ("Failed to decode snap icon %s: %s", icon_url, error->message); + return FALSE; + } + gs_app_set_pixbuf (app, pixbuf); + + return TRUE; +} + +static gboolean +app_name_matches_snap_name (SnapdSnap *snap, SnapdApp *app) +{ + return g_strcmp0 (snapd_snap_get_name (snap), snapd_app_get_name (app)) == 0; +} + +static SnapdApp * +get_primary_app (SnapdSnap *snap) +{ + GPtrArray *apps; + guint i; + SnapdApp *primary_app = NULL; + + /* Pick the "main" app from the snap. In order of + * preference, we want to pick: + * + * 1. the main app, provided it has a desktop file + * 2. the first app with a desktop file + * 3. the main app + * 4. the first app + * + * The "main app" is one whose name matches the snap name. + */ + apps = snapd_snap_get_apps (snap); + for (i = 0; i < apps->len; i++) { + SnapdApp *app = apps->pdata[i]; + + if (primary_app == NULL || + (snapd_app_get_desktop_file (primary_app) == NULL && snapd_app_get_desktop_file (app) != NULL) || + (!app_name_matches_snap_name (snap, primary_app) && app_name_matches_snap_name (snap, app))) + primary_app = app; + } + + return primary_app; +} + +static gboolean +load_desktop_icon (GsApp *app, SnapdSnap *snap) +{ + GPtrArray *apps; + guint i; + + apps = snapd_snap_get_apps (snap); + for (i = 0; i < apps->len; i++) { + SnapdApp *snap_app = apps->pdata[i]; + const gchar *desktop_file_path; + g_autoptr(GKeyFile) desktop_file = NULL; + g_autoptr(GError) error = NULL; + g_autofree gchar *icon_value = NULL; + g_autoptr(AsIcon) icon = NULL; + + desktop_file_path = snapd_app_get_desktop_file (snap_app); + if (desktop_file_path == NULL) + continue; + + desktop_file = g_key_file_new (); + if (!g_key_file_load_from_file (desktop_file, desktop_file_path, G_KEY_FILE_NONE, &error)) { + g_warning ("Failed to load desktop file %s: %s", desktop_file_path, error->message); + continue; + } + + icon_value = g_key_file_get_string (desktop_file, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_ICON, &error); + if (icon_value == NULL) { + g_warning ("Failed to get desktop file icon %s: %s", desktop_file_path, error->message); + continue; + } + + icon = as_icon_new (); + if (g_str_has_prefix (icon_value, "/")) { + as_icon_set_kind (icon, AS_ICON_KIND_LOCAL); + as_icon_set_filename (icon, icon_value); + } else { + as_icon_set_kind (icon, AS_ICON_KIND_STOCK); + as_icon_set_name (icon, icon_value); + } + gs_app_add_icon (app, icon); + + return TRUE; + } + + return FALSE; +} + +static gboolean +load_store_icon (GsApp *app, SnapdSnap *snap) +{ + const gchar *icon_url; + + icon_url = snapd_snap_get_icon (snap); + if (icon_url == NULL) + return FALSE; + + if (g_str_has_prefix (icon_url, "http://") || g_str_has_prefix (icon_url, "https://")) { + g_autoptr(AsIcon) icon = as_icon_new (); + as_icon_set_kind (icon, AS_ICON_KIND_REMOTE); + as_icon_set_url (icon, icon_url); + gs_app_add_icon (app, icon); + return TRUE; + } + + return FALSE; +} + +static gboolean +load_icon (GsPlugin *plugin, SnapdClient *client, GsApp *app, const gchar *id, SnapdSnap *local_snap, SnapdSnap *store_snap, GCancellable *cancellable) +{ + if (local_snap != NULL) { + if (load_snap_icon (app, client, local_snap, cancellable)) + return TRUE; + if (load_desktop_icon (app, local_snap)) + return TRUE; + } + + if (store_snap == NULL) + store_snap = get_store_snap (plugin, gs_app_get_metadata_item (app, "snap::name"), FALSE, cancellable, NULL); + if (store_snap != NULL) + return load_store_icon (app, store_snap); + + return FALSE; +} + +static gchar * +gs_plugin_snap_get_description_safe (SnapdSnap *snap) +{ + GString *str = g_string_new (snapd_snap_get_description (snap)); + as_utils_string_replace (str, "\r", ""); + as_utils_string_replace (str, " ", " "); + return g_string_free (str, FALSE); +} + +static void +refine_screenshots (GsApp *app, SnapdSnap *snap) +{ + GPtrArray *media; + guint i; + + media = snapd_snap_get_media (snap); + for (i = 0; i < media->len; i++) { + SnapdMedia *m = media->pdata[i]; + const gchar *url; + g_autofree gchar *filename = NULL; + g_autoptr(AsScreenshot) ss = NULL; + g_autoptr(AsImage) image = NULL; + + if (g_strcmp0 (snapd_media_get_media_type (m), "screenshot") != 0) + continue; + + /* skip screenshots used for banner when app is featured */ + url = snapd_media_get_url (m); + filename = g_path_get_basename (url); + if (is_banner_image (filename) || is_banner_icon_image (filename)) + continue; + + ss = as_screenshot_new (); + as_screenshot_set_kind (ss, AS_SCREENSHOT_KIND_NORMAL); + image = as_image_new (); + as_image_set_url (image, snapd_media_get_url (m)); + as_image_set_kind (image, AS_IMAGE_KIND_SOURCE); + as_image_set_width (image, snapd_media_get_width (m)); + as_image_set_height (image, snapd_media_get_height (m)); + as_screenshot_add_image (ss, image); + gs_app_add_screenshot (app, ss); + } +} + +static gboolean +refine_app_with_client (GsPlugin *plugin, + SnapdClient *client, + GsApp *app, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + const gchar *snap_name, *name, *version; + g_autofree gchar *channel = NULL; + g_autofree gchar *store_channel = NULL; + g_autofree gchar *tracking_channel = NULL; + gboolean need_details = FALSE; + SnapdConfinement confinement = SNAPD_CONFINEMENT_UNKNOWN; + g_autoptr(SnapdSnap) local_snap = NULL; + g_autoptr(SnapdSnap) store_snap = NULL; + SnapdSnap *snap; + const gchar *developer_name; + g_autofree gchar *description = NULL; + + /* not us */ + if (g_strcmp0 (gs_app_get_management_plugin (app), "snap") != 0) + return TRUE; + + snap_name = gs_app_get_metadata_item (app, "snap::name"); + channel = g_strdup (gs_app_get_branch (app)); + + /* get information from locally installed snaps and information we already have */ + local_snap = snapd_client_get_snap_sync (client, snap_name, cancellable, NULL); + store_snap = store_snap_cache_lookup (plugin, snap_name, FALSE); + if (store_snap != NULL) + store_channel = expand_channel_name (snapd_snap_get_channel (store_snap)); + + /* check if requested information requires us to go to the Snap Store */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS) + need_details = TRUE; + if (channel != NULL && g_strcmp0 (store_channel, channel) != 0) + need_details = TRUE; + if (need_details) { + g_clear_object (&store_snap); + store_snap = get_store_snap (plugin, snap_name, need_details, cancellable, NULL); + } + + /* we don't know anything about this snap */ + if (local_snap == NULL && store_snap == NULL) + return TRUE; + + if (local_snap != NULL) + tracking_channel = expand_channel_name (snapd_snap_get_tracking_channel (local_snap)); + + /* Get default channel to install */ + if (channel == NULL) { + if (local_snap != NULL) + channel = g_strdup (tracking_channel); + else + channel = expand_channel_name (snapd_snap_get_channel (store_snap)); + + gs_app_set_branch (app, channel); + } + + if (local_snap != NULL && g_strcmp0 (tracking_channel, channel) == 0) { + /* Do not set to installed state if app is updatable */ + if (gs_app_get_state (app) != AS_APP_STATE_UPDATABLE_LIVE) { + gs_app_set_state (app, AS_APP_STATE_INSTALLED); + } + } else + gs_app_set_state (app, AS_APP_STATE_AVAILABLE); + gs_app_add_quirk (app, GS_APP_QUIRK_DO_NOT_AUTO_UPDATE); + + /* use store information for basic metadata over local information */ + snap = store_snap != NULL ? store_snap : local_snap; + name = snapd_snap_get_title (snap); + if (name == NULL || g_strcmp0 (name, "") == 0) + name = snapd_snap_get_name (snap); + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, name); + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, snapd_snap_get_summary (snap)); + description = gs_plugin_snap_get_description_safe (snap); + if (description != NULL) + gs_app_set_description (app, GS_APP_QUALITY_NORMAL, description); + gs_app_set_license (app, GS_APP_QUALITY_NORMAL, snapd_snap_get_license (snap)); + developer_name = snapd_snap_get_publisher_display_name (snap); + if (developer_name == NULL) + developer_name = snapd_snap_get_publisher_username (snap); + gs_app_set_developer_name (app, developer_name); + if (snapd_snap_get_publisher_validation (snap) == SNAPD_PUBLISHER_VALIDATION_VERIFIED) + gs_app_add_quirk (app, GS_APP_QUIRK_DEVELOPER_VERIFIED); + + snap = local_snap != NULL ? local_snap : store_snap; + version = snapd_snap_get_version (snap); + confinement = snapd_snap_get_confinement (snap); + + if (channel != NULL && store_snap != NULL) { + GPtrArray *channels = snapd_snap_get_channels (store_snap); + guint i; + for (i = 0; i < channels->len; i++) { + SnapdChannel *c = channels->pdata[i]; + g_autofree gchar *expanded_name = NULL; + + expanded_name = expand_channel_name (snapd_channel_get_name (c)); + if (g_strcmp0 (expanded_name, channel) != 0) + continue; + + version = snapd_channel_get_version (c); + confinement = snapd_channel_get_confinement (c); + } + } + + gs_app_set_version (app, version); + + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS && + priv->system_confinement == SNAPD_SYSTEM_CONFINEMENT_STRICT && + confinement == SNAPD_CONFINEMENT_STRICT) + gs_app_add_kudo (app, GS_APP_KUDO_SANDBOXED); + + switch (snapd_snap_get_snap_type (snap)) { + case SNAPD_SNAP_TYPE_APP: + gs_app_set_kind (app, AS_APP_KIND_DESKTOP); + break; + case SNAPD_SNAP_TYPE_KERNEL: + case SNAPD_SNAP_TYPE_GADGET: + case SNAPD_SNAP_TYPE_OS: + gs_app_set_kind (app, AS_APP_KIND_RUNTIME); + break; + default: + case SNAPD_SNAP_TYPE_UNKNOWN: + gs_app_set_kind (app, AS_APP_KIND_UNKNOWN); + break; + } + + /* add information specific to installed snaps */ + if (local_snap != NULL) { + SnapdApp *snap_app; + GDateTime *install_date; + + install_date = snapd_snap_get_install_date (local_snap); + gs_app_set_size_installed (app, snapd_snap_get_installed_size (local_snap)); + gs_app_set_install_date (app, install_date != NULL ? g_date_time_to_unix (install_date) : GS_APP_INSTALL_DATE_UNKNOWN); + + snap_app = get_primary_app (local_snap); + if (snap_app != NULL) { + gs_app_set_metadata (app, "snap::launch-name", snapd_app_get_name (snap_app)); + gs_app_set_metadata (app, "snap::launch-desktop", snapd_app_get_desktop_file (snap_app)); + } else { + gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE); + } + } + + /* add information specific to store snaps */ + if (store_snap != NULL) { + gs_app_set_origin (app, priv->store_name); + gs_app_set_origin_hostname (app, priv->store_hostname); + gs_app_set_size_download (app, snapd_snap_get_download_size (store_snap)); + + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS && gs_app_get_screenshots (app)->len == 0) + refine_screenshots (app, store_snap); + } + + /* load icon if requested */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON && gs_app_get_pixbuf (app) == NULL) + load_icon (plugin, client, app, snap_name, local_snap, store_snap, cancellable); + + return TRUE; +} + +gboolean +gs_plugin_refine (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(SnapdClient) client = NULL; + + client = get_client (plugin, error); + if (client == NULL) + return FALSE; + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + if (!refine_app_with_client (plugin, client, app, flags, cancellable, error)) + return FALSE; + } + + return TRUE; +} + +static void +progress_cb (SnapdClient *client, SnapdChange *change, gpointer deprecated, gpointer user_data) +{ + GsApp *app = user_data; + GPtrArray *tasks; + guint i; + gint64 done = 0, total = 0; + + tasks = snapd_change_get_tasks (change); + for (i = 0; i < tasks->len; i++) { + SnapdTask *task = tasks->pdata[i]; + done += snapd_task_get_progress_done (task); + total += snapd_task_get_progress_total (task); + } + + gs_app_set_progress (app, (guint) (100 * done / total)); +} + +gboolean +gs_plugin_app_install (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(SnapdClient) client = NULL; + const gchar *name, *channel; + SnapdInstallFlags flags = SNAPD_INSTALL_FLAGS_NONE; + gboolean result; + g_autoptr(GError) error_local = NULL; + + /* We can only install apps we know of */ + if (g_strcmp0 (gs_app_get_management_plugin (app), "snap") != 0) + return TRUE; + + client = get_client (plugin, error); + if (client == NULL) + return FALSE; + + name = gs_app_get_metadata_item (app, "snap::name"); + channel = gs_app_get_branch (app); + + gs_app_set_state (app, AS_APP_STATE_INSTALLING); + + if (g_strcmp0 (gs_app_get_metadata_item (app, "snap::confinement"), "classic") == 0) + flags |= SNAPD_INSTALL_FLAGS_CLASSIC; + result = snapd_client_install2_sync (client, flags, name, channel, NULL, progress_cb, app, cancellable, &error_local); + + /* if already installed then just try to switch channel */ + if (!result && g_error_matches (error_local, SNAPD_ERROR, SNAPD_ERROR_ALREADY_INSTALLED)) { + g_clear_error (&error_local); + result = snapd_client_refresh_sync (client, name, channel, progress_cb, app, cancellable, &error_local); + } + + if (!result) { + gs_app_set_state_recover (app); + g_propagate_error (error, g_steal_pointer (&error_local)); + snapd_error_convert (error); + return FALSE; + } + + gs_app_set_state (app, AS_APP_STATE_INSTALLED); + + return TRUE; +} + +// Check if an app is graphical by checking if it uses a known GUI interface. +// This doesn't necessarily mean that every binary uses this interfaces, but is probably true. +// https://bugs.launchpad.net/bugs/1595023 +static gboolean +is_graphical (GsPlugin *plugin, GsApp *app, GCancellable *cancellable) +{ + g_autoptr(SnapdClient) client = NULL; + g_autoptr(GPtrArray) plugs = NULL; + guint i; + g_autoptr(GError) error = NULL; + + client = get_client (plugin, &error); + if (client == NULL) + return FALSE; + + if (!snapd_client_get_connections2_sync (client, + SNAPD_GET_CONNECTIONS_FLAGS_SELECT_ALL, NULL, NULL, + NULL, NULL, &plugs, NULL, + cancellable, &error)) { + g_warning ("Failed to get connections: %s", error->message); + return FALSE; + } + + for (i = 0; i < plugs->len; i++) { + SnapdPlug *plug = plugs->pdata[i]; + const gchar *interface; + + // Only looks at the plugs for this snap + if (g_strcmp0 (snapd_plug_get_snap (plug), gs_app_get_metadata_item (app, "snap::name")) != 0) + continue; + + interface = snapd_plug_get_interface (plug); + if (interface == NULL) + continue; + + if (g_strcmp0 (interface, "unity7") == 0 || g_strcmp0 (interface, "x11") == 0 || g_strcmp0 (interface, "mir") == 0) + return TRUE; + } + + return FALSE; +} + +gboolean +gs_plugin_launch (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + const gchar *launch_name; + const gchar *launch_desktop; + g_autoptr(GAppInfo) info = NULL; + + /* We can only launch apps we know of */ + if (g_strcmp0 (gs_app_get_management_plugin (app), "snap") != 0) + return TRUE; + + launch_name = gs_app_get_metadata_item (app, "snap::launch-name"); + launch_desktop = gs_app_get_metadata_item (app, "snap::launch-desktop"); + if (!launch_name) + return TRUE; + + if (launch_desktop) { + info = (GAppInfo *)g_desktop_app_info_new_from_filename (launch_desktop); + } else { + g_autofree gchar *commandline = NULL; + GAppInfoCreateFlags flags = G_APP_INFO_CREATE_NONE; + + if (g_strcmp0 (launch_name, gs_app_get_metadata_item (app, "snap::name")) == 0) + commandline = g_strdup_printf ("snap run %s", launch_name); + else + commandline = g_strdup_printf ("snap run %s.%s", gs_app_get_metadata_item (app, "snap::name"), launch_name); + + if (!is_graphical (plugin, app, cancellable)) + flags |= G_APP_INFO_CREATE_NEEDS_TERMINAL; + info = g_app_info_create_from_commandline (commandline, NULL, flags, error); + } + + if (info == NULL) + return FALSE; + + return g_app_info_launch (info, NULL, NULL, error); +} + +gboolean +gs_plugin_app_remove (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(SnapdClient) client = NULL; + + /* We can only remove apps we know of */ + if (g_strcmp0 (gs_app_get_management_plugin (app), "snap") != 0) + return TRUE; + + client = get_client (plugin, error); + if (client == NULL) + return FALSE; + + gs_app_set_state (app, AS_APP_STATE_REMOVING); + if (!snapd_client_remove2_sync (client, SNAPD_REMOVE_FLAGS_NONE, gs_app_get_metadata_item (app, "snap::name"), progress_cb, app, cancellable, error)) { + gs_app_set_state_recover (app); + snapd_error_convert (error); + return FALSE; + } + gs_app_set_state (app, AS_APP_STATE_AVAILABLE); + return TRUE; +} + +gboolean +gs_plugin_add_updates (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GPtrArray) apps = NULL; + g_autoptr(SnapdClient) client = NULL; + g_autoptr(GError) error_local = NULL; + + client = get_client (plugin, error); + if (client == NULL) + return FALSE; + + /* Get the list of refreshable snaps */ + apps = snapd_client_find_refreshable_sync (client, cancellable, &error_local); + if (apps == NULL) { + g_warning ("Failed to find refreshable snaps: %s", error_local->message); + return TRUE; + } + + for (guint i = 0; i < apps->len; i++) { + SnapdSnap *snap = g_ptr_array_index (apps, i); + g_autoptr(GsApp) app = NULL; + + /* Convert SnapdSnap to a GsApp */ + app = snap_to_app (plugin, snap); + + /* If for some reason the app is already getting updated, then + * don't change its state */ + if (gs_app_get_state (app) != AS_APP_STATE_INSTALLING) + gs_app_set_state (app, AS_APP_STATE_UPDATABLE_LIVE); + + /* Add GsApp to updatable GsAppList */ + gs_app_list_add (list, app); + } + + return TRUE; +} + +gboolean +gs_plugin_update (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(SnapdClient) client = NULL; + + client = get_client (plugin, error); + if (client == NULL) + return FALSE; + + for (guint i = 0; i < gs_app_list_length (list); i++) { + /* Get the name of the snap to refresh */ + GsApp *app = gs_app_list_index (list, i); + gchar *name = gs_app_get_metadata_item (app, "snap::name"); + + /* Refresh the snap */ + gs_app_set_state (app, AS_APP_STATE_INSTALLING); + + if (!snapd_client_refresh_sync (client, name, NULL, progress_cb, app, cancellable, error)) { + gs_app_set_state_recover (app); + snapd_error_convert (error); + return FALSE; + } + + gs_app_set_state (app, AS_APP_STATE_INSTALLED); + } + + return TRUE; +} diff --git a/plugins/snap/gs-self-test.c b/plugins/snap/gs-self-test.c new file mode 100644 index 0000000..f10de87 --- /dev/null +++ b/plugins/snap/gs-self-test.c @@ -0,0 +1,374 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2017 Canonical Ltd + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <snapd-glib/snapd-glib.h> + +#include "config.h" + +#include "gnome-software-private.h" + +#include "gs-test.h" + +static gboolean snap_installed = FALSE; + +SnapdAuthData * +snapd_login_sync (const gchar *username, const gchar *password, const gchar *otp, + GCancellable *cancellable, GError **error) +{ + return snapd_auth_data_new ("macaroon", NULL); +} + +SnapdClient * +snapd_client_new (void) +{ + /* use a dummy object - we intercept all snapd-glib calls */ + return g_object_new (G_TYPE_OBJECT, NULL); +} + +void +snapd_client_set_allow_interaction (SnapdClient *client, gboolean allow_interaction) +{ +} + +void +snapd_client_set_auth_data (SnapdClient *client, SnapdAuthData *auth_data) +{ +} + +gboolean +snapd_client_connect_sync (SnapdClient *client, GCancellable *cancellable, GError **error) +{ + /* skip connection */ + return TRUE; +} + +const gchar * +snapd_client_get_user_agent (SnapdClient *client) +{ + return "snapd-glib/0.0.1"; +} + +void +snapd_client_set_user_agent (SnapdClient *client, const gchar *user_agent) +{ +} + +SnapdSystemInformation * +snapd_client_get_system_information_sync (SnapdClient *client, GCancellable *cancellable, GError **error) +{ + g_autoptr(GHashTable) sandbox_features = g_hash_table_new (g_str_hash, g_str_equal); + return g_object_new (SNAPD_TYPE_SYSTEM_INFORMATION, + "version", "2.31", + "confinement", SNAPD_SYSTEM_CONFINEMENT_STRICT, + "sandbox-features", sandbox_features, + NULL); +} + +static SnapdSnap * +make_snap (const gchar *name, SnapdSnapStatus status) +{ + gchar *common_ids[] = { NULL }; + g_autoptr(GDateTime) install_date = NULL; + g_autoptr(GPtrArray) apps = NULL; + g_autoptr(GPtrArray) media = NULL; + SnapdMedia *m; + + install_date = g_date_time_new_utc (2017, 1, 2, 11, 23, 58); + + apps = g_ptr_array_new_with_free_func (g_object_unref); + + media = g_ptr_array_new_with_free_func (g_object_unref); + m = g_object_new (SNAPD_TYPE_MEDIA, + "type", "screenshot", + "url", "http://example.com/screenshot1.jpg", + "width", 640, + "height", 480, + NULL); + g_ptr_array_add (media, m); + m = g_object_new (SNAPD_TYPE_MEDIA, + "type", "screenshot", + "url", "http://example.com/screenshot2.jpg", + "width", 1024, + "height", 768, + NULL); + g_ptr_array_add (media, m); + + return g_object_new (SNAPD_TYPE_SNAP, + "apps", status == SNAPD_SNAP_STATUS_INSTALLED ? apps : NULL, + "common-ids", common_ids, + "description", "DESCRIPTION", + "download-size", status == SNAPD_SNAP_STATUS_AVAILABLE ? 500 : 0, + "icon", status == SNAPD_SNAP_STATUS_AVAILABLE ? NULL : "/icon", + "id", name, + "install-date", status == SNAPD_SNAP_STATUS_INSTALLED ? install_date : NULL, + "installed-size", status == SNAPD_SNAP_STATUS_INSTALLED ? 1000 : 0, + "media", status == SNAPD_SNAP_STATUS_AVAILABLE ? media : NULL, + "name", name, + "status", status, + "snap-type", SNAPD_SNAP_TYPE_APP, + "summary", "SUMMARY", + "version", "VERSION", + NULL); +} + +GPtrArray * +snapd_client_get_snaps_sync (SnapdClient *client, + SnapdGetSnapsFlags flags, gchar **names, + GCancellable *cancellable, GError **error) +{ + GPtrArray *snaps; + + snaps = g_ptr_array_new_with_free_func (g_object_unref); + if (snap_installed) + g_ptr_array_add (snaps, make_snap ("snap", SNAPD_SNAP_STATUS_INSTALLED)); + + return snaps; +} + +SnapdSnap * +snapd_client_get_snap_sync (SnapdClient *client, + const gchar *name, + GCancellable *cancellable, GError **error) +{ + if (snap_installed) { + return make_snap ("snap", SNAPD_SNAP_STATUS_INSTALLED); + } else { + g_set_error_literal (error, SNAPD_ERROR, SNAPD_ERROR_NOT_INSTALLED, "not installed"); + return NULL; + } +} + +SnapdIcon * +snapd_client_get_icon_sync (SnapdClient *client, + const gchar *name, + GCancellable *cancellable, GError **error) +{ + g_autoptr(GBytes) data = NULL; + /* apparently this is the smallest valid PNG file (1x1) */ + const gchar png_data[67] = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, + 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, + 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, + 0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, + 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00, + 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, + 0x42, 0x60, 0x82 }; + + data = g_bytes_new (png_data, 67); + return g_object_new (SNAPD_TYPE_ICON, + "mime-type", "image/png", + "data", data, + NULL); +} + +gboolean +snapd_client_get_connections_sync (SnapdClient *client, + GPtrArray **established, GPtrArray **undesired, + GPtrArray **plugs, GPtrArray **slots, + GCancellable *cancellable, GError **error) +{ + if (plugs) + *plugs = g_ptr_array_new_with_free_func (g_object_unref); + if (slots) + *slots = g_ptr_array_new_with_free_func (g_object_unref); + return TRUE; +} + +GPtrArray * +snapd_client_find_section_sync (SnapdClient *client, + SnapdFindFlags flags, + const gchar *section, const gchar *query, + gchar **suggested_currency, + GCancellable *cancellable, GError **error) +{ + GPtrArray *snaps; + + snaps = g_ptr_array_new_with_free_func (g_object_unref); + g_ptr_array_add (snaps, make_snap ("snap", SNAPD_SNAP_STATUS_AVAILABLE)); + + return snaps; +} + +gboolean +snapd_client_install2_sync (SnapdClient *client, + SnapdInstallFlags flags, + const gchar *name, const gchar *channel, const gchar *revision, + SnapdProgressCallback progress_callback, gpointer progress_callback_data, + GCancellable *cancellable, GError **error) +{ + g_autoptr(SnapdChange) change = NULL; + g_autoptr(GPtrArray) tasks = NULL; + SnapdTask *task; + + g_assert_cmpstr (name, ==, "snap"); + g_assert (channel == NULL); + + tasks = g_ptr_array_new_with_free_func (g_object_unref); + task = g_object_new (SNAPD_TYPE_TASK, + "progress-done", 0, + "progress-total", 1, + NULL); + g_ptr_array_add (tasks, task); + change = g_object_new (SNAPD_TYPE_CHANGE, + "tasks", tasks, + NULL); + progress_callback (client, change, NULL, progress_callback_data); + + snap_installed = TRUE; + return TRUE; +} + +gboolean +snapd_client_remove_sync (SnapdClient *client, + const gchar *name, + SnapdProgressCallback progress_callback, gpointer progress_callback_data, + GCancellable *cancellable, GError **error) +{ + g_autoptr(SnapdChange) change = NULL; + g_autoptr(GPtrArray) tasks = NULL; + SnapdTask *task; + + g_assert_cmpstr (name, ==, "snap"); + + tasks = g_ptr_array_new_with_free_func (g_object_unref); + task = g_object_new (SNAPD_TYPE_TASK, + "progress-done", 0, + "progress-total", 1, + NULL); + g_ptr_array_add (tasks, task); + change = g_object_new (SNAPD_TYPE_CHANGE, + "tasks", tasks, + NULL); + progress_callback (client, change, NULL, progress_callback_data); + + snap_installed = FALSE; + return TRUE; +} + +static void +gs_plugins_snap_test_func (GsPluginLoader *plugin_loader) +{ + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GsAppList) apps = NULL; + gboolean ret; + GsApp *app; + GPtrArray *screenshots, *images; + AsScreenshot *screenshot; + AsImage *image; + GdkPixbuf *pixbuf; + g_autoptr(GError) error = NULL; + + /* no snap, abort */ + if (!gs_plugin_loader_get_enabled (plugin_loader, "snap")) { + g_test_skip ("not enabled"); + return; + } + + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH, + "search", "snap", + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON | GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS, + NULL); + apps = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert (apps != NULL); + g_assert_cmpint (gs_app_list_length (apps), ==, 1); + app = gs_app_list_index (apps, 0); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_AVAILABLE); + g_assert_cmpstr (gs_app_get_name (app), ==, "snap"); + g_assert_cmpstr (gs_app_get_version (app), ==, "VERSION"); + g_assert_cmpstr (gs_app_get_summary (app), ==, "SUMMARY"); + g_assert_cmpstr (gs_app_get_description (app), ==, "DESCRIPTION"); + screenshots = gs_app_get_screenshots (app); + g_assert_cmpint (screenshots->len, ==, 2); + screenshot = g_ptr_array_index (screenshots, 0); + images = as_screenshot_get_images (screenshot); + g_assert_cmpint (images->len, ==, 1); + image = g_ptr_array_index (images, 0); + g_assert_cmpstr (as_image_get_url (image), ==, "http://example.com/screenshot1.jpg"); + g_assert_cmpint (as_image_get_width (image), ==, 640); + g_assert_cmpint (as_image_get_height (image), ==, 480); + screenshot = g_ptr_array_index (screenshots, 1); + images = as_screenshot_get_images (screenshot); + g_assert_cmpint (images->len, ==, 1); + image = g_ptr_array_index (images, 0); + g_assert_cmpstr (as_image_get_url (image), ==, "http://example.com/screenshot2.jpg"); + g_assert_cmpint (as_image_get_width (image), ==, 1024); + g_assert_cmpint (as_image_get_height (image), ==, 768); + pixbuf = gs_app_get_pixbuf (app); + g_assert_null (pixbuf); + g_assert_cmpint (gs_app_get_size_installed (app), ==, 0); + g_assert_cmpint (gs_app_get_size_download (app), ==, 500); + g_assert_cmpint (gs_app_get_install_date (app), ==, 0); + + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", app, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (ret); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_INSTALLED); + g_assert_cmpint (gs_app_get_size_installed (app), ==, 1000); + g_assert_cmpint (gs_app_get_install_date (app), ==, g_date_time_to_unix (g_date_time_new_utc (2017, 1, 2, 11, 23, 58))); + + pixbuf = gs_app_get_pixbuf (app); + g_assert_cmpint (gdk_pixbuf_get_width (pixbuf), ==, 64); + g_assert_cmpint (gdk_pixbuf_get_height (pixbuf), ==, 64); + + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", app, + NULL); + gs_test_flush_main_context (); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert (ret); +} + +int +main (int argc, char **argv) +{ + gboolean ret; + g_autoptr(GError) error = NULL; + g_autoptr(GsPluginLoader) plugin_loader = NULL; + const gchar *allowlist[] = { + "snap", + NULL + }; + + 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); + + /* 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); + gs_plugin_loader_add_location (plugin_loader, LOCALPLUGINDIR_CORE); + 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/snap/test", + plugin_loader, + (GTestDataFunc) gs_plugins_snap_test_func); + return g_test_run (); +} diff --git a/plugins/snap/meson.build b/plugins/snap/meson.build new file mode 100644 index 0000000..6afe62c --- /dev/null +++ b/plugins/snap/meson.build @@ -0,0 +1,54 @@ +cargs = ['-DG_LOG_DOMAIN="GsPluginSnap"'] + +shared_module( + 'gs_plugin_snap', + sources : [ + 'gs-plugin-snap.c' + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : [ plugin_libs, snap ], + link_with : [ + libgnomesoftware + ] +) +metainfo = 'org.gnome.Software.Plugin.Snap.metainfo.xml' + +i18n.merge_file( + input: metainfo + '.in', + output: metainfo, + type: 'xml', + po_dir: join_paths(meson.source_root(), 'po'), + install: true, + install_dir: join_paths(get_option('datadir'), 'metainfo') +) + +if get_option('tests') + cargs += ['-DLOCALPLUGINDIR="' + meson.current_build_dir() + '"'] + cargs += ['-DLOCALPLUGINDIR_CORE="' + meson.current_build_dir() + '/../core"'] + e = executable( + 'gs-self-test-snap', + compiled_schemas, + sources : [ + 'gs-self-test.c' + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + dependencies : [ + plugin_libs, + snap + ], + link_with : [ + libgnomesoftware + ], + c_args : cargs, + ) + test('gs-self-test-snap', e, suite: ['plugins', 'snap'], env: test_env) +endif diff --git a/plugins/snap/org.gnome.Software.Plugin.Snap.metainfo.xml.in b/plugins/snap/org.gnome.Software.Plugin.Snap.metainfo.xml.in new file mode 100644 index 0000000..2478778 --- /dev/null +++ b/plugins/snap/org.gnome.Software.Plugin.Snap.metainfo.xml.in @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright 2013-2016 Richard Hughes <richard@hughsie.com> --> +<component type="addon"> + <id>org.gnome.Software.Plugin.Snap</id> + <extends>org.gnome.Software.desktop</extends> + <name>Snap Support</name> + <summary>A snap is a universal Linux package</summary> + <url type="homepage">https://snapcraft.io/</url> + <metadata_license>CC0-1.0</metadata_license> + <project_license>GPL-2.0+</project_license> + <update_contact>richard_at_hughsie.com</update_contact> +</component> |