1
0
Fork 0
gnome-software/plugins/epiphany/gs-plugin-epiphany.c
Daniel Baumann 68ee05b3fd
Adding upstream version 48.2.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-22 21:00:23 +02:00

1639 lines
55 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
* vi:set noexpandtab tabstop=8 shiftwidth=8:
*
* Copyright (C) 2021-2022 Matthew Leeds <mwleeds@protonmail.com>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include <config.h>
#include <glib/gi18n.h>
#include <gnome-software.h>
#include <fcntl.h>
#include <gio/gunixfdlist.h>
#include <glib/gstdio.h>
#include "gs-epiphany-generated.h"
#include "gs-plugin-epiphany.h"
#include "gs-plugin-private.h"
/*
* SECTION:
* This plugin uses Epiphany to install, launch, and uninstall web applications.
*
* If the org.gnome.Epiphany.WebAppProvider D-Bus interface is not present or
* the DynamicLauncher portal is not available then it self-disables. This
* should work with both Flatpak'd and traditionally packaged Epiphany, for new
* enough versions of Epiphany.
*
* It's worth noting that this plugin has to deal with two different app IDs
* for installed web apps:
*
* 1. The app ID used in the <id> element in the AppStream metainfo file, which
* looks like "org.gnome.Software.WebApp_527a2dd6729c3574227c145bbc447997f0048537.desktop"
* See https://gitlab.gnome.org/mwleeds/gnome-pwa-list/-/blob/6e8b17b018f99dbf00b1fa956ed75c4a0ccbf389/pwa-metainfo-generator.py#L84-89
* This app ID is used for gs_app_new() so that the appstream plugin
* refines the apps created here, and used for the plugin cache.
*
* 2. The app ID generated by Epiphany when installing a web app, which looks
* like "org.gnome.Epiphany.WebApp_e9d0e1e4b0a10856aa3b38d9eb4375de4070d043.desktop"
* though it can have a different prefix if Epiphany was built with, for
* example, a development profile. Throughout this plugin this type of app
* ID is handled with a variable called "installed_app_id". This app ID is
* used in method calls to the org.gnome.Epiphany.WebAppProvider interface,
* and used for gs_app_set_launchable() and g_desktop_app_info_new().
*
* Since: 43
*/
struct _GsPluginEpiphany
{
GsPlugin parent;
GsWorkerThread *worker; /* (owned) */
GsEphyWebAppProvider *epiphany_proxy; /* (owned) */
GDBusProxy *launcher_portal_proxy; /* (owned) */
GFileMonitor *monitor; /* (owned) */
guint changed_id;
/* protects installed_apps_cached, url_id_map, and the plugin cache */
GMutex installed_apps_mutex;
/* installed_apps_cached: whether the plugin cache has all installed apps */
gboolean installed_apps_cached;
GHashTable *url_id_map; /* (owned) (not nullable) (element-type utf8 utf8) */
/* default permissions, shared between all applications */
GsAppPermissions *permissions; /* (owned) (not nullable) */
};
G_DEFINE_TYPE (GsPluginEpiphany, gs_plugin_epiphany, GS_TYPE_PLUGIN)
#define assert_in_worker(self) \
g_assert (gs_worker_thread_is_in_worker_context (self->worker))
static void
gs_epiphany_error_convert (GError **perror)
{
GError *error = perror != NULL ? *perror : NULL;
/* not set */
if (error == NULL)
return;
/* parse remote epiphany-webapp-provider error */
if (g_dbus_error_is_remote_error (error)) {
g_autofree gchar *remote_error = g_dbus_error_get_remote_error (error);
g_dbus_error_strip_remote_error (error);
if (g_str_equal (remote_error, "org.freedesktop.DBus.Error.ServiceUnknown")) {
error->code = GS_PLUGIN_ERROR_NOT_SUPPORTED;
} else if (g_str_has_prefix (remote_error, "org.gnome.Epiphany.WebAppProvider.Error")) {
error->code = GS_PLUGIN_ERROR_FAILED;
} else {
g_warning ("Cant reliably fixup remote error %s", remote_error);
error->code = GS_PLUGIN_ERROR_FAILED;
}
error->domain = GS_PLUGIN_ERROR;
return;
}
/* this is allowed for low-level errors */
if (gs_utils_error_convert_gio (perror))
return;
/* this is allowed for low-level errors */
if (gs_utils_error_convert_gdbus (perror))
return;
}
/* Run in the main thread. */
static void
gs_plugin_epiphany_changed_cb (GFileMonitor *monitor,
GFile *file,
GFile *other_file,
GFileMonitorEvent event_type,
gpointer user_data)
{
GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (user_data);
{
g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->installed_apps_mutex);
gs_plugin_cache_invalidate (GS_PLUGIN (self));
g_hash_table_remove_all (self->url_id_map);
self->installed_apps_cached = FALSE;
}
/* FIXME: With the current API this is the only way to reload the list
* of installed apps.
*/
gs_plugin_reload (GS_PLUGIN (self));
}
static void
epiphany_web_app_provider_proxy_created_cb (GObject *source_object,
GAsyncResult *result,
gpointer user_data);
static void
dynamic_launcher_portal_proxy_created_cb (GObject *source_object,
GAsyncResult *result,
gpointer user_data);
static void
gs_plugin_epiphany_setup_async (GsPlugin *plugin,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (plugin);
g_autoptr(GTask) task = NULL;
g_autoptr(GError) local_error = NULL;
g_autofree char *portal_apps_path = NULL;
g_autoptr(GFile) portal_apps_file = NULL;
GDBusConnection *connection;
task = g_task_new (plugin, cancellable, callback, user_data);
g_task_set_source_tag (task, gs_plugin_epiphany_setup_async);
g_debug ("%s", G_STRFUNC);
self->installed_apps_cached = FALSE;
/* This is a mapping from URL to app ID, where the app ID comes from
* Epiphany. This allows us to use that app ID rather than the
* AppStream app ID in certain contexts (see the comment at the top of
* this file).
*/
self->url_id_map = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
/* Watch for changes to the set of installed apps in the main thread.
* This will also trigger when other apps' dynamic launchers are
* installed or removed but that is expected to be infrequent.
*/
portal_apps_path = g_build_filename (g_get_user_data_dir (), "xdg-desktop-portal", "applications", NULL);
portal_apps_file = g_file_new_for_path (portal_apps_path);
/* Monitoring the directory works even if it doesn't exist yet */
self->monitor = g_file_monitor_directory (portal_apps_file, G_FILE_MONITOR_WATCH_MOVES,
cancellable, &local_error);
if (self->monitor == NULL) {
gs_epiphany_error_convert (&local_error);
g_task_return_error (task, g_steal_pointer (&local_error));
return;
}
self->changed_id = g_signal_connect (self->monitor, "changed",
G_CALLBACK (gs_plugin_epiphany_changed_cb), self);
connection = gs_plugin_get_session_bus_connection (GS_PLUGIN (self));
g_assert (connection != NULL);
gs_ephy_web_app_provider_proxy_new (connection,
G_DBUS_PROXY_FLAGS_NONE,
"org.gnome.Epiphany.WebAppProvider",
"/org/gnome/Epiphany/WebAppProvider",
cancellable,
epiphany_web_app_provider_proxy_created_cb,
g_steal_pointer (&task));
}
static void
epiphany_web_app_provider_proxy_created_cb (GObject *source_object,
GAsyncResult *result,
gpointer user_data)
{
g_autoptr(GTask) task = G_TASK (user_data);
g_autofree gchar *name_owner = NULL;
g_autoptr(GError) local_error = NULL;
GsPluginEpiphany *self = g_task_get_source_object (task);
GDBusConnection *connection;
GCancellable *cancellable;
/* Check that the proxy exists (and is owned; it should auto-start) so
* we can disable the plugin for systems which dont have new enough
* Epiphany.
*/
self->epiphany_proxy = gs_ephy_web_app_provider_proxy_new_finish (result, &local_error);
if (self->epiphany_proxy == NULL) {
gs_epiphany_error_convert (&local_error);
g_task_return_error (task, g_steal_pointer (&local_error));
return;
}
name_owner = g_dbus_proxy_get_name_owner (G_DBUS_PROXY (self->epiphany_proxy));
if (name_owner == NULL) {
g_task_return_new_error (task, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NOT_SUPPORTED,
"Couldnt create Epiphany WebAppProvider proxy: couldnt get name owner");
return;
}
connection = g_dbus_proxy_get_connection (G_DBUS_PROXY (self->epiphany_proxy));
cancellable = g_task_get_cancellable (task);
g_dbus_proxy_new (connection,
G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS,
NULL,
"org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop",
"org.freedesktop.portal.DynamicLauncher",
cancellable,
dynamic_launcher_portal_proxy_created_cb,
g_steal_pointer (&task));
}
static void
dynamic_launcher_portal_proxy_created_cb (GObject *source_object,
GAsyncResult *result,
gpointer user_data)
{
g_autoptr(GTask) task = G_TASK (user_data);
g_autoptr(GVariant) version = NULL;
g_autoptr(GError) local_error = NULL;
GsPluginEpiphany *self = g_task_get_source_object (task);
/* Check that the proxy exists (and is owned; it should auto-start) so
* we can disable the plugin for systems which dont have new enough
* Epiphany.
*/
self->launcher_portal_proxy = g_dbus_proxy_new_finish (result, &local_error);
if (self->launcher_portal_proxy == NULL) {
gs_epiphany_error_convert (&local_error);
g_task_return_error (task, g_steal_pointer (&local_error));
return;
}
version = g_dbus_proxy_get_cached_property (self->launcher_portal_proxy, "version");
if (version == NULL) {
g_task_return_new_error (task, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NOT_SUPPORTED,
"Dynamic launcher portal not available");
return;
} else {
g_debug ("Found version %" G_GUINT32_FORMAT " of the dynamic launcher portal",
g_variant_get_uint32 (version));
}
/* Start up a worker thread to process all the plugins function calls. */
self->worker = gs_worker_thread_new ("gs-plugin-epiphany");
g_task_return_boolean (task, TRUE);
}
static gboolean
gs_plugin_epiphany_setup_finish (GsPlugin *plugin,
GAsyncResult *result,
GError **error)
{
return g_task_propagate_boolean (G_TASK (result), error);
}
static void shutdown_cb (GObject *source_object,
GAsyncResult *result,
gpointer user_data);
static void
gs_plugin_epiphany_shutdown_async (GsPlugin *plugin,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (plugin);
g_autoptr(GTask) task = NULL;
task = g_task_new (self, cancellable, callback, user_data);
g_task_set_source_tag (task, gs_plugin_epiphany_shutdown_async);
/* Stop the worker thread. */
gs_worker_thread_shutdown_async (self->worker, cancellable, shutdown_cb, g_steal_pointer (&task));
}
static void
shutdown_cb (GObject *source_object,
GAsyncResult *result,
gpointer user_data)
{
g_autoptr(GTask) task = G_TASK (user_data);
GsPluginEpiphany *self = g_task_get_source_object (task);
g_autoptr(GsWorkerThread) worker = NULL;
g_autoptr(GError) local_error = NULL;
worker = g_steal_pointer (&self->worker);
if (!gs_worker_thread_shutdown_finish (worker, result, &local_error)) {
g_task_return_error (task, g_steal_pointer (&local_error));
return;
}
g_task_return_boolean (task, TRUE);
}
static gboolean
gs_plugin_epiphany_shutdown_finish (GsPlugin *plugin,
GAsyncResult *result,
GError **error)
{
return g_task_propagate_boolean (G_TASK (result), error);
}
static void
gs_plugin_epiphany_init (GsPluginEpiphany *self)
{
/* Re-used permissions by all GsApp instances; do not modify it out
of this place. */
self->permissions = gs_app_permissions_new ();
gs_app_permissions_set_flags (self->permissions, GS_APP_PERMISSIONS_FLAGS_NETWORK);
gs_app_permissions_seal (self->permissions);
/* set name of MetaInfo file */
gs_plugin_set_appstream_id (GS_PLUGIN (self), "org.gnome.Software.Plugin.Epiphany");
/* need help from appstream */
gs_plugin_add_rule (GS_PLUGIN (self), GS_PLUGIN_RULE_RUN_AFTER, "appstream");
/* prioritize over packages */
gs_plugin_add_rule (GS_PLUGIN (self), GS_PLUGIN_RULE_BETTER_THAN, "packagekit");
}
static void
gs_plugin_epiphany_dispose (GObject *object)
{
GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (object);
if (self->changed_id > 0) {
g_signal_handler_disconnect (self->monitor, self->changed_id);
self->changed_id = 0;
}
g_clear_object (&self->epiphany_proxy);
g_clear_object (&self->launcher_portal_proxy);
g_clear_object (&self->monitor);
g_clear_object (&self->worker);
g_clear_pointer (&self->url_id_map, g_hash_table_unref);
G_OBJECT_CLASS (gs_plugin_epiphany_parent_class)->dispose (object);
}
static void
gs_plugin_epiphany_finalize (GObject *object)
{
GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (object);
g_mutex_clear (&self->installed_apps_mutex);
g_clear_object (&self->permissions);
G_OBJECT_CLASS (gs_plugin_epiphany_parent_class)->finalize (object);
}
static gboolean ensure_installed_apps_cache (GsPluginEpiphany *self,
gboolean interactive,
GCancellable *cancellable,
GError **error);
/* May be run in @worker or in main thread. The caller must have already done ensure_installed_apps_cache() */
static void
gs_epiphany_refine_app_state (GsPlugin *plugin,
GsApp *app)
{
if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN) {
g_autoptr(GsApp) cached_app = NULL;
const char *appstream_source;
/* If we have a cached app, set the state from there. Otherwise
* only set the state to available if the app came from
* appstream data, because there's no way to re-install an app
* in Software that was originally installed from Epiphany,
* unless we have appstream metainfo for it.
*/
cached_app = gs_plugin_cache_lookup (plugin, gs_app_get_id (app));
appstream_source = gs_app_get_metadata_item (app, "appstream::source-file");
if (cached_app)
gs_app_set_state (app, gs_app_get_state (cached_app));
else if (appstream_source)
gs_app_set_state (app, GS_APP_STATE_AVAILABLE);
else {
gs_app_set_state (app, GS_APP_STATE_UNAVAILABLE);
gs_app_set_url_missing (app,
"help:gnome-software/how-to-reinstall-a-web-app");
}
}
}
void
gs_plugin_adopt_app (GsPlugin *plugin,
GsApp *app)
{
if (gs_app_get_kind (app) == AS_COMPONENT_KIND_WEB_APP &&
gs_app_get_bundle_kind (app) != AS_BUNDLE_KIND_PACKAGE) {
gs_app_set_management_plugin (app, plugin);
}
}
static gint
get_priority_for_interactivity (gboolean interactive)
{
return interactive ? G_PRIORITY_DEFAULT : G_PRIORITY_LOW;
}
static void list_apps_thread_cb (GTask *task,
gpointer source_object,
gpointer task_data,
GCancellable *cancellable);
static void
gs_plugin_epiphany_list_apps_async (GsPlugin *plugin,
GsAppQuery *query,
GsPluginListAppsFlags flags,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (plugin);
g_autoptr(GTask) task = NULL;
gboolean interactive = (flags & GS_PLUGIN_LIST_APPS_FLAGS_INTERACTIVE);
task = gs_plugin_list_apps_data_new_task (plugin, query, flags,
cancellable, callback, user_data);
g_task_set_source_tag (task, gs_plugin_epiphany_list_apps_async);
/* Queue a job to get the apps. */
gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive),
list_apps_thread_cb, g_steal_pointer (&task));
}
/* Run in @worker */
static void
refine_app (GsPluginEpiphany *self,
GsApp *app,
GsPluginRefineFlags flags,
GUri *uri,
const char *url)
{
const char *hostname;
const char *installed_app_id;
const struct {
const gchar *hostname;
const gchar *license_spdx;
} app_licenses[] = {
/* Keep in alphabetical order by hostname */
{ "app.diagrams.net", "Apache-2.0" },
{ "devdocs.io", "MPL-2.0" },
{ "discourse.flathub.org", "GPL-2.0-or-later" },
{ "discourse.gnome.org", "GPL-2.0-or-later" },
{ "excalidraw.com", "MIT" },
{ "pinafore.social", "AGPL-3.0-only" },
{ "snapdrop.net", "GPL-3.0-only" },
{ "stackedit.io", "Apache-2.0" },
{ "squoosh.app", "Apache-2.0" },
};
g_return_if_fail (GS_IS_APP (app));
g_return_if_fail (uri != NULL);
g_return_if_fail (url != NULL);
gs_app_set_origin (app, "gnome-web");
if (gs_app_get_name (app) == NULL)
gs_app_set_origin_ui (app, _("Web App"));
else
gs_app_set_origin_ui (app, gs_app_get_name (app));
gs_app_set_origin_hostname (app, g_uri_get_host (uri));
gs_app_set_metadata (app, "GnomeSoftware::PackagingFormat", _("Web App"));
gs_app_set_metadata (app, "GnomeSoftware::PackagingIcon", "web-browser-symbolic");
gs_app_set_scope (app, AS_COMPONENT_SCOPE_USER);
gs_app_set_launchable (app, AS_LAUNCHABLE_KIND_URL, url);
installed_app_id = g_hash_table_lookup (self->url_id_map, url);
if (installed_app_id) {
gs_app_set_launchable (app, AS_LAUNCHABLE_KIND_DESKTOP_ID, installed_app_id);
}
/* Hard-code the licenses as it's hard to get them programmatically. We
* can move them to an AppStream file if needed.
*/
hostname = g_uri_get_host (uri);
if (gs_app_get_license (app) == NULL && hostname != NULL) {
for (gsize i = 0; i < G_N_ELEMENTS (app_licenses); i++) {
if (g_str_equal (hostname, app_licenses[i].hostname)) {
gs_app_set_license (app, GS_APP_QUALITY_NORMAL,
app_licenses[i].license_spdx);
break;
}
}
}
gs_app_set_size_download (app, GS_SIZE_TYPE_VALID, 0);
/* Use the default permissions */
gs_app_set_permissions (app, self->permissions);
if (gs_app_get_url (app, AS_URL_KIND_HOMEPAGE) == NULL)
gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, url);
/* Use the domain name (e.g. "discourse.gnome.org") as a fallback summary.
* FIXME: Fetch the summary from the site's webapp manifest.
*/
if (gs_app_get_summary (app) == NULL) {
if (hostname != NULL && *hostname != '\0')
gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, hostname);
else
gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, url);
}
if (installed_app_id == NULL)
return;
{
const gchar *name;
g_autofree char *icon_path = NULL;
goffset desktop_size = 0, icon_size = 0;
g_autoptr(GDesktopAppInfo) desktop_info = NULL;
g_autoptr(GFileInfo) file_info = NULL;
g_autoptr(GFile) icon_file = NULL;
desktop_info = g_desktop_app_info_new (installed_app_id);
if (desktop_info == NULL) {
g_warning ("Couldn't get GDesktopAppInfo for app %s", installed_app_id);
return;
}
name = g_app_info_get_name (G_APP_INFO (desktop_info));
gs_app_set_name (app, GS_APP_QUALITY_NORMAL, name);
if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE) {
g_autoptr(GFile) desktop_file = NULL;
const gchar *desktop_path;
guint64 install_date = 0;
desktop_path = g_desktop_app_info_get_filename (desktop_info);
g_assert (desktop_path);
desktop_file = g_file_new_for_path (desktop_path);
file_info = g_file_query_info (desktop_file,
G_FILE_ATTRIBUTE_TIME_CREATED "," G_FILE_ATTRIBUTE_STANDARD_SIZE,
0, NULL, NULL);
if (file_info) {
install_date = g_file_info_get_attribute_uint64 (file_info, G_FILE_ATTRIBUTE_TIME_CREATED);
desktop_size = g_file_info_get_size (file_info);
}
if (install_date) {
gs_app_set_install_date (app, install_date);
}
}
icon_path = g_desktop_app_info_get_string (desktop_info, "Icon");
if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE &&
icon_path) {
icon_file = g_file_new_for_path (icon_path);
g_clear_object (&file_info);
file_info = g_file_query_info (icon_file,
G_FILE_ATTRIBUTE_STANDARD_SIZE,
0, NULL, NULL);
if (file_info)
icon_size = g_file_info_get_size (file_info);
}
if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON &&
!gs_app_has_icons (app) &&
icon_path) {
g_autoptr(GIcon) icon = g_file_icon_new (icon_file);
g_autofree char *icon_dir = g_path_get_dirname (icon_path);
g_autofree char *icon_dir_basename = g_path_get_basename (icon_dir);
const char *x;
guint64 width = 0;
/* dir should be either scalable or e.g. 512x512 */
if (g_strcmp0 (icon_dir_basename, "scalable") == 0) {
/* Ensure scalable icons are preferred */
width = 4096;
} else if ((x = strchr (icon_dir_basename, 'x')) != NULL) {
g_ascii_string_to_unsigned (x + 1, 10, 1, G_MAXINT, &width, NULL);
}
if (width > 0 && width <= 4096) {
gs_icon_set_width (icon, width);
gs_icon_set_height (icon, width);
} else {
g_warning ("Unexpectedly unable to determine width of icon %s", icon_path);
}
gs_app_add_icon (app, icon);
}
if (desktop_size > 0 || icon_size > 0) {
gs_app_set_size_installed (app, GS_SIZE_TYPE_VALID, desktop_size + icon_size);
}
}
}
/* Run in @worker */
static GsApp *
gs_epiphany_create_app (GsPluginEpiphany *self,
const char *id)
{
g_autoptr(GsApp) app = NULL;
assert_in_worker (self);
app = gs_plugin_cache_lookup (GS_PLUGIN (self), id);
if (app != NULL)
return g_steal_pointer (&app);
app = gs_app_new (id);
gs_app_set_management_plugin (app, GS_PLUGIN (self));
gs_app_set_kind (app, AS_COMPONENT_KIND_WEB_APP);
gs_app_set_metadata (app, "GnomeSoftware::Creator",
gs_plugin_get_name (GS_PLUGIN (self)));
gs_plugin_cache_add (GS_PLUGIN (self), id, app);
return g_steal_pointer (&app);
}
static gchar * /* (transfer full) */
generate_app_id_for_url (const gchar *url)
{
/* Generate the app ID used in the AppStream data using the
* same method as pwa-metainfo-generator.py in
* https://gitlab.gnome.org/mwleeds/gnome-pwa-list
* Using this app ID rather than the one provided by Epiphany
* makes it possible for the appstream plugin to refine the
* GsApp we create (see the comment at the top of this file).
*/
g_autofree gchar *url_hash = g_compute_checksum_for_string (G_CHECKSUM_SHA1, url, -1);
return g_strconcat ("org.gnome.Software.WebApp_", url_hash, ".desktop", NULL);
}
/* Run in @worker */
static gboolean
ensure_installed_apps_cache (GsPluginEpiphany *self,
gboolean interactive,
GCancellable *cancellable,
GError **error)
{
g_auto(GStrv) webapps = NULL;
guint n_webapps;
g_autoptr(GsAppList) installed_cache = gs_app_list_new ();
g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->installed_apps_mutex);
assert_in_worker (self);
if (self->installed_apps_cached)
return TRUE;
if (!gs_ephy_web_app_provider_call_get_installed_apps_sync (self->epiphany_proxy,
interactive ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION : G_DBUS_CALL_FLAGS_NONE,
-1 /* timeout */,
&webapps,
cancellable,
error)) {
gs_epiphany_error_convert (error);
return FALSE;
}
n_webapps = g_strv_length (webapps);
g_debug ("%s: epiphany-webapp-provider returned %u installed web apps", G_STRFUNC, n_webapps);
for (guint i = 0; i < n_webapps; i++) {
const gchar *desktop_file_id = webapps[i];
const gchar *url = NULL;
g_autofree char *metainfo_app_id = NULL;
const gchar *exec;
int argc;
GsPluginRefineFlags refine_flags;
g_auto(GStrv) argv = NULL;
g_autoptr(GsApp) app = NULL;
g_autoptr(GDesktopAppInfo) desktop_info = NULL;
g_autoptr(GUri) uri = NULL;
g_debug ("%s: Working on installed web app %s", G_STRFUNC, desktop_file_id);
desktop_info = g_desktop_app_info_new (desktop_file_id);
if (desktop_info == NULL) {
g_warning ("Epiphany returned a non-existent or invalid desktop ID %s", desktop_file_id);
continue;
}
/* This way of getting the URL is a bit hacky but it's what
* Epiphany does, specifically in
* ephy_web_application_for_profile_directory() which lives in
* https://gitlab.gnome.org/GNOME/epiphany/-/blob/master/lib/ephy-web-app-utils.c
*/
exec = g_app_info_get_commandline (G_APP_INFO (desktop_info));
if (g_shell_parse_argv (exec, &argc, &argv, NULL)) {
g_assert (argc > 0);
url = argv[argc - 1];
}
if (!url || !(uri = g_uri_parse (url, G_URI_FLAGS_NONE, NULL))) {
g_warning ("Failed to parse URL for web app %s: %s",
desktop_file_id, url ? url : "(null)");
continue;
}
/* Store the installed app id for use in refine_app() */
g_hash_table_insert (self->url_id_map, g_strdup (url),
g_strdup (desktop_file_id));
metainfo_app_id = generate_app_id_for_url (url);
g_debug ("Creating GsApp for webapp with URL %s using app ID %s (desktop file id: %s)",
url, metainfo_app_id, desktop_file_id);
/* App gets added to the plugin cache here */
app = gs_epiphany_create_app (self, metainfo_app_id);
gs_app_set_state (app, GS_APP_STATE_INSTALLED);
refine_flags = GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON |
GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE |
GS_PLUGIN_REFINE_FLAGS_REQUIRE_ID;
refine_app (self, app, refine_flags, uri, url);
}
/* Update the state on any apps that were uninstalled outside
* gnome-software
*/
gs_plugin_cache_lookup_by_state (GS_PLUGIN (self), installed_cache, GS_APP_STATE_INSTALLED);
for (guint i = 0; i < gs_app_list_length (installed_cache); i++) {
GsApp *app = gs_app_list_index (installed_cache, i);
const char *installed_app_id;
const char *appstream_source;
installed_app_id = gs_app_get_launchable (app, AS_LAUNCHABLE_KIND_DESKTOP_ID);
if (installed_app_id == NULL) {
g_warning ("Installed app unexpectedly has no desktop id: %s", gs_app_get_id (app));
continue;
}
if (g_strv_contains ((const char * const *)webapps, installed_app_id))
continue;
gs_plugin_cache_remove (GS_PLUGIN (self), gs_app_get_id (app));
appstream_source = gs_app_get_metadata_item (app, "appstream::source-file");
if (appstream_source)
gs_app_set_state (app, GS_APP_STATE_AVAILABLE);
else
gs_app_set_state (app, GS_APP_STATE_UNKNOWN);
}
self->installed_apps_cached = TRUE;
return TRUE;
}
/* Run in @worker */
static void
list_apps_thread_cb (GTask *task,
gpointer source_object,
gpointer task_data,
GCancellable *cancellable)
{
GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (source_object);
g_autoptr(GsAppList) list = gs_app_list_new ();
GsPluginListAppsData *data = task_data;
GsAppQueryTristate is_installed = GS_APP_QUERY_TRISTATE_UNSET;
const gchar * const *keywords = NULL;
g_autoptr(GError) local_error = NULL;
assert_in_worker (self);
if (data->query != NULL) {
is_installed = gs_app_query_get_is_installed (data->query);
keywords = gs_app_query_get_keywords (data->query);
}
/* Currently only support a subset of query properties, and only one set at once.
* Also dont currently support GS_APP_QUERY_TRISTATE_FALSE. */
if ((is_installed == GS_APP_QUERY_TRISTATE_UNSET &&
keywords == NULL) ||
is_installed == GS_APP_QUERY_TRISTATE_FALSE ||
gs_app_query_get_n_properties_set (data->query) != 1) {
g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
"Unsupported query");
return;
}
/* Ensure the cache is up to date. */
if (!ensure_installed_apps_cache (self, data->flags & GS_PLUGIN_LIST_APPS_FLAGS_INTERACTIVE, cancellable, &local_error)) {
g_task_return_error (task, g_steal_pointer (&local_error));
return;
}
if (is_installed == GS_APP_QUERY_TRISTATE_TRUE)
gs_plugin_cache_lookup_by_state (GS_PLUGIN (self), list, GS_APP_STATE_INSTALLED);
else if (keywords != NULL) {
for (gsize i = 0; keywords[i]; i++) {
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init (&iter, self->url_id_map);
while (g_hash_table_iter_next (&iter, &key, &value)) {
const gchar *url = key;
const gchar *app_id = value;
if (g_strcmp0 (app_id, keywords[i]) == 0) {
g_autoptr(GsApp) app = NULL;
g_autofree gchar *metainfo_app_id = NULL;
metainfo_app_id = generate_app_id_for_url (url);
app = gs_plugin_cache_lookup (GS_PLUGIN (self), metainfo_app_id);
if (app != NULL)
gs_app_list_add (list, app);
break;
}
}
}
}
g_task_return_pointer (task, g_steal_pointer (&list), g_object_unref);
}
static GsAppList *
gs_plugin_epiphany_list_apps_finish (GsPlugin *plugin,
GAsyncResult *result,
GError **error)
{
g_return_val_if_fail (g_task_get_source_tag (G_TASK (result)) == gs_plugin_epiphany_list_apps_async, FALSE);
return g_task_propagate_pointer (G_TASK (result), error);
}
static void
gs_epiphany_refine_app (GsPluginEpiphany *self,
GsApp *app,
GsPluginRefineFlags refine_flags,
const char *url)
{
g_autoptr(GUri) uri = NULL;
g_return_if_fail (url != NULL && *url != '\0');
if (!(uri = g_uri_parse (url, G_URI_FLAGS_NONE, NULL))) {
g_warning ("Failed to parse URL for web app %s: %s", gs_app_get_id (app), url);
return;
}
refine_app (self, app, refine_flags, uri, url);
}
static void refine_thread_cb (GTask *task,
gpointer source_object,
gpointer task_data,
GCancellable *cancellable);
static void
gs_plugin_epiphany_refine_async (GsPlugin *plugin,
GsAppList *list,
GsPluginRefineFlags flags,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (plugin);
g_autoptr(GTask) task = NULL;
gboolean interactive = gs_plugin_has_flags (GS_PLUGIN (self), GS_PLUGIN_FLAGS_INTERACTIVE);
task = gs_plugin_refine_data_new_task (plugin, list, flags, cancellable, callback, user_data);
g_task_set_source_tag (task, gs_plugin_epiphany_refine_async);
/* Queue a job for the refine. */
gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive),
refine_thread_cb, g_steal_pointer (&task));
}
/* Run in @worker. */
static void
refine_thread_cb (GTask *task,
gpointer source_object,
gpointer task_data,
GCancellable *cancellable)
{
GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (source_object);
GsPluginRefineData *data = task_data;
GsPluginRefineFlags flags = data->flags;
GsAppList *list = data->list;
gboolean interactive = gs_plugin_has_flags (GS_PLUGIN (self), GS_PLUGIN_FLAGS_INTERACTIVE);
g_autoptr(GError) local_error = NULL;
assert_in_worker (self);
if (!ensure_installed_apps_cache (self, interactive, cancellable, &local_error)) {
g_task_return_error (task, g_steal_pointer (&local_error));
return;
}
for (guint i = 0; i < gs_app_list_length (list); i++) {
GsApp *app = gs_app_list_index (list, i);
const char *url;
/* not us */
if (gs_app_get_kind (app) != AS_COMPONENT_KIND_WEB_APP ||
gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_PACKAGE)
continue;
url = gs_app_get_launchable (app, AS_LAUNCHABLE_KIND_URL);
if (url == NULL || *url == '\0') {
/* A launchable URL is required by the AppStream spec */
g_warning ("Web app %s missing launchable url", gs_app_get_id (app));
continue;
}
g_debug ("epiphany: refining app %s", gs_app_get_id (app));
gs_epiphany_refine_app (self, app, flags, url);
gs_epiphany_refine_app_state (GS_PLUGIN (self), app);
/* Usually the way to refine wildcard apps is to create a new
* GsApp and add it to the results list, but in this case we
* need to use the app that was refined by the appstream plugin
* as it has all the metadata set already, and this is the only
* plugin for dealing with web apps, so it should be safe to
* adopt the wildcard app.
*/
if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD)) {
gs_app_remove_quirk (app, GS_APP_QUIRK_IS_WILDCARD);
gs_app_set_management_plugin (app, GS_PLUGIN (self));
gs_plugin_cache_add (GS_PLUGIN (self), gs_app_get_id (app), app);
}
}
/* success */
g_task_return_boolean (task, TRUE);
}
static gboolean
gs_plugin_epiphany_refine_finish (GsPlugin *plugin,
GAsyncResult *result,
GError **error)
{
g_return_val_if_fail (g_task_get_source_tag (G_TASK (result)) == gs_plugin_epiphany_refine_async, FALSE);
return g_task_propagate_boolean (G_TASK (result), error);
}
static GVariant *
get_serialized_icon (GsApp *app,
GIcon *icon)
{
g_autofree char *icon_path = NULL;
g_autoptr(GInputStream) stream = NULL;
g_autoptr(GBytes) bytes = NULL;
g_autoptr(GIcon) bytes_icon = NULL;
g_autoptr(GVariant) icon_v = NULL;
guint icon_width;
/* Note: GsRemoteIcon will work on this GFileIcon code path.
* The icons plugin should have called
* gs_app_ensure_icons_downloaded() for us.
*/
if (!G_IS_FILE_ICON (icon))
return NULL;
icon_path = g_file_get_path (g_file_icon_get_file (G_FILE_ICON (icon)));
if (!g_str_has_suffix (icon_path, ".png") &&
!g_str_has_suffix (icon_path, ".svg") &&
!g_str_has_suffix (icon_path, ".jpeg") &&
!g_str_has_suffix (icon_path, ".jpg")) {
g_warning ("Icon for app %s has unsupported file extension: %s",
gs_app_get_id (app), icon_path);
return NULL;
}
/* Scale down to the portal's size limit if needed */
icon_width = gs_icon_get_width (icon);
if (icon_width > 512)
icon_width = 512;
/* Serialize the icon as a #GBytesIcon since that's
* what the dynamic launcher portal requires.
*/
stream = g_loadable_icon_load (G_LOADABLE_ICON (icon), icon_width, NULL, NULL, NULL);
/* Icons are usually smaller than 1 MiB. Set a 10 MiB
* limit so we can't use a huge amount of memory or hit
* the D-Bus message size limit
*/
if (stream)
bytes = g_input_stream_read_bytes (stream, 10485760 /* 10 MiB */, NULL, NULL);
if (bytes)
bytes_icon = g_bytes_icon_new (bytes);
if (bytes_icon)
icon_v = g_icon_serialize (bytes_icon);
return g_steal_pointer (&icon_v);
}
typedef struct {
/* Input data. */
guint n_apps;
GsPluginInstallAppsFlags flags;
GsPluginProgressCallback progress_callback;
gpointer progress_user_data;
/* In-progress data. */
guint n_pending_ops;
GError *saved_error; /* (owned) (nullable) */
/* For progress reporting */
guint n_tokens_requested;
guint n_tokens_received;
guint n_apps_installed;
} InstallAppsData;
static void
install_apps_data_free (InstallAppsData *data)
{
/* Error should have been propagated by now, and all pending ops completed. */
g_assert (data->saved_error == NULL);
g_assert (data->n_pending_ops == 0);
g_free (data);
}
G_DEFINE_AUTOPTR_CLEANUP_FUNC (InstallAppsData, install_apps_data_free)
typedef struct {
GTask *task; /* (owned) */
GsApp *app; /* (owned) */
gchar *name; /* (owned) (not nullable) */
gchar *url; /* (owned) (not nullable) */
} InstallSingleAppData;
static void
install_single_app_data_free (InstallSingleAppData *data)
{
g_clear_object (&data->app);
g_clear_object (&data->task);
g_free (data->name);
g_free (data->url);
g_free (data);
}
G_DEFINE_AUTOPTR_CLEANUP_FUNC (InstallSingleAppData, install_single_app_data_free)
static void install_request_token_cb (GObject *source_object,
GAsyncResult *result,
gpointer user_data);
static void install_install_cb (GObject *source_object,
GAsyncResult *result,
gpointer user_data);
static void finish_install_apps_op (GTask *task,
GError *error);
static void
gs_plugin_epiphany_install_apps_async (GsPlugin *plugin,
GsAppList *apps,
GsPluginInstallAppsFlags flags,
GsPluginProgressCallback progress_callback,
gpointer progress_user_data,
GsPluginAppNeedsUserActionCallback app_needs_user_action_callback,
gpointer app_needs_user_action_data,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (plugin);
g_autoptr(GTask) task = NULL;
InstallAppsData *data;
g_autoptr(InstallAppsData) data_owned = NULL;
gboolean interactive = (flags & GS_PLUGIN_INSTALL_APPS_FLAGS_INTERACTIVE);
g_autoptr(GError) local_error = NULL;
task = g_task_new (self, cancellable, callback, user_data);
g_task_set_source_tag (task, gs_plugin_epiphany_install_apps_async);
data = data_owned = g_new0 (InstallAppsData, 1);
data->flags = flags;
data->progress_callback = progress_callback;
data->progress_user_data = progress_user_data;
data->n_apps = gs_app_list_length (apps);
g_task_set_task_data (task, g_steal_pointer (&data_owned), (GDestroyNotify) install_apps_data_free);
/* Start a load of operations in parallel to install the apps.
*
* When all installs are finished for all apps, finish_install_apps_op()
* will return success/error for the overall #GTask. */
data->n_pending_ops = 1;
data->n_tokens_requested = 0;
data->n_tokens_received = 0;
for (guint i = 0; i < data->n_apps; i++) {
GsApp *app = gs_app_list_index (apps, i);
g_autoptr(InstallSingleAppData) app_data = NULL;
const char *url;
const char *name;
g_autoptr(GVariant) icon_v = NULL;
GVariantBuilder opt_builder;
const int icon_sizes[] = {512, 192, 128, 1};
const char *missing_element = NULL;
/* only process this app if was created by this plugin */
if (!gs_app_has_management_plugin (app, GS_PLUGIN (self)))
continue;
/* This is a required flag for Epiphany. */
if (flags & GS_PLUGIN_INSTALL_APPS_FLAGS_NO_APPLY)
continue;
url = gs_app_get_url (app, AS_URL_KIND_HOMEPAGE);
if (url == NULL || *url == '\0')
missing_element = "url";
name = gs_app_get_name (app);
if (name == NULL || *name == '\0')
missing_element = "name";
for (guint j = 0; j < G_N_ELEMENTS (icon_sizes); j++) {
GIcon *icon = gs_app_get_icon_for_size (app, icon_sizes[j], 1, NULL);
if (icon != NULL)
icon_v = get_serialized_icon (app, icon);
if (icon_v != NULL)
break;
}
if (icon_v == NULL)
missing_element = "icon";
if (missing_element != NULL) {
g_autoptr(GsPluginEvent) event = NULL;
g_set_error (&local_error,
GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED,
"Can't install web app %s without %s",
gs_app_get_id (app), missing_element);
event = gs_plugin_event_new ("error", local_error,
"app", app,
NULL);
if (interactive)
gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE);
gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING);
gs_plugin_report_event (GS_PLUGIN (self), event);
g_clear_error (&local_error);
continue;
}
app_data = g_new0 (InstallSingleAppData, 1);
app_data->task = g_object_ref (task);
app_data->app = g_object_ref (app);
app_data->name = g_strdup (name);
app_data->url = g_strdup (url);
gs_app_set_state (app, GS_APP_STATE_INSTALLING);
gs_app_set_progress (app, 0);
/* First get a token from xdg-desktop-portal so Epiphany can do the
* installation without user confirmation
*/
data->n_tokens_requested++;
data->n_pending_ops++;
g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT);
g_dbus_proxy_call (self->launcher_portal_proxy,
"RequestInstallToken",
g_variant_new ("(sva{sv})",
name, icon_v, &opt_builder),
interactive ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION : G_DBUS_CALL_FLAGS_NONE,
-1,
cancellable,
install_request_token_cb,
g_steal_pointer (&app_data));
}
finish_install_apps_op (task, NULL);
}
static void
install_report_progress (GsPluginEpiphany *self,
InstallAppsData *data)
{
guint max_points, current_points;
if (data->progress_callback == NULL)
return;
/* We assign two progress points to each app: one for when its token has
* been received, and one for when its been installed. */
max_points = data->n_tokens_requested * 2;
current_points = data->n_tokens_received + data->n_apps_installed;
g_assert (current_points <= max_points);
g_assert (max_points > 0);
data->progress_callback (GS_PLUGIN (self),
current_points * 100 / max_points,
data->progress_user_data);
}
static void
install_request_token_cb (GObject *source_object,
GAsyncResult *result,
gpointer user_data)
{
GDBusProxy *launcher_portal_proxy = G_DBUS_PROXY (source_object);
g_autoptr(InstallSingleAppData) app_data = g_steal_pointer (&user_data);
GTask *task = app_data->task;
GsPluginEpiphany *self = g_task_get_source_object (task);
InstallAppsData *data = g_task_get_task_data (task);
GCancellable *cancellable = g_task_get_cancellable (task);
gboolean interactive = (data->flags & GS_PLUGIN_INSTALL_APPS_FLAGS_INTERACTIVE);
const char *token = NULL;
g_autoptr(GVariant) token_v = NULL;
g_autoptr(GError) local_error = NULL;
gs_app_set_progress (app_data->app, 50);
data->n_tokens_received++;
install_report_progress (self, data);
token_v = g_dbus_proxy_call_finish (launcher_portal_proxy, result, &local_error);
if (token_v == NULL) {
g_autoptr(GsPluginEvent) event = NULL;
gs_app_set_state_recover (app_data->app);
gs_epiphany_error_convert (&local_error);
event = gs_plugin_event_new ("error", local_error,
"app", app_data->app,
NULL);
if (interactive)
gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE);
gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING);
gs_plugin_report_event (GS_PLUGIN (self), event);
g_clear_error (&local_error);
finish_install_apps_op (task, g_steal_pointer (&local_error));
return;
}
/* Then pass the token to Epiphany which will use xdg-desktop-portal to
* complete the installation
*/
g_variant_get (token_v, "(&s)", &token);
gs_ephy_web_app_provider_call_install (self->epiphany_proxy,
app_data->url, app_data->name, token,
interactive ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION : G_DBUS_CALL_FLAGS_NONE,
-1 /* timeout */,
cancellable,
install_install_cb,
app_data /* steals ownership */);
app_data = NULL;
}
static void
install_install_cb (GObject *source_object,
GAsyncResult *result,
gpointer user_data)
{
GsEphyWebAppProvider *epiphany_proxy = GS_EPHY_WEB_APP_PROVIDER (source_object);
g_autoptr(InstallSingleAppData) app_data = g_steal_pointer (&user_data);
GTask *task = app_data->task;
GsPluginEpiphany *self = g_task_get_source_object (task);
InstallAppsData *data = g_task_get_task_data (task);
gboolean interactive = (data->flags & GS_PLUGIN_INSTALL_APPS_FLAGS_INTERACTIVE);
g_autofree char *installed_app_id = NULL;
g_autoptr(GError) local_error = NULL;
gs_app_set_progress (app_data->app, 100);
data->n_apps_installed++;
install_report_progress (self, data);
if (!gs_ephy_web_app_provider_call_install_finish (epiphany_proxy,
&installed_app_id,
result,
&local_error)) {
g_autoptr(GsPluginEvent) event = NULL;
gs_app_set_state_recover (app_data->app);
gs_epiphany_error_convert (&local_error);
event = gs_plugin_event_new ("error", local_error,
"app", app_data->app,
NULL);
if (interactive)
gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE);
gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING);
gs_plugin_report_event (GS_PLUGIN (self), event);
g_clear_error (&local_error);
finish_install_apps_op (task, g_steal_pointer (&local_error));
return;
}
/* Install complete! Update internal and app state. */
{
g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->installed_apps_mutex);
g_hash_table_insert (self->url_id_map, g_strdup (app_data->url),
g_strdup (installed_app_id));
}
gs_app_set_launchable (app_data->app, AS_LAUNCHABLE_KIND_DESKTOP_ID, installed_app_id);
gs_app_set_state (app_data->app, GS_APP_STATE_INSTALLED);
finish_install_apps_op (task, NULL);
}
/* @error is (transfer full) if non-%NULL */
static void
finish_install_apps_op (GTask *task,
GError *error)
{
InstallAppsData *data = g_task_get_task_data (task);
g_autoptr(GError) error_owned = g_steal_pointer (&error);
if (error_owned != NULL && data->saved_error == NULL)
data->saved_error = g_steal_pointer (&error_owned);
else if (error_owned != NULL)
g_debug ("Additional error while installing apps: %s", error_owned->message);
g_assert (data->n_pending_ops > 0);
data->n_pending_ops--;
if (data->n_pending_ops > 0)
return;
/* Get the results of the parallel ops. */
if (data->saved_error != NULL)
g_task_return_error (task, g_steal_pointer (&data->saved_error));
else
g_task_return_boolean (task, TRUE);
}
static gboolean
gs_plugin_epiphany_install_apps_finish (GsPlugin *plugin,
GAsyncResult *result,
GError **error)
{
return g_task_propagate_boolean (G_TASK (result), error);
}
typedef struct {
/* Input data. */
GsPluginUninstallAppsFlags flags;
GsPluginProgressCallback progress_callback;
gpointer progress_user_data;
/* In-progress data. */
guint n_pending_ops;
GError *saved_error; /* (owned) (nullable) */
/* For progress reporting */
guint n_uninstalls_started;
guint n_apps_uninstalled;
} UninstallAppsData;
static void
uninstall_apps_data_free (UninstallAppsData *data)
{
/* Error should have been propagated by now, and all pending ops completed. */
g_assert (data->saved_error == NULL);
g_assert (data->n_pending_ops == 0);
g_free (data);
}
G_DEFINE_AUTOPTR_CLEANUP_FUNC (UninstallAppsData, uninstall_apps_data_free)
typedef struct {
GTask *task; /* (owned) */
GsApp *app; /* (owned) */
} UninstallSingleAppData;
static void
uninstall_single_app_data_free (UninstallSingleAppData *data)
{
g_clear_object (&data->app);
g_clear_object (&data->task);
g_free (data);
}
G_DEFINE_AUTOPTR_CLEANUP_FUNC (UninstallSingleAppData, uninstall_single_app_data_free)
static void uninstall_cb (GObject *source_object,
GAsyncResult *result,
gpointer user_data);
static void finish_uninstall_apps_op (GTask *task,
GError *error);
static void
gs_plugin_epiphany_uninstall_apps_async (GsPlugin *plugin,
GsAppList *apps,
GsPluginUninstallAppsFlags flags,
GsPluginProgressCallback progress_callback,
gpointer progress_user_data,
GsPluginAppNeedsUserActionCallback app_needs_user_action_callback,
gpointer app_needs_user_action_data,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (plugin);
g_autoptr(GTask) task = NULL;
UninstallAppsData *data;
g_autoptr(UninstallAppsData) data_owned = NULL;
gboolean interactive = (flags & GS_PLUGIN_UNINSTALL_APPS_FLAGS_INTERACTIVE);
g_autoptr(GError) local_error = NULL;
task = g_task_new (self, cancellable, callback, user_data);
g_task_set_source_tag (task, gs_plugin_epiphany_uninstall_apps_async);
data = data_owned = g_new0 (UninstallAppsData, 1);
data->flags = flags;
data->progress_callback = progress_callback;
data->progress_user_data = progress_user_data;
g_task_set_task_data (task, g_steal_pointer (&data_owned), (GDestroyNotify) uninstall_apps_data_free);
/* Start a load of operations in parallel to uninstall the apps.
*
* When all uninstalls are finished for all apps, finish_uninstall_apps_op()
* will return success/error for the overall #GTask. */
data->n_pending_ops = 1;
data->n_uninstalls_started = 0;
for (guint i = 0; i < gs_app_list_length (apps); i++) {
GsApp *app = gs_app_list_index (apps, i);
g_autoptr(UninstallSingleAppData) app_data = NULL;
const char *installed_app_id;
/* only process this app if was created by this plugin */
if (!gs_app_has_management_plugin (app, GS_PLUGIN (self)))
continue;
installed_app_id = gs_app_get_launchable (app, AS_LAUNCHABLE_KIND_DESKTOP_ID);
if (installed_app_id == NULL) {
g_autoptr(GsPluginEvent) event = NULL;
g_set_error (&local_error,
GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED,
"Cant uninstall web app %s without installed app ID",
gs_app_get_id (app));
event = gs_plugin_event_new ("error", local_error,
"app", app,
NULL);
if (interactive)
gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE);
gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING);
gs_plugin_report_event (GS_PLUGIN (self), event);
g_clear_error (&local_error);
continue;
}
app_data = g_new0 (UninstallSingleAppData, 1);
app_data->task = g_object_ref (task);
app_data->app = g_object_ref (app);
gs_app_set_state (app, GS_APP_STATE_REMOVING);
gs_app_set_progress (app, 0);
data->n_uninstalls_started++;
data->n_pending_ops++;
gs_ephy_web_app_provider_call_uninstall (self->epiphany_proxy,
installed_app_id,
interactive ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION : G_DBUS_CALL_FLAGS_NONE,
-1 /* timeout */,
cancellable,
uninstall_cb,
g_steal_pointer (&app_data));
}
finish_uninstall_apps_op (task, NULL);
}
static void
uninstall_cb (GObject *source_object,
GAsyncResult *result,
gpointer user_data)
{
GsEphyWebAppProvider *epiphany_proxy = GS_EPHY_WEB_APP_PROVIDER (source_object);
g_autoptr(UninstallSingleAppData) app_data = g_steal_pointer (&user_data);
GTask *task = app_data->task;
GsPluginEpiphany *self = g_task_get_source_object (task);
UninstallAppsData *data = g_task_get_task_data (task);
gboolean interactive = (data->flags & GS_PLUGIN_UNINSTALL_APPS_FLAGS_INTERACTIVE);
const char *url;
g_autoptr(GError) local_error = NULL;
gs_app_set_progress (app_data->app, 100);
data->n_apps_uninstalled++;
/* Progress report. */
if (data->progress_callback != NULL) {
g_assert (data->n_uninstalls_started > 0);
data->progress_callback (GS_PLUGIN (self),
data->n_apps_uninstalled * 100 / data->n_uninstalls_started,
data->progress_user_data);
}
if (!gs_ephy_web_app_provider_call_uninstall_finish (epiphany_proxy,
result,
&local_error)) {
g_autoptr(GsPluginEvent) event = NULL;
gs_app_set_state_recover (app_data->app);
gs_epiphany_error_convert (&local_error);
event = gs_plugin_event_new ("error", local_error,
"app", app_data->app,
NULL);
if (interactive)
gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE);
gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING);
gs_plugin_report_event (GS_PLUGIN (self), event);
g_clear_error (&local_error);
finish_uninstall_apps_op (task, NULL);
return;
}
url = gs_app_get_launchable (app_data->app, AS_LAUNCHABLE_KIND_URL);
if (url != NULL && *url != '\0') {
g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->installed_apps_mutex);
g_hash_table_remove (self->url_id_map, url);
}
/* The app is not necessarily available; it may have been installed
* directly in Epiphany. So refine it to check.
*/
gs_app_set_state (app_data->app, GS_APP_STATE_UNKNOWN);
gs_epiphany_refine_app (self, app_data->app,
GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN |
GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION,
url);
gs_epiphany_refine_app_state (GS_PLUGIN (self), app_data->app);
finish_uninstall_apps_op (task, NULL);
}
/* @error is (transfer full) if non-%NULL */
static void
finish_uninstall_apps_op (GTask *task,
GError *error)
{
UninstallAppsData *data = g_task_get_task_data (task);
g_autoptr(GError) error_owned = g_steal_pointer (&error);
if (error_owned != NULL && data->saved_error == NULL)
data->saved_error = g_steal_pointer (&error_owned);
else if (error_owned != NULL)
g_debug ("Additional error while uninstalling apps: %s", error_owned->message);
g_assert (data->n_pending_ops > 0);
data->n_pending_ops--;
if (data->n_pending_ops > 0)
return;
/* Get the results of the parallel ops. */
if (data->saved_error != NULL)
g_task_return_error (task, g_steal_pointer (&data->saved_error));
else
g_task_return_boolean (task, TRUE);
}
static gboolean
gs_plugin_epiphany_uninstall_apps_finish (GsPlugin *plugin,
GAsyncResult *result,
GError **error)
{
return g_task_propagate_boolean (G_TASK (result), error);
}
static void
gs_plugin_epiphany_launch_async (GsPlugin *plugin,
GsApp *app,
GsPluginLaunchFlags flags,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
gs_plugin_app_launch_async (plugin, app, flags, cancellable, callback, user_data);
}
static gboolean
gs_plugin_epiphany_launch_finish (GsPlugin *plugin,
GAsyncResult *result,
GError **error)
{
return gs_plugin_app_launch_finish (plugin, result, error);
}
static void
gs_plugin_epiphany_class_init (GsPluginEpiphanyClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass);
object_class->dispose = gs_plugin_epiphany_dispose;
object_class->finalize = gs_plugin_epiphany_finalize;
plugin_class->setup_async = gs_plugin_epiphany_setup_async;
plugin_class->setup_finish = gs_plugin_epiphany_setup_finish;
plugin_class->shutdown_async = gs_plugin_epiphany_shutdown_async;
plugin_class->shutdown_finish = gs_plugin_epiphany_shutdown_finish;
plugin_class->refine_async = gs_plugin_epiphany_refine_async;
plugin_class->refine_finish = gs_plugin_epiphany_refine_finish;
plugin_class->list_apps_async = gs_plugin_epiphany_list_apps_async;
plugin_class->list_apps_finish = gs_plugin_epiphany_list_apps_finish;
plugin_class->install_apps_async = gs_plugin_epiphany_install_apps_async;
plugin_class->install_apps_finish = gs_plugin_epiphany_install_apps_finish;
plugin_class->uninstall_apps_async = gs_plugin_epiphany_uninstall_apps_async;
plugin_class->uninstall_apps_finish = gs_plugin_epiphany_uninstall_apps_finish;
plugin_class->launch_async = gs_plugin_epiphany_launch_async;
plugin_class->launch_finish = gs_plugin_epiphany_launch_finish;
}
GType
gs_plugin_query_type (void)
{
return GS_TYPE_PLUGIN_EPIPHANY;
}