From f3eb807464e06eb68b2901e553a21e31620aba53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= Date: Tue, 17 May 2022 03:33:21 +0200 Subject: notification: Use Desktop Portal notification when running in sandbox When an application is running from a snap or a flatpak, libnotify should just work as a FDO Portal Notification wrapper. As per this, make the library to try use the portal API in sandboxed applications, by emulating the needed missing pieces. Not everything can be achieved 1:1, but it's just safer to use in such contexts. --- libnotify/internal.h | 8 + libnotify/notification.c | 386 +++++++++++++++++++++++++++++++++++++++++++++++ libnotify/notify.c | 159 +++++++++++++++++++ 3 files changed, 553 insertions(+) diff --git a/libnotify/internal.h b/libnotify/internal.h index 35683d6..4d7ceb0 100644 --- a/libnotify/internal.h +++ b/libnotify/internal.h @@ -26,6 +26,10 @@ #define NOTIFY_DBUS_CORE_INTERFACE "org.freedesktop.Notifications" #define NOTIFY_DBUS_CORE_OBJECT "/org/freedesktop/Notifications" +#define NOTIFY_PORTAL_DBUS_NAME "org.freedesktop.portal.Desktop" +#define NOTIFY_PORTAL_DBUS_CORE_INTERFACE "org.freedesktop.portal.Notification" +#define NOTIFY_PORTAL_DBUS_CORE_OBJECT "/org/freedesktop/portal/desktop" + G_BEGIN_DECLS GDBusProxy * _notify_get_proxy (GError **error); @@ -40,6 +44,10 @@ const char * _notify_get_snap_name (void); const char * _notify_get_snap_path (void); const char * _notify_get_snap_app (void); +const char * _notify_get_flatpak_app (void); + +gboolean _notify_uses_portal_notifications (void); + G_END_DECLS #endif /* _LIBNOTIFY_INTERNAL_H_ */ diff --git a/libnotify/notification.c b/libnotify/notification.c index f20414d..de563d2 100644 --- a/libnotify/notification.c +++ b/libnotify/notification.c @@ -53,6 +53,7 @@ static void notify_notification_class_init (NotifyNotificationClass *klass); static void notify_notification_init (NotifyNotification *sp); static void notify_notification_finalize (GObject *object); +static void notify_notification_dispose (GObject *object); typedef struct { @@ -72,6 +73,7 @@ struct _NotifyNotificationPrivate /* NULL to use icon data. Anything else to have server lookup icon */ char *icon_name; + GdkPixbuf *icon_pixbuf; /* * -1 = use server default @@ -79,6 +81,7 @@ struct _NotifyNotificationPrivate * > 0 = Number of milliseconds before we timeout */ gint timeout; + guint portal_timeout_id; GSList *actions; GHashTable *action_map; @@ -150,6 +153,7 @@ notify_notification_class_init (NotifyNotificationClass *klass) object_class->constructor = notify_notification_constructor; object_class->get_property = notify_notification_get_property; object_class->set_property = notify_notification_set_property; + object_class->dispose = notify_notification_dispose; object_class->finalize = notify_notification_finalize; /** @@ -369,6 +373,22 @@ notify_notification_init (NotifyNotification *obj) (GDestroyNotify) destroy_pair); } +static void +notify_notification_dispose (GObject *object) +{ + NotifyNotification *obj = NOTIFY_NOTIFICATION (object); + NotifyNotificationPrivate *priv = obj->priv; + + if (priv->portal_timeout_id) { + g_source_remove (priv->portal_timeout_id); + priv->portal_timeout_id = 0; + } + + g_clear_object (&priv->icon_pixbuf); + + G_OBJECT_CLASS (parent_class)->dispose (object); +} + static void notify_notification_finalize (GObject *object) { @@ -405,6 +425,18 @@ notify_notification_finalize (GObject *object) G_OBJECT_CLASS (parent_class)->finalize (object); } +static gboolean +maybe_warn_portal_unsupported_feature (const char *feature_name) +{ + if (!_notify_uses_portal_notifications ()) { + return FALSE; + } + + g_message ("%s is not available when using Portal Notifications", + feature_name); + return TRUE; +} + /** * notify_notification_new: * @summary: The required summary text. @@ -592,6 +624,33 @@ notify_notification_update (NotifyNotification *notification, return TRUE; } +static char * +get_portal_notification_id (NotifyNotification *notification) +{ + char *app_id; + char *notification_id; + + g_assert (_notify_uses_portal_notifications ()); + + if (_notify_get_snap_name ()) { + app_id = g_strdup_printf ("snap.%s_%s", + _notify_get_snap_name (), + _notify_get_snap_app ()); + } else { + app_id = g_strdup_printf ("flatpak.%s", + _notify_get_flatpak_app ()); + } + + notification_id = g_strdup_printf ("libnotify-%s-%s-%u", + app_id, + notify_get_app_name (), + notification->priv->id); + + g_free (app_id); + + return notification_id; +} + static gboolean activate_action (NotifyNotification *notification, const gchar *action) @@ -646,8 +705,12 @@ proxy_g_signal_cb (GDBusProxy *proxy, GVariant *parameters, NotifyNotification *notification) { + const char *interface; + g_return_if_fail (NOTIFY_IS_NOTIFICATION (notification)); + interface = g_dbus_proxy_get_interface_name (proxy); + if (g_strcmp0 (signal_name, "NotificationClosed") == 0 && g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(uu)"))) { guint32 id, reason; @@ -658,6 +721,7 @@ proxy_g_signal_cb (GDBusProxy *proxy, close_notification (notification, reason); } else if (g_strcmp0 (signal_name, "ActionInvoked") == 0 && + g_str_equal (interface, NOTIFY_DBUS_CORE_INTERFACE) && g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(us)"))) { guint32 id; const char *action; @@ -683,9 +747,306 @@ proxy_g_signal_cb (GDBusProxy *proxy, g_free (notification->priv->activation_token); notification->priv->activation_token = g_strdup (activation_token); + } else if (g_str_equal (signal_name, "ActionInvoked") && + g_str_equal (interface, NOTIFY_PORTAL_DBUS_CORE_INTERFACE) && + g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(ssav)"))) { + char *notification_id; + const char *id; + const char *action; + GVariant *parameter; + + g_variant_get (parameters, "(&s&s@av)", &id, &action, ¶meter); + g_variant_unref (parameter); + + notification_id = get_portal_notification_id (notification); + + if (!g_str_equal (notification_id, id)) { + g_free (notification_id); + return; + } + + if (!activate_action (notification, action) && + g_str_equal (action, "default-action") && + !_notify_get_snap_app ()) { + g_warning ("Received unknown action %s", action); + } + + close_notification (notification, NOTIFY_CLOSED_REASON_DISMISSED); + + g_free (notification_id); + } else { + g_debug ("Unhandled signal '%s.%s'", interface, signal_name); } } +static gboolean +remove_portal_notification (GDBusProxy *proxy, + NotifyNotification *notification, + NotifyClosedReason reason, + GError **error) +{ + GVariant *ret; + gchar *notification_id; + + if (notification->priv->portal_timeout_id) { + g_source_remove (notification->priv->portal_timeout_id); + notification->priv->portal_timeout_id = 0; + } + + notification_id = get_portal_notification_id (notification); + + ret = g_dbus_proxy_call_sync (proxy, + "RemoveNotification", + g_variant_new ("(s)", notification_id), + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + error); + + g_free (notification_id); + + if (!ret) { + return FALSE; + } + + close_notification (notification, reason); + + g_variant_unref (ret); + + return TRUE; +} + +static gboolean +on_portal_timeout (gpointer data) +{ + NotifyNotification *notification = data; + GDBusProxy *proxy; + + notification->priv->portal_timeout_id = 0; + + proxy = _notify_get_proxy (NULL); + if (proxy == NULL) { + return FALSE; + } + + remove_portal_notification (proxy, notification, + NOTIFY_CLOSED_REASON_EXPIRED, NULL); + return FALSE; +} + +static GIcon * +get_notification_gicon (NotifyNotification *notification, + GError **error) +{ + NotifyNotificationPrivate *priv = notification->priv; + GFileInputStream *input; + GFile *file = NULL; + GIcon *gicon = NULL; + + if (priv->icon_pixbuf) { + return G_ICON (g_object_ref (priv->icon_pixbuf)); + } + + if (!priv->icon_name) { + return NULL; + } + + if (strstr (priv->icon_name, "://")) { + file = g_file_new_for_uri (priv->icon_name); + } else if (g_file_test (priv->icon_name, G_FILE_TEST_EXISTS)) { + file = g_file_new_for_path (priv->icon_name); + } else { + gicon = g_themed_icon_new (priv->icon_name); + } + + if (!file) { + return gicon; + } + + input = g_file_read (file, NULL, error); + + if (input) { + GByteArray *bytes_array = g_byte_array_new (); + guint8 buf[1024]; + + while (TRUE) { + gssize read; + + read = g_input_stream_read (G_INPUT_STREAM (input), + buf, + G_N_ELEMENTS (buf), + NULL, NULL); + + if (read > 0) { + g_byte_array_append (bytes_array, buf, read); + } else { + if (read < 0) { + g_byte_array_unref (bytes_array); + bytes_array = NULL; + } + + break; + } + } + + if (bytes_array && bytes_array->len) { + GBytes *bytes; + + bytes = g_byte_array_free_to_bytes (bytes_array); + bytes_array = NULL; + + gicon = g_bytes_icon_new (bytes); + } else if (bytes_array) { + g_byte_array_unref (bytes_array); + } + } + + g_clear_object (&input); + g_clear_object (&file); + + return gicon; +} + +static gboolean +add_portal_notification (GDBusProxy *proxy, + NotifyNotification *notification, + GError **error) +{ + GIcon *icon; + GVariant *urgency; + GVariant *ret; + GVariantBuilder builder; + NotifyNotificationPrivate *priv = notification->priv; + GError *local_error = NULL; + static guint32 portal_notification_count = 0; + char *notification_id; + + g_variant_builder_init (&builder, G_VARIANT_TYPE_VARDICT); + + g_variant_builder_add (&builder, "{sv}", "title", + g_variant_new_string (priv->summary ? priv->summary : "")); + g_variant_builder_add (&builder, "{sv}", "body", + g_variant_new_string (priv->body ? priv->body : "")); + + if (g_hash_table_lookup (priv->action_map, "default")) { + g_variant_builder_add (&builder, "{sv}", "default-action", + g_variant_new_string ("default")); + } else if (g_hash_table_lookup (priv->action_map, "DEFAULT")) { + g_variant_builder_add (&builder, "{sv}", "default-action", + g_variant_new_string ("DEFAULT")); + } else if (_notify_get_snap_app ()) { + /* In the snap case we may need to ensure that a default-action + * is set to ensure that we will use the FDO notification daemon + * and won't fallback to GTK one, as app-id won't match. + * See: https://github.com/flatpak/xdg-desktop-portal/issues/769 + */ + g_variant_builder_add (&builder, "{sv}", "default-action", + g_variant_new_string ("snap-fake-default-action")); + } + + if (priv->has_nondefault_actions) { + GVariantBuilder buttons; + GSList *l; + + g_variant_builder_init (&buttons, G_VARIANT_TYPE ("aa{sv}")); + + for (l = priv->actions; l && l->next; l = l->next->next) { + GVariantBuilder button; + const char *action; + const char *label; + + g_variant_builder_init (&button, G_VARIANT_TYPE_VARDICT); + + action = l->data; + label = l->next->data; + + g_variant_builder_add (&button, "{sv}", "action", + g_variant_new_string (action)); + g_variant_builder_add (&button, "{sv}", "label", + g_variant_new_string (label)); + + g_variant_builder_add (&buttons, "@a{sv}", + g_variant_builder_end (&button)); + } + + g_variant_builder_add (&builder, "{sv}", "buttons", + g_variant_builder_end (&buttons)); + } + + urgency = g_hash_table_lookup (notification->priv->hints, "urgency"); + if (urgency) { + switch (g_variant_get_byte (urgency)) { + case NOTIFY_URGENCY_LOW: + g_variant_builder_add (&builder, "{sv}", "priority", + g_variant_new_string ("low")); + break; + case NOTIFY_URGENCY_NORMAL: + g_variant_builder_add (&builder, "{sv}", "priority", + g_variant_new_string ("normal")); + break; + case NOTIFY_URGENCY_CRITICAL: + g_variant_builder_add (&builder, "{sv}", "priority", + g_variant_new_string ("urgent")); + break; + default: + g_warn_if_reached (); + } + } + + icon = get_notification_gicon (notification, &local_error); + if (icon) { + GVariant *serialized_icon = g_icon_serialize (icon); + + g_variant_builder_add (&builder, "{sv}", "icon", + serialized_icon); + g_variant_unref (serialized_icon); + g_clear_object (&icon); + } else if (local_error) { + g_propagate_error (error, local_error); + return FALSE; + } + + if (!priv->id) { + priv->id = ++portal_notification_count; + } else if (priv->closed_reason == NOTIFY_CLOSED_REASON_UNSET) { + remove_portal_notification (proxy, notification, + NOTIFY_CLOSED_REASON_UNSET, NULL); + } + + notification_id = get_portal_notification_id (notification); + + ret = g_dbus_proxy_call_sync (proxy, + "AddNotification", + g_variant_new ("(s@a{sv})", + notification_id, + g_variant_builder_end (&builder)), + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + error); + + if (priv->portal_timeout_id) { + g_source_remove (priv->portal_timeout_id); + priv->portal_timeout_id = 0; + } + + g_free (notification_id); + + if (!ret) { + return FALSE; + } + + if (priv->timeout > 0) { + priv->portal_timeout_id = g_timeout_add (priv->timeout, + on_portal_timeout, + notification); + } + + g_variant_unref (ret); + + return TRUE; +} + /** * notify_notification_show: * @notification: The notification. @@ -731,6 +1092,10 @@ notify_notification_show (NotifyNotification *notification, notification); } + if (_notify_uses_portal_notifications ()) { + return add_portal_notification (proxy, notification, error); + } + g_variant_builder_init (&actions_builder, G_VARIANT_TYPE ("as")); for (l = priv->actions; l != NULL; l = l->next) { g_variant_builder_add (&actions_builder, "s", l->data); @@ -854,6 +1219,10 @@ notify_notification_set_category (NotifyNotification *notification, g_return_if_fail (notification != NULL); g_return_if_fail (NOTIFY_IS_NOTIFICATION (notification)); + if (maybe_warn_portal_unsupported_feature ("Category")) { + return; + } + if (category != NULL && category[0] != '\0') { notify_notification_set_hint_string (notification, "category", @@ -931,11 +1300,18 @@ notify_notification_set_image_from_pixbuf (NotifyNotification *notification, hint_name = "icon_data"; } + g_clear_object (¬ification->priv->icon_pixbuf); + if (pixbuf == NULL) { notify_notification_set_hint (notification, hint_name, NULL); return; } + if (_notify_uses_portal_notifications ()) { + notification->priv->icon_pixbuf = g_object_ref (pixbuf); + return; + } + g_object_get (pixbuf, "width", &width, "height", &height, @@ -1059,6 +1435,10 @@ notify_notification_set_app_name (NotifyNotification *notification, { g_return_if_fail (NOTIFY_IS_NOTIFICATION (notification)); + if (maybe_warn_portal_unsupported_feature ("App Name")) { + return; + } + g_free (notification->priv->app_name); notification->priv->app_name = g_strdup (app_name); @@ -1357,6 +1737,12 @@ notify_notification_close (NotifyNotification *notification, return FALSE; } + if (_notify_uses_portal_notifications ()) { + return remove_portal_notification (proxy, notification, + NOTIFY_CLOSED_REASON_API_REQUEST, + error); + } + /* FIXME: make this nonblocking! */ result = g_dbus_proxy_call_sync (proxy, "CloseNotification", diff --git a/libnotify/notify.c b/libnotify/notify.c index e6214c7..92b4010 100644 --- a/libnotify/notify.c +++ b/libnotify/notify.c @@ -45,10 +45,12 @@ static gboolean _initted = FALSE; static char *_app_name = NULL; static char *_snap_name = NULL; static char *_snap_app = NULL; +static char *_flatpak_app = NULL; static GDBusProxy *_proxy = NULL; static GList *_active_notifications = NULL; static int _spec_version_major = 0; static int _spec_version_minor = 0; +static int _portal_version = 0; gboolean _notify_check_spec_version (int major, @@ -76,6 +78,26 @@ _notify_get_server_info (char **ret_name, return FALSE; } + if (_notify_uses_portal_notifications ()) { + if (ret_name) { + *ret_name = g_strdup ("Portal Notification"); + } + + if (ret_vendor) { + *ret_vendor = g_strdup ("Freedesktop"); + } + + if (ret_version) { + *ret_version = g_strdup_printf ("%u", _portal_version); + } + + if (ret_spec_version) { + *ret_spec_version = g_strdup ("1.2"); + } + + return TRUE; + } + result = g_dbus_proxy_call_sync (proxy, "GetServerInformation", g_variant_new ("()"), @@ -327,6 +349,81 @@ _notify_get_snap_app (void) return _snap_app; } +const char * +_notify_get_flatpak_app (void) +{ + static gsize flatpak_app_set = FALSE; + + if (g_once_init_enter (&flatpak_app_set)) { + GKeyFile *info = g_key_file_new (); + + if (g_key_file_load_from_file (info, "/.flatpak-info", + G_KEY_FILE_NONE, NULL)) { + const char *group = "Application"; + + if (g_key_file_has_group (info, "Runtime")) { + group = "Runtime"; + } + + _flatpak_app = g_key_file_get_string (info, group, + "name", NULL); + } + + g_key_file_free (info); + g_once_init_leave (&flatpak_app_set, TRUE); + } + + return _flatpak_app; +} + +static gboolean +_notify_is_running_under_flatpak (void) +{ + return !!_notify_get_flatpak_app (); +} + +static gboolean +_notify_is_running_under_snap (void) +{ + return !!_notify_get_snap_app (); +} + +static gboolean +_notify_is_running_in_sandbox (void) +{ + static gsize use_portal = 0; + enum { + IGNORE_PORTAL = 1, + TRY_USE_PORTAL = 2, + FORCE_PORTAL = 3 + }; + + if (g_once_init_enter (&use_portal)) { + if (G_UNLIKELY (g_getenv ("NOTIFY_IGNORE_PORTAL"))) { + g_once_init_leave (&use_portal, IGNORE_PORTAL); + } else if (G_UNLIKELY (g_getenv ("NOTIFY_FORCE_PORTAL"))) { + g_once_init_leave (&use_portal, FORCE_PORTAL); + } else { + g_once_init_leave (&use_portal, TRY_USE_PORTAL); + } + } + + if (use_portal == IGNORE_PORTAL) { + return FALSE; + } + + return use_portal == FORCE_PORTAL || + _notify_is_running_under_flatpak () || + _notify_is_running_under_snap (); +} + +gboolean +_notify_uses_portal_notifications (void) +{ + return _portal_version != 0; +} + + /** * notify_get_app_name: * @@ -382,6 +479,9 @@ notify_uninit (void) g_free (_snap_app); _snap_app = NULL; + g_free (_flatpak_app); + _flatpak_app = NULL; + _initted = FALSE; } @@ -398,6 +498,46 @@ notify_is_initted (void) return _initted; } +GDBusProxy * +_get_portal_proxy (GError **error) +{ + GError *local_error = NULL; + GDBusProxy *proxy; + GVariant *res; + + proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + NOTIFY_PORTAL_DBUS_NAME, + NOTIFY_PORTAL_DBUS_CORE_OBJECT, + NOTIFY_PORTAL_DBUS_CORE_INTERFACE, + NULL, + &local_error); + + if (proxy == NULL) { + g_debug ("Failed to get portal proxy: %s", local_error->message); + g_clear_error (&local_error); + + return NULL; + } + + res = g_dbus_proxy_get_cached_property (proxy, "version"); + if (!res) { + g_object_unref (proxy); + return NULL; + } + + _portal_version = g_variant_get_uint32 (res); + g_assert (_portal_version > 0); + + g_warning ("Running in confined mode, using Portal notifications. " + "Some features and hints won't be supported"); + + g_variant_unref (res); + + return proxy; +} + /* * _notify_get_proxy: * @error: (allow-none): a location to store a #GError, or %NULL @@ -413,6 +553,14 @@ _notify_get_proxy (GError **error) if (_proxy != NULL) return _proxy; + if (_notify_is_running_in_sandbox ()) { + _proxy = _get_portal_proxy (error); + + if (_proxy != NULL) { + goto out; + } + } + _proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES, NULL, @@ -421,6 +569,8 @@ _notify_get_proxy (GError **error) NOTIFY_DBUS_CORE_INTERFACE, NULL, error); + +out: if (_proxy == NULL) { return NULL; } @@ -458,6 +608,15 @@ notify_get_server_caps (void) return NULL; } + if (_notify_uses_portal_notifications ()) { + list = g_list_prepend (list, g_strdup ("actions")); + list = g_list_prepend (list, g_strdup ("body")); + list = g_list_prepend (list, g_strdup ("body-images")); + list = g_list_prepend (list, g_strdup ("icon-static")); + + return list; + } + result = g_dbus_proxy_call_sync (proxy, "GetCapabilities", g_variant_new ("()"), -- cgit v1.2.1