/* -*- 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 * Copyright (C) 2016-2018 Richard Hughes * Copyright (C) 2017-2020 Kalev Lember * * SPDX-License-Identifier: GPL-2.0+ */ /* Notes: * * All GsApp's created have management-plugin set to flatpak * Some GsApp's created have have flatpak::kind of app or runtime * The GsApp:origin is the remote name, e.g. test-repo */ #include #include #include #include "gs-appstream.h" #include "gs-flatpak-app.h" #include "gs-flatpak.h" #include "gs-flatpak-transaction.h" #include "gs-flatpak-utils.h" #include "gs-metered.h" struct GsPluginData { GPtrArray *flatpaks; /* of GsFlatpak */ gboolean has_system_helper; const gchar *destdir_for_tests; }; void gs_plugin_initialize (GsPlugin *plugin) { GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData)); const gchar *action_id = "org.freedesktop.Flatpak.appstream-update"; g_autoptr(GError) error_local = NULL; g_autoptr(GPermission) permission = NULL; priv->flatpaks = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); /* getting app properties from appstream is quicker */ gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "appstream"); /* like appstream, we need the icon plugin to load cached icons into pixbufs */ gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_BEFORE, "icons"); /* prioritize over packages */ gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_BETTER_THAN, "packagekit"); /* set name of MetaInfo file */ gs_plugin_set_appstream_id (plugin, "org.gnome.Software.Plugin.Flatpak"); /* if we can't update the AppStream database system-wide don't even * pull the data as we can't do anything with it */ permission = gs_utils_get_permission (action_id, NULL, &error_local); if (permission == NULL) { g_debug ("no permission for %s: %s", action_id, error_local->message); g_clear_error (&error_local); } else { priv->has_system_helper = g_permission_get_allowed (permission) || g_permission_get_can_acquire (permission); } /* used for self tests */ priv->destdir_for_tests = g_getenv ("GS_SELF_TEST_FLATPAK_DATADIR"); } static gboolean _as_app_scope_is_compatible (AsAppScope scope1, AsAppScope scope2) { if (scope1 == AS_APP_SCOPE_UNKNOWN) return TRUE; if (scope2 == AS_APP_SCOPE_UNKNOWN) return TRUE; return scope1 == scope2; } void gs_plugin_destroy (GsPlugin *plugin) { GsPluginData *priv = gs_plugin_get_data (plugin); g_ptr_array_unref (priv->flatpaks); } void gs_plugin_adopt_app (GsPlugin *plugin, GsApp *app) { if (gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_FLATPAK) gs_app_set_management_plugin (app, gs_plugin_get_name (plugin)); } static gboolean gs_plugin_flatpak_add_installation (GsPlugin *plugin, FlatpakInstallation *installation, GCancellable *cancellable, GError **error) { GsPluginData *priv = gs_plugin_get_data (plugin); g_autoptr(GsFlatpak) flatpak = NULL; /* create and set up */ flatpak = gs_flatpak_new (plugin, installation, GS_FLATPAK_FLAG_NONE); if (!gs_flatpak_setup (flatpak, cancellable, error)) return FALSE; g_debug ("successfully set up %s", gs_flatpak_get_id (flatpak)); /* add objects that set up correctly */ g_ptr_array_add (priv->flatpaks, g_steal_pointer (&flatpak)); return TRUE; } gboolean gs_plugin_setup (GsPlugin *plugin, GCancellable *cancellable, GError **error) { GsPluginData *priv = gs_plugin_get_data (plugin); /* clear in case we're called from resetup in the self tests */ g_ptr_array_set_size (priv->flatpaks, 0); /* we use a permissions helper to elevate privs */ if (priv->has_system_helper && priv->destdir_for_tests == NULL) { g_autoptr(GPtrArray) installations = NULL; installations = flatpak_get_system_installations (cancellable, error); if (installations == NULL) { gs_flatpak_error_convert (error); return FALSE; } for (guint i = 0; i < installations->len; i++) { FlatpakInstallation *installation = g_ptr_array_index (installations, i); if (!gs_plugin_flatpak_add_installation (plugin, installation, cancellable, error)) { return FALSE; } } } /* in gs-self-test */ if (priv->destdir_for_tests != NULL) { g_autofree gchar *full_path = g_build_filename (priv->destdir_for_tests, "flatpak", NULL); g_autoptr(GFile) file = g_file_new_for_path (full_path); g_autoptr(FlatpakInstallation) installation = NULL; g_debug ("using custom flatpak path %s", full_path); installation = flatpak_installation_new_for_path (file, TRUE, cancellable, error); if (installation == NULL) { gs_flatpak_error_convert (error); return FALSE; } if (!gs_plugin_flatpak_add_installation (plugin, installation, cancellable, error)) { return FALSE; } } /* per-user installations always available when not in self tests */ if (priv->destdir_for_tests == NULL) { g_autoptr(FlatpakInstallation) installation = NULL; installation = flatpak_installation_new_user (cancellable, error); if (installation == NULL) { gs_flatpak_error_convert (error); return FALSE; } if (!gs_plugin_flatpak_add_installation (plugin, installation, cancellable, error)) { return FALSE; } } return TRUE; } gboolean gs_plugin_add_installed (GsPlugin *plugin, GsAppList *list, GCancellable *cancellable, GError **error) { GsPluginData *priv = gs_plugin_get_data (plugin); for (guint i = 0; i < priv->flatpaks->len; i++) { GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i); if (!gs_flatpak_add_installed (flatpak, list, cancellable, error)) return FALSE; } return TRUE; } gboolean gs_plugin_add_sources (GsPlugin *plugin, GsAppList *list, GCancellable *cancellable, GError **error) { GsPluginData *priv = gs_plugin_get_data (plugin); for (guint i = 0; i < priv->flatpaks->len; i++) { GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i); if (!gs_flatpak_add_sources (flatpak, list, cancellable, error)) return FALSE; } return TRUE; } gboolean gs_plugin_add_updates (GsPlugin *plugin, GsAppList *list, GCancellable *cancellable, GError **error) { GsPluginData *priv = gs_plugin_get_data (plugin); for (guint i = 0; i < priv->flatpaks->len; i++) { GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i); if (!gs_flatpak_add_updates (flatpak, list, cancellable, error)) return FALSE; } return TRUE; } gboolean gs_plugin_refresh (GsPlugin *plugin, guint cache_age, GCancellable *cancellable, GError **error) { GsPluginData *priv = gs_plugin_get_data (plugin); for (guint i = 0; i < priv->flatpaks->len; i++) { GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i); if (!gs_flatpak_refresh (flatpak, cache_age, cancellable, error)) return FALSE; } return TRUE; } static GsFlatpak * gs_plugin_flatpak_get_handler (GsPlugin *plugin, GsApp *app) { GsPluginData *priv = gs_plugin_get_data (plugin); const gchar *object_id; /* only process this app if was created by this plugin */ if (g_strcmp0 (gs_app_get_management_plugin (app), gs_plugin_get_name (plugin)) != 0) { return NULL; } /* specified an explicit name */ object_id = gs_flatpak_app_get_object_id (app); if (object_id != NULL) { for (guint i = 0; i < priv->flatpaks->len; i++) { GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i); if (g_strcmp0 (gs_flatpak_get_id (flatpak), object_id) == 0) return flatpak; } } /* find a scope that matches */ for (guint i = 0; i < priv->flatpaks->len; i++) { GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i); if (_as_app_scope_is_compatible (gs_flatpak_get_scope (flatpak), gs_app_get_scope (app))) return flatpak; } return NULL; } static gboolean gs_plugin_flatpak_refine_app (GsPlugin *plugin, GsApp *app, GsPluginRefineFlags flags, GCancellable *cancellable, GError **error) { GsPluginData *priv = gs_plugin_get_data (plugin); GsFlatpak *flatpak = NULL; /* not us */ if (gs_app_get_bundle_kind (app) != AS_BUNDLE_KIND_FLATPAK) { g_debug ("%s not a package, ignoring", gs_app_get_unique_id (app)); return TRUE; } /* we have to look for the app in all GsFlatpak stores */ if (gs_app_get_scope (app) == AS_APP_SCOPE_UNKNOWN) { for (guint i = 0; i < priv->flatpaks->len; i++) { GsFlatpak *flatpak_tmp = g_ptr_array_index (priv->flatpaks, i); g_autoptr(GError) error_local = NULL; if (gs_flatpak_refine_app_state (flatpak_tmp, app, cancellable, &error_local)) { flatpak = flatpak_tmp; break; } else { g_debug ("%s", error_local->message); } } } else { flatpak = gs_plugin_flatpak_get_handler (plugin, app); } if (flatpak == NULL) return TRUE; return gs_flatpak_refine_app (flatpak, app, flags, cancellable, error); } static gboolean refine_app (GsPlugin *plugin, GsApp *app, GsPluginRefineFlags flags, GCancellable *cancellable, GError **error) { /* only process this app if was created by this plugin */ if (g_strcmp0 (gs_app_get_management_plugin (app), gs_plugin_get_name (plugin)) != 0) { return TRUE; } /* get the runtime first */ if (!gs_plugin_flatpak_refine_app (plugin, app, flags, cancellable, error)) return FALSE; /* the runtime might be installed in a different scope */ if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME) { GsApp *runtime = gs_app_get_runtime (app); if (runtime != NULL) { if (!gs_plugin_flatpak_refine_app (plugin, app, flags, cancellable, error)) { return FALSE; } } } return TRUE; } gboolean gs_plugin_refine (GsPlugin *plugin, GsAppList *list, GsPluginRefineFlags flags, GCancellable *cancellable, GError **error) { for (guint i = 0; i < gs_app_list_length (list); i++) { GsApp *app = gs_app_list_index (list, i); if (!refine_app (plugin, app, flags, cancellable, error)) return FALSE; } return TRUE; } gboolean gs_plugin_refine_wildcard (GsPlugin *plugin, GsApp *app, GsAppList *list, GsPluginRefineFlags flags, GCancellable *cancellable, GError **error) { GsPluginData *priv = gs_plugin_get_data (plugin); for (guint i = 0; i < priv->flatpaks->len; i++) { GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i); if (!gs_flatpak_refine_wildcard (flatpak, app, list, flags, cancellable, error)) { return FALSE; } } return TRUE; } gboolean gs_plugin_launch (GsPlugin *plugin, GsApp *app, GCancellable *cancellable, GError **error) { GsFlatpak *flatpak = gs_plugin_flatpak_get_handler (plugin, app); if (flatpak == NULL) return TRUE; return gs_flatpak_launch (flatpak, app, cancellable, error); } /* ref full */ static GsApp * gs_plugin_flatpak_find_app_by_ref (GsPlugin *plugin, const gchar *ref, GCancellable *cancellable, GError **error) { GsPluginData *priv = gs_plugin_get_data (plugin); g_debug ("finding ref %s", ref); for (guint i = 0; i < priv->flatpaks->len; i++) { GsFlatpak *flatpak_tmp = g_ptr_array_index (priv->flatpaks, i); g_autoptr(GsApp) app = NULL; g_autoptr(GError) error_local = NULL; app = gs_flatpak_ref_to_app (flatpak_tmp, ref, cancellable, &error_local); if (app == NULL) { g_debug ("%s", error_local->message); continue; } g_debug ("found ref=%s->%s", ref, gs_app_get_unique_id (app)); return g_steal_pointer (&app); } return NULL; } /* ref full */ static GsApp * _ref_to_app (FlatpakTransaction *transaction, const gchar *ref, GsPlugin *plugin) { g_return_val_if_fail (GS_IS_FLATPAK_TRANSACTION (transaction), NULL); g_return_val_if_fail (ref != NULL, NULL); g_return_val_if_fail (GS_IS_PLUGIN (plugin), NULL); /* search through each GsFlatpak */ return gs_plugin_flatpak_find_app_by_ref (plugin, ref, NULL, NULL); } /* * Returns: (transfer full) (element-type GsFlatpak GsAppList): * a map from GsFlatpak to non-empty lists of apps from @list associated * with that installation. */ static GHashTable * _group_apps_by_installation (GsPlugin *plugin, GsAppList *list) { g_autoptr(GHashTable) applist_by_flatpaks = NULL; /* list of apps to be handled by each flatpak installation */ applist_by_flatpaks = g_hash_table_new_full (g_direct_hash, g_direct_equal, (GDestroyNotify) g_object_unref, (GDestroyNotify) g_object_unref); /* put each app into the correct per-GsFlatpak list */ for (guint i = 0; i < gs_app_list_length (list); i++) { GsApp *app = gs_app_list_index (list, i); GsFlatpak *flatpak = gs_plugin_flatpak_get_handler (plugin, app); if (flatpak != NULL) { GsAppList *list_tmp = g_hash_table_lookup (applist_by_flatpaks, flatpak); if (list_tmp == NULL) { list_tmp = gs_app_list_new (); g_hash_table_insert (applist_by_flatpaks, g_object_ref (flatpak), list_tmp); } gs_app_list_add (list_tmp, app); } } return g_steal_pointer (&applist_by_flatpaks); } #if FLATPAK_CHECK_VERSION(1,6,0) typedef struct { FlatpakTransaction *transaction; guint id; } BasicAuthData; static void basic_auth_data_free (BasicAuthData *data) { g_object_unref (data->transaction); g_slice_free (BasicAuthData, data); } G_DEFINE_AUTOPTR_CLEANUP_FUNC(BasicAuthData, basic_auth_data_free) static void _basic_auth_cb (const gchar *user, const gchar *password, gpointer user_data) { g_autoptr(BasicAuthData) data = user_data; g_debug ("Submitting basic auth data"); /* NULL user aborts the basic auth request */ flatpak_transaction_complete_basic_auth (data->transaction, data->id, user, password, NULL /* options */); } static gboolean _basic_auth_start (FlatpakTransaction *transaction, const char *remote, const char *realm, GVariant *options, guint id, GsPlugin *plugin) { BasicAuthData *data; if (!gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)) return FALSE; data = g_slice_new0 (BasicAuthData); data->transaction = g_object_ref (transaction); data->id = id; g_debug ("Login required remote %s (realm %s)\n", remote, realm); gs_plugin_basic_auth_start (plugin, remote, realm, G_CALLBACK (_basic_auth_cb), data); return TRUE; } static gboolean _webflow_start (FlatpakTransaction *transaction, const char *remote, const char *url, GVariant *options, guint id, GsPlugin *plugin) { const char *browser; g_autoptr(GError) error_local = NULL; if (!gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)) return FALSE; g_debug ("Authentication required for remote '%s'", remote); /* Allow hard overrides with $BROWSER */ browser = g_getenv ("BROWSER"); if (browser != NULL) { const char *args[3] = { NULL, url, NULL }; args[0] = browser; if (!g_spawn_async (NULL, (char **)args, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, NULL, &error_local)) { g_autoptr(GsPluginEvent) event = NULL; g_warning ("Failed to start browser %s: %s", browser, error_local->message); event = gs_plugin_event_new (); gs_flatpak_error_convert (&error_local); gs_plugin_event_set_error (event, error_local); gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING); gs_plugin_report_event (plugin, event); return FALSE; } } else { if (!g_app_info_launch_default_for_uri (url, NULL, &error_local)) { g_autoptr(GsPluginEvent) event = NULL; g_warning ("Failed to show url: %s", error_local->message); event = gs_plugin_event_new (); gs_flatpak_error_convert (&error_local); gs_plugin_event_set_error (event, error_local); gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING); gs_plugin_report_event (plugin, event); return FALSE; } } g_debug ("Waiting for browser..."); return TRUE; } static void _webflow_done (FlatpakTransaction *transaction, GVariant *options, guint id, GsPlugin *plugin) { g_debug ("Browser done"); } #endif static FlatpakTransaction * _build_transaction (GsPlugin *plugin, GsFlatpak *flatpak, GCancellable *cancellable, GError **error) { FlatpakInstallation *installation; #if !FLATPAK_CHECK_VERSION(1, 7, 3) g_autoptr(GFile) installation_path = NULL; #endif /* flatpak < 1.7.3 */ g_autoptr(FlatpakInstallation) installation_clone = NULL; g_autoptr(FlatpakTransaction) transaction = NULL; installation = gs_flatpak_get_installation (flatpak); #if !FLATPAK_CHECK_VERSION(1, 7, 3) /* Operate on a copy of the installation so we can set the interactive * flag for the duration of this transaction. */ installation_path = flatpak_installation_get_path (installation); installation_clone = flatpak_installation_new_for_path (installation_path, flatpak_installation_get_is_user (installation), cancellable, error); if (installation_clone == NULL) return NULL; /* Let flatpak know if it is a background operation */ flatpak_installation_set_no_interaction (installation_clone, !gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)); #else /* if flatpak ≥ 1.7.3 */ installation_clone = g_object_ref (installation); #endif /* flatpak ≥ 1.7.3 */ /* create transaction */ transaction = gs_flatpak_transaction_new (installation_clone, cancellable, error); if (transaction == NULL) { g_prefix_error (error, "failed to build transaction: "); gs_flatpak_error_convert (error); return NULL; } #if FLATPAK_CHECK_VERSION(1, 7, 3) /* Let flatpak know if it is a background operation */ flatpak_transaction_set_no_interaction (transaction, !gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)); #endif /* flatpak ≥ 1.7.3 */ /* connect up signals */ g_signal_connect (transaction, "ref-to-app", G_CALLBACK (_ref_to_app), plugin); #if FLATPAK_CHECK_VERSION(1,6,0) g_signal_connect (transaction, "basic-auth-start", G_CALLBACK (_basic_auth_start), plugin); g_signal_connect (transaction, "webflow-start", G_CALLBACK (_webflow_start), plugin); g_signal_connect (transaction, "webflow-done", G_CALLBACK (_webflow_done), plugin); #endif /* use system installations as dependency sources for user installations */ flatpak_transaction_add_default_dependency_sources (transaction); return g_steal_pointer (&transaction); } gboolean gs_plugin_download (GsPlugin *plugin, GsAppList *list, GCancellable *cancellable, GError **error) { g_autoptr(GHashTable) applist_by_flatpaks = NULL; GHashTableIter iter; gpointer key, value; /* build and run transaction for each flatpak installation */ applist_by_flatpaks = _group_apps_by_installation (plugin, list); g_hash_table_iter_init (&iter, applist_by_flatpaks); while (g_hash_table_iter_next (&iter, &key, &value)) { GsFlatpak *flatpak = GS_FLATPAK (key); GsAppList *list_tmp = GS_APP_LIST (value); g_autoptr(FlatpakTransaction) transaction = NULL; g_assert (GS_IS_FLATPAK (flatpak)); g_assert (list_tmp != NULL); g_assert (gs_app_list_length (list_tmp) > 0); if (!gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)) { g_autoptr(GError) error_local = NULL; if (!gs_metered_block_app_list_on_download_scheduler (list_tmp, cancellable, &error_local)) { g_warning ("Failed to block on download scheduler: %s", error_local->message); g_clear_error (&error_local); } } /* build and run non-deployed transaction */ transaction = _build_transaction (plugin, flatpak, cancellable, error); if (transaction == NULL) { gs_flatpak_error_convert (error); return FALSE; } #if !FLATPAK_CHECK_VERSION(1,5,1) gs_flatpak_transaction_set_no_deploy (transaction, TRUE); #else flatpak_transaction_set_no_deploy (transaction, TRUE); #endif for (guint i = 0; i < gs_app_list_length (list_tmp); i++) { GsApp *app = gs_app_list_index (list_tmp, i); g_autofree gchar *ref = NULL; ref = gs_flatpak_app_get_ref_display (app); if (!flatpak_transaction_add_update (transaction, ref, NULL, NULL, error)) { gs_flatpak_error_convert (error); return FALSE; } } if (!gs_flatpak_transaction_run (transaction, cancellable, error)) { gs_flatpak_error_convert (error); return FALSE; } /* Traverse over the GsAppList again and set that the update has been already downloaded * for the apps. */ for (guint i = 0; i < gs_app_list_length (list_tmp); i++) { GsApp *app = gs_app_list_index (list_tmp, i); gs_app_set_is_update_downloaded (app, TRUE); } } return TRUE; } gboolean gs_plugin_app_remove (GsPlugin *plugin, GsApp *app, GCancellable *cancellable, GError **error) { GsFlatpak *flatpak; g_autoptr(FlatpakTransaction) transaction = NULL; g_autofree gchar *ref = NULL; /* not supported */ flatpak = gs_plugin_flatpak_get_handler (plugin, app); if (flatpak == NULL) return TRUE; /* is a source */ if (gs_app_get_kind (app) == AS_APP_KIND_SOURCE) return gs_flatpak_app_remove_source (flatpak, app, cancellable, error); /* build and run transaction */ transaction = _build_transaction (plugin, flatpak, cancellable, error); if (transaction == NULL) { gs_flatpak_error_convert (error); return FALSE; } /* add to the transaction cache for quick look up -- other unrelated * refs will be matched using gs_plugin_flatpak_find_app_by_ref() */ gs_flatpak_transaction_add_app (transaction, app); ref = gs_flatpak_app_get_ref_display (app); if (!flatpak_transaction_add_uninstall (transaction, ref, error)) { gs_flatpak_error_convert (error); return FALSE; } /* run transaction */ gs_app_set_state (app, AS_APP_STATE_REMOVING); if (!gs_flatpak_transaction_run (transaction, cancellable, error)) { gs_flatpak_error_convert (error); gs_app_set_state_recover (app); return FALSE; } /* get any new state */ if (!gs_flatpak_refresh (flatpak, G_MAXUINT, cancellable, error)) { gs_flatpak_error_convert (error); return FALSE; } if (!gs_flatpak_refine_app (flatpak, app, GS_PLUGIN_REFINE_FLAGS_DEFAULT, cancellable, error)) { g_prefix_error (error, "failed to run refine for %s: ", ref); gs_flatpak_error_convert (error); return FALSE; } return TRUE; } static gboolean app_has_local_source (GsApp *app) { const gchar *url = gs_app_get_origin_hostname (app); return url != NULL && g_str_has_prefix (url, "file://"); } gboolean gs_plugin_app_install (GsPlugin *plugin, GsApp *app, GCancellable *cancellable, GError **error) { GsPluginData *priv = gs_plugin_get_data (plugin); GsFlatpak *flatpak; g_autoptr(FlatpakTransaction) transaction = NULL; /* queue for install if installation needs the network */ if (!app_has_local_source (app) && !gs_plugin_get_network_available (plugin)) { gs_app_set_state (app, AS_APP_STATE_QUEUED_FOR_INSTALL); return TRUE; } /* set the app scope */ if (gs_app_get_scope (app) == AS_APP_SCOPE_UNKNOWN) { g_autoptr(GSettings) settings = g_settings_new ("org.gnome.software"); /* get the new GsFlatpak for handling of local files */ gs_app_set_scope (app, g_settings_get_boolean (settings, "install-bundles-system-wide") ? AS_APP_SCOPE_SYSTEM : AS_APP_SCOPE_USER); if (!priv->has_system_helper) { g_info ("no flatpak system helper is available, using user"); gs_app_set_scope (app, AS_APP_SCOPE_USER); } if (priv->destdir_for_tests != NULL) { g_debug ("in self tests, using user"); gs_app_set_scope (app, AS_APP_SCOPE_USER); } } /* not supported */ flatpak = gs_plugin_flatpak_get_handler (plugin, app); if (flatpak == NULL) return TRUE; /* is a source */ if (gs_app_get_kind (app) == AS_APP_KIND_SOURCE) return gs_flatpak_app_install_source (flatpak, app, cancellable, error); if (!gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)) { g_autoptr(GError) error_local = NULL; /* FIXME: Add additional details here, especially the download * size bounds (using `size-minimum` and `size-maximum`, both * type `t`). */ if (!gs_metered_block_app_on_download_scheduler (app, cancellable, &error_local)) { g_warning ("Failed to block on download scheduler: %s", error_local->message); g_clear_error (&error_local); } } /* build */ transaction = _build_transaction (plugin, flatpak, cancellable, error); if (transaction == NULL) { gs_flatpak_error_convert (error); return FALSE; } /* add to the transaction cache for quick look up -- other unrelated * refs will be matched using gs_plugin_flatpak_find_app_by_ref() */ gs_flatpak_transaction_add_app (transaction, app); /* add flatpakref */ if (gs_flatpak_app_get_file_kind (app) == GS_FLATPAK_APP_FILE_KIND_REF) { GFile *file = gs_app_get_local_file (app); g_autoptr(GBytes) blob = NULL; if (file == NULL) { g_set_error (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NOT_SUPPORTED, "no local file set for bundle %s", gs_app_get_unique_id (app)); return FALSE; } blob = g_file_load_bytes (file, cancellable, NULL, error); if (blob == NULL) { gs_flatpak_error_convert (error); return FALSE; } if (!flatpak_transaction_add_install_flatpakref (transaction, blob, error)) { gs_flatpak_error_convert (error); return FALSE; } /* add bundle */ } else if (gs_flatpak_app_get_file_kind (app) == GS_FLATPAK_APP_FILE_KIND_BUNDLE) { GFile *file = gs_app_get_local_file (app); if (file == NULL) { g_set_error (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NOT_SUPPORTED, "no local file set for bundle %s", gs_app_get_unique_id (app)); return FALSE; } if (!flatpak_transaction_add_install_bundle (transaction, file, NULL, error)) { gs_flatpak_error_convert (error); return FALSE; } /* add normal ref */ } else { g_autofree gchar *ref = gs_flatpak_app_get_ref_display (app); if (!flatpak_transaction_add_install (transaction, gs_app_get_origin (app), ref, NULL, error)) { gs_flatpak_error_convert (error); return FALSE; } } /* run transaction */ gs_app_set_state (app, AS_APP_STATE_INSTALLING); if (!gs_flatpak_transaction_run (transaction, cancellable, error)) { gs_flatpak_error_convert (error); gs_app_set_state_recover (app); return FALSE; } /* get any new state */ if (!gs_flatpak_refresh (flatpak, G_MAXUINT, cancellable, error)) { gs_flatpak_error_convert (error); return FALSE; } if (!gs_flatpak_refine_app (flatpak, app, GS_PLUGIN_REFINE_FLAGS_DEFAULT, cancellable, error)) { g_prefix_error (error, "failed to run refine for %s: ", gs_app_get_unique_id (app)); gs_flatpak_error_convert (error); return FALSE; } return TRUE; } static gboolean gs_plugin_flatpak_update (GsPlugin *plugin, GsFlatpak *flatpak, GsAppList *list_tmp, GCancellable *cancellable, GError **error) { g_autoptr(FlatpakTransaction) transaction = NULL; gboolean is_update_downloaded = TRUE; /* build and run transaction */ transaction = _build_transaction (plugin, flatpak, cancellable, error); if (transaction == NULL) { gs_flatpak_error_convert (error); return FALSE; } for (guint i = 0; i < gs_app_list_length (list_tmp); i++) { GsApp *app = gs_app_list_index (list_tmp, i); g_autofree gchar *ref = NULL; ref = gs_flatpak_app_get_ref_display (app); if (!flatpak_transaction_add_update (transaction, ref, NULL, NULL, error)) { gs_flatpak_error_convert (error); return FALSE; } /* add to the transaction cache for quick look up -- other unrelated * refs will be matched using gs_plugin_flatpak_find_app_by_ref() */ gs_flatpak_transaction_add_app (transaction, app); } /* run transaction */ for (guint i = 0; i < gs_app_list_length (list_tmp); i++) { GsApp *app = gs_app_list_index (list_tmp, i); gs_app_set_state (app, AS_APP_STATE_INSTALLING); /* If all apps' update are previously downloaded and available locally, * FlatpakTransaction should run with no-pull flag. This is the case * for apps' autoupdates. */ is_update_downloaded &= gs_app_get_is_update_downloaded (app); } if (is_update_downloaded) flatpak_transaction_set_no_pull (transaction, TRUE); if (!gs_flatpak_transaction_run (transaction, cancellable, error)) { for (guint i = 0; i < gs_app_list_length (list_tmp); i++) { GsApp *app = gs_app_list_index (list_tmp, i); gs_app_set_state_recover (app); } gs_flatpak_error_convert (error); return FALSE; } gs_plugin_updates_changed (plugin); /* get any new state */ if (!gs_flatpak_refresh (flatpak, G_MAXUINT, cancellable, error)) { gs_flatpak_error_convert (error); return FALSE; } for (guint i = 0; i < gs_app_list_length (list_tmp); i++) { GsApp *app = gs_app_list_index (list_tmp, i); g_autofree gchar *ref = NULL; ref = gs_flatpak_app_get_ref_display (app); if (!gs_flatpak_refine_app (flatpak, app, GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME, cancellable, error)) { g_prefix_error (error, "failed to run refine for %s: ", ref); gs_flatpak_error_convert (error); return FALSE; } } return TRUE; } gboolean gs_plugin_update (GsPlugin *plugin, GsAppList *list, GCancellable *cancellable, GError **error) { g_autoptr(GHashTable) applist_by_flatpaks = NULL; GHashTableIter iter; gpointer key, value; /* build and run transaction for each flatpak installation */ applist_by_flatpaks = _group_apps_by_installation (plugin, list); g_hash_table_iter_init (&iter, applist_by_flatpaks); while (g_hash_table_iter_next (&iter, &key, &value)) { GsFlatpak *flatpak = GS_FLATPAK (key); GsAppList *list_tmp = GS_APP_LIST (value); g_assert (GS_IS_FLATPAK (flatpak)); g_assert (list_tmp != NULL); g_assert (gs_app_list_length (list_tmp) > 0); if (!gs_plugin_flatpak_update (plugin, flatpak, list_tmp, cancellable, error)) return FALSE; } return TRUE; } static GsApp * gs_plugin_flatpak_file_to_app_repo (GsPlugin *plugin, GFile *file, GCancellable *cancellable, GError **error) { GsPluginData *priv = gs_plugin_get_data (plugin); g_autoptr(GsApp) app = NULL; /* parse the repo file */ app = gs_flatpak_app_new_from_repo_file (file, cancellable, error); if (app == NULL) return NULL; /* already exists */ for (guint i = 0; i < priv->flatpaks->len; i++) { GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i); g_autoptr(GError) error_local = NULL; g_autoptr(GsApp) app_tmp = NULL; app_tmp = gs_flatpak_find_source_by_url (flatpak, gs_flatpak_app_get_repo_url (app), cancellable, &error_local); if (app_tmp == NULL) { g_debug ("%s", error_local->message); continue; } return g_steal_pointer (&app_tmp); } /* this is new */ gs_app_set_management_plugin (app, gs_plugin_get_name (plugin)); return g_steal_pointer (&app); } static GsFlatpak * gs_plugin_flatpak_create_temporary (GsPlugin *plugin, GCancellable *cancellable, GError **error) { g_autofree gchar *installation_path = NULL; g_autoptr(FlatpakInstallation) installation = NULL; g_autoptr(GFile) installation_file = NULL; /* create new per-user installation in a cache dir */ installation_path = gs_utils_get_cache_filename ("flatpak", "installation-tmp", GS_UTILS_CACHE_FLAG_WRITEABLE | GS_UTILS_CACHE_FLAG_ENSURE_EMPTY, error); if (installation_path == NULL) return NULL; installation_file = g_file_new_for_path (installation_path); installation = flatpak_installation_new_for_path (installation_file, TRUE, /* user */ cancellable, error); if (installation == NULL) { gs_flatpak_error_convert (error); return NULL; } return gs_flatpak_new (plugin, installation, GS_FLATPAK_FLAG_IS_TEMPORARY); } static GsApp * gs_plugin_flatpak_file_to_app_bundle (GsPlugin *plugin, GFile *file, GCancellable *cancellable, GError **error) { g_autofree gchar *ref = NULL; g_autoptr(GsApp) app = NULL; g_autoptr(GsApp) app_tmp = NULL; g_autoptr(GsFlatpak) flatpak_tmp = NULL; /* only use the temporary GsFlatpak to avoid the auth dialog */ flatpak_tmp = gs_plugin_flatpak_create_temporary (plugin, cancellable, error); if (flatpak_tmp == NULL) return NULL; /* add object */ app = gs_flatpak_file_to_app_bundle (flatpak_tmp, file, cancellable, error); if (app == NULL) return NULL; /* is this already installed or available in a configured remote */ ref = gs_flatpak_app_get_ref_display (app); app_tmp = gs_plugin_flatpak_find_app_by_ref (plugin, ref, cancellable, NULL); if (app_tmp != NULL) return g_steal_pointer (&app_tmp); /* force this to be 'any' scope for installation */ gs_app_set_scope (app, AS_APP_SCOPE_UNKNOWN); /* this is new */ return g_steal_pointer (&app); } static GsApp * gs_plugin_flatpak_file_to_app_ref (GsPlugin *plugin, GFile *file, GCancellable *cancellable, GError **error) { GsApp *runtime; g_autofree gchar *ref = NULL; g_autoptr(GsApp) app = NULL; g_autoptr(GsApp) app_tmp = NULL; g_autoptr(GsFlatpak) flatpak_tmp = NULL; /* only use the temporary GsFlatpak to avoid the auth dialog */ flatpak_tmp = gs_plugin_flatpak_create_temporary (plugin, cancellable, error); if (flatpak_tmp == NULL) return NULL; /* add object */ app = gs_flatpak_file_to_app_ref (flatpak_tmp, file, cancellable, error); if (app == NULL) return NULL; /* is this already installed or available in a configured remote */ ref = gs_flatpak_app_get_ref_display (app); app_tmp = gs_plugin_flatpak_find_app_by_ref (plugin, ref, cancellable, NULL); if (app_tmp != NULL) return g_steal_pointer (&app_tmp); /* force this to be 'any' scope for installation */ gs_app_set_scope (app, AS_APP_SCOPE_UNKNOWN); /* do we have a system runtime available */ runtime = gs_app_get_runtime (app); if (runtime != NULL) { g_autoptr(GsApp) runtime_tmp = NULL; g_autofree gchar *runtime_ref = gs_flatpak_app_get_ref_display (runtime); runtime_tmp = gs_plugin_flatpak_find_app_by_ref (plugin, runtime_ref, cancellable, NULL); if (runtime_tmp != NULL) { gs_app_set_runtime (app, runtime_tmp); } else { /* the new runtime is available from the RuntimeRepo */ if (gs_flatpak_app_get_runtime_url (runtime) != NULL) gs_app_set_state (runtime, AS_APP_STATE_AVAILABLE_LOCAL); } } /* this is new */ return g_steal_pointer (&app); } gboolean gs_plugin_file_to_app (GsPlugin *plugin, GsAppList *list, GFile *file, GCancellable *cancellable, GError **error) { g_autofree gchar *content_type = NULL; g_autoptr(GsApp) app = NULL; const gchar *mimetypes_bundle[] = { "application/vnd.flatpak", NULL }; const gchar *mimetypes_repo[] = { "application/vnd.flatpak.repo", NULL }; const gchar *mimetypes_ref[] = { "application/vnd.flatpak.ref", NULL }; /* does this match any of the mimetypes we support */ content_type = gs_utils_get_content_type (file, cancellable, error); if (content_type == NULL) return FALSE; if (g_strv_contains (mimetypes_bundle, content_type)) { app = gs_plugin_flatpak_file_to_app_bundle (plugin, file, cancellable, error); if (app == NULL) return FALSE; } else if (g_strv_contains (mimetypes_repo, content_type)) { app = gs_plugin_flatpak_file_to_app_repo (plugin, file, cancellable, error); if (app == NULL) return FALSE; } else if (g_strv_contains (mimetypes_ref, content_type)) { app = gs_plugin_flatpak_file_to_app_ref (plugin, file, cancellable, error); if (app == NULL) return FALSE; } if (app != NULL) gs_app_list_add (list, app); return TRUE; } gboolean gs_plugin_add_search (GsPlugin *plugin, gchar **values, GsAppList *list, GCancellable *cancellable, GError **error) { GsPluginData *priv = gs_plugin_get_data (plugin); for (guint i = 0; i < priv->flatpaks->len; i++) { GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i); if (!gs_flatpak_search (flatpak, (const gchar * const *) values, list, cancellable, error)) { return FALSE; } } return TRUE; } gboolean gs_plugin_add_categories (GsPlugin *plugin, GPtrArray *list, GCancellable *cancellable, GError **error) { GsPluginData *priv = gs_plugin_get_data (plugin); for (guint i = 0; i < priv->flatpaks->len; i++) { GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i); if (!gs_flatpak_add_categories (flatpak, list, cancellable, error)) return FALSE; } return TRUE; } gboolean gs_plugin_add_category_apps (GsPlugin *plugin, GsCategory *category, GsAppList *list, GCancellable *cancellable, GError **error) { GsPluginData *priv = gs_plugin_get_data (plugin); for (guint i = 0; i < priv->flatpaks->len; i++) { GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i); if (!gs_flatpak_add_category_apps (flatpak, category, list, cancellable, error)) { return FALSE; } } return TRUE; } gboolean gs_plugin_add_popular (GsPlugin *plugin, GsAppList *list, GCancellable *cancellable, GError **error) { GsPluginData *priv = gs_plugin_get_data (plugin); for (guint i = 0; i < priv->flatpaks->len; i++) { GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i); if (!gs_flatpak_add_popular (flatpak, list, cancellable, error)) return FALSE; } return TRUE; } gboolean gs_plugin_add_alternates (GsPlugin *plugin, GsApp *app, GsAppList *list, GCancellable *cancellable, GError **error) { GsPluginData *priv = gs_plugin_get_data (plugin); for (guint i = 0; i < priv->flatpaks->len; i++) { GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i); if (!gs_flatpak_add_alternates (flatpak, app, list, cancellable, error)) return FALSE; } return TRUE; } gboolean gs_plugin_add_featured (GsPlugin *plugin, GsAppList *list, GCancellable *cancellable, GError **error) { GsPluginData *priv = gs_plugin_get_data (plugin); for (guint i = 0; i < priv->flatpaks->len; i++) { GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i); if (!gs_flatpak_add_featured (flatpak, list, cancellable, error)) return FALSE; } return TRUE; } gboolean gs_plugin_add_recent (GsPlugin *plugin, GsAppList *list, guint64 age, GCancellable *cancellable, GError **error) { GsPluginData *priv = gs_plugin_get_data (plugin); for (guint i = 0; i < priv->flatpaks->len; i++) { GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i); if (!gs_flatpak_add_recent (flatpak, list, age, cancellable, error)) return FALSE; } return TRUE; }