diff options
Diffstat (limited to 'plugins/flatpak/gs-flatpak.c')
-rw-r--r-- | plugins/flatpak/gs-flatpak.c | 3258 |
1 files changed, 3258 insertions, 0 deletions
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); +} |