/* * Copyright © 2013 Lars Uebernickel * * SPDX-License-Identifier: LGPL-2.1-or-later * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General * Public License along with this library; if not, see . * * Authors: Lars Uebernickel */ #include "config.h" #include "gnotificationbackend.h" #include "gapplication.h" #include "giomodule-priv.h" #include "gnotification-private.h" #include "gdbusconnection.h" #include "gdbusnamewatching.h" #include "gactiongroup.h" #include "gaction.h" #include "gthemedicon.h" #include "gfileicon.h" #include "gfile.h" #include "gdbusutils.h" #define G_TYPE_FDO_NOTIFICATION_BACKEND (g_fdo_notification_backend_get_type ()) #define G_FDO_NOTIFICATION_BACKEND(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), G_TYPE_FDO_NOTIFICATION_BACKEND, GFdoNotificationBackend)) typedef struct _GFdoNotificationBackend GFdoNotificationBackend; typedef GNotificationBackendClass GFdoNotificationBackendClass; struct _GFdoNotificationBackend { GNotificationBackend parent; guint bus_name_id; guint notify_subscription; GSList *notifications; }; GType g_fdo_notification_backend_get_type (void); G_DEFINE_TYPE_WITH_CODE (GFdoNotificationBackend, g_fdo_notification_backend, G_TYPE_NOTIFICATION_BACKEND, _g_io_modules_ensure_extension_points_registered (); g_io_extension_point_implement (G_NOTIFICATION_BACKEND_EXTENSION_POINT_NAME, g_define_type_id, "freedesktop", 0)) typedef struct { GFdoNotificationBackend *backend; gchar *id; guint32 notify_id; gchar *default_action; /* (nullable) (owned) */ GVariant *default_action_target; /* (nullable) (owned), not floating */ } FreedesktopNotification; static void freedesktop_notification_free (gpointer data) { FreedesktopNotification *n = data; g_object_unref (n->backend); g_free (n->id); g_free (n->default_action); if (n->default_action_target) g_variant_unref (n->default_action_target); g_slice_free (FreedesktopNotification, n); } static FreedesktopNotification * freedesktop_notification_new (GFdoNotificationBackend *backend, const gchar *id, GNotification *notification) { FreedesktopNotification *n; n = g_slice_new0 (FreedesktopNotification); n->backend = g_object_ref (backend); n->id = g_strdup (id); n->notify_id = 0; g_notification_get_default_action (notification, &n->default_action, &n->default_action_target); return n; } static FreedesktopNotification * g_fdo_notification_backend_find_notification (GFdoNotificationBackend *backend, const gchar *id) { GSList *it; for (it = backend->notifications; it != NULL; it = it->next) { FreedesktopNotification *n = it->data; if (g_str_equal (n->id, id)) return n; } return NULL; } static FreedesktopNotification * g_fdo_notification_backend_find_notification_by_notify_id (GFdoNotificationBackend *backend, guint32 id) { GSList *it; for (it = backend->notifications; it != NULL; it = it->next) { FreedesktopNotification *n = it->data; if (n->notify_id == id) return n; } return NULL; } static gboolean activate_action (GFdoNotificationBackend *backend, const gchar *name, GVariant *parameter) { GNotificationBackend *g_backend = G_NOTIFICATION_BACKEND (backend); /* Callers should not provide a floating variant here */ g_assert (parameter == NULL || !g_variant_is_floating (parameter)); if (name != NULL && g_str_has_prefix (name, "app.")) { const GVariantType *parameter_type = NULL; const gchar *action_name = name + strlen ("app."); /* @name and @parameter come as untrusted input over D-Bus, so validate them first */ if (g_action_group_query_action (G_ACTION_GROUP (g_backend->application), action_name, NULL, ¶meter_type, NULL, NULL, NULL) && ((parameter_type == NULL && parameter == NULL) || (parameter_type != NULL && parameter != NULL && g_variant_is_of_type (parameter, parameter_type)))) { g_action_group_activate_action (G_ACTION_GROUP (g_backend->application), action_name, parameter); return TRUE; } } else if (name == NULL) { g_application_activate (g_backend->application); return TRUE; } return FALSE; } static void notify_signal (GDBusConnection *connection, const gchar *sender_name, const gchar *object_path, const gchar *interface_name, const gchar *signal_name, GVariant *parameters, gpointer user_data) { GFdoNotificationBackend *backend = user_data; guint32 id = 0; const gchar *action = NULL; FreedesktopNotification *n; gboolean notification_closed = TRUE; if (g_str_equal (signal_name, "NotificationClosed") && g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(uu)"))) { g_variant_get (parameters, "(uu)", &id, NULL); } else if (g_str_equal (signal_name, "ActionInvoked") && g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(us)"))) { g_variant_get (parameters, "(u&s)", &id, &action); } else return; n = g_fdo_notification_backend_find_notification_by_notify_id (backend, id); if (n == NULL) return; if (action) { if (g_str_equal (action, "default")) { if (!activate_action (backend, n->default_action, n->default_action_target)) notification_closed = FALSE; } else { gchar *name = NULL; GVariant *target = NULL; if (!g_action_parse_detailed_name (action, &name, &target, NULL) || !activate_action (backend, name, target)) notification_closed = FALSE; g_free (name); g_clear_pointer (&target, g_variant_unref); } } /* Remove the notification, as it’s either been explicitly closed * (`NotificationClosed` signal) or has been closed as a result of activating * an action successfully. GLib doesn’t currently support the `resident` hint * on notifications which would allow them to stay around after having an * action invoked on them (see * https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html#idm45877717456448) * * First, get the notification again in case the action redrew it */ if (notification_closed) { n = g_fdo_notification_backend_find_notification_by_notify_id (backend, id); if (n != NULL) { backend->notifications = g_slist_remove (backend->notifications, n); freedesktop_notification_free (n); } } } static void name_vanished_handler_cb (GDBusConnection *connection, const gchar *name, gpointer user_data) { GFdoNotificationBackend *backend = user_data; if (backend->notifications) { g_slist_free_full (backend->notifications, freedesktop_notification_free); backend->notifications = NULL; } } /* Converts a GNotificationPriority to an urgency level as defined by * the freedesktop spec (0: low, 1: normal, 2: critical). */ static guchar urgency_from_priority (GNotificationPriority priority) { switch (priority) { case G_NOTIFICATION_PRIORITY_LOW: return 0; default: case G_NOTIFICATION_PRIORITY_NORMAL: case G_NOTIFICATION_PRIORITY_HIGH: return 1; case G_NOTIFICATION_PRIORITY_URGENT: return 2; } } static void call_notify (GDBusConnection *con, GApplication *app, guint32 replace_id, GNotification *notification, GAsyncReadyCallback callback, gpointer user_data) { GVariantBuilder action_builder; guint n_buttons; guint i; GVariantBuilder hints_builder; GIcon *icon; GVariant *parameters; const gchar *app_name; const gchar *body; guchar urgency; g_variant_builder_init_static (&action_builder, G_VARIANT_TYPE_STRING_ARRAY); if (g_notification_get_default_action (notification, NULL, NULL)) { g_variant_builder_add (&action_builder, "s", "default"); g_variant_builder_add (&action_builder, "s", ""); } n_buttons = g_notification_get_n_buttons (notification); for (i = 0; i < n_buttons; i++) { gchar *label; gchar *action; GVariant *target; gchar *detailed_name; g_notification_get_button (notification, i, &label, &action, &target); detailed_name = g_action_print_detailed_name (action, target); /* Actions named 'default' collide with libnotify's naming of the * default action. Rewriting them to something unique is enough, * because those actions can never be activated (they aren't * prefixed with 'app.'). */ if (g_str_equal (detailed_name, "default")) { g_free (detailed_name); detailed_name = g_dbus_generate_guid (); } g_variant_builder_add_value (&action_builder, g_variant_new_take_string (detailed_name)); g_variant_builder_add_value (&action_builder, g_variant_new_take_string (label)); g_free (action); if (target) g_variant_unref (target); } g_variant_builder_init_static (&hints_builder, G_VARIANT_TYPE ("a{sv}")); g_variant_builder_add (&hints_builder, "{sv}", "desktop-entry", g_variant_new_string (g_application_get_application_id (app))); urgency = urgency_from_priority (g_notification_get_priority (notification)); g_variant_builder_add (&hints_builder, "{sv}", "urgency", g_variant_new_byte (urgency)); if (g_notification_get_category (notification)) { g_variant_builder_add (&hints_builder, "{sv}", "category", g_variant_new_string (g_notification_get_category (notification))); } icon = g_notification_get_icon (notification); if (icon != NULL) { if (G_IS_FILE_ICON (icon)) { GFile *file; file = g_file_icon_get_file (G_FILE_ICON (icon)); g_variant_builder_add (&hints_builder, "{sv}", "image-path", g_variant_new_take_string (g_file_get_path (file))); } else if (G_IS_THEMED_ICON (icon)) { const gchar* const* icon_names = g_themed_icon_get_names(G_THEMED_ICON (icon)); /* Take first name from GThemedIcon */ g_variant_builder_add (&hints_builder, "{sv}", "image-path", g_variant_new_string (icon_names[0])); } } app_name = g_get_application_name (); body = g_notification_get_body (notification); parameters = g_variant_new ("(susssasa{sv}i)", app_name ? app_name : "", replace_id, "", /* app icon */ g_notification_get_title (notification), body ? body : "", &action_builder, &hints_builder, -1); /* expire_timeout */ g_dbus_connection_call (con, "org.freedesktop.Notifications", "/org/freedesktop/Notifications", "org.freedesktop.Notifications", "Notify", parameters, G_VARIANT_TYPE ("(u)"), G_DBUS_CALL_FLAGS_NONE, -1, NULL, callback, user_data); } static void notification_sent (GObject *source_object, GAsyncResult *result, gpointer user_data) { FreedesktopNotification *n = user_data; GVariant *val; GError *error = NULL; static gboolean warning_printed = FALSE; val = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source_object), result, &error); if (val) { GFdoNotificationBackend *backend = n->backend; FreedesktopNotification *match; g_variant_get (val, "(u)", &n->notify_id); g_variant_unref (val); match = g_fdo_notification_backend_find_notification_by_notify_id (backend, n->notify_id); if (match != NULL) { backend->notifications = g_slist_remove (backend->notifications, match); freedesktop_notification_free (match); } backend->notifications = g_slist_prepend (backend->notifications, n); } else { if (!warning_printed) { g_warning ("unable to send notifications through org.freedesktop.Notifications: %s", error->message); warning_printed = TRUE; } freedesktop_notification_free (n); g_error_free (error); } } static void g_fdo_notification_backend_dispose (GObject *object) { GFdoNotificationBackend *backend = G_FDO_NOTIFICATION_BACKEND (object); if (backend->bus_name_id) { g_bus_unwatch_name (backend->bus_name_id); backend->bus_name_id = 0; } if (backend->notify_subscription) { GDBusConnection *session_bus; session_bus = G_NOTIFICATION_BACKEND (backend)->dbus_connection; g_dbus_connection_signal_unsubscribe (session_bus, backend->notify_subscription); backend->notify_subscription = 0; } if (backend->notifications) { g_slist_free_full (backend->notifications, freedesktop_notification_free); backend->notifications = NULL; } G_OBJECT_CLASS (g_fdo_notification_backend_parent_class)->dispose (object); } static gboolean g_fdo_notification_backend_is_supported (void) { /* This is the fallback backend with the lowest priority. To avoid an * unnecessary synchronous dbus call to check for * org.freedesktop.Notifications, this function always succeeds. A * warning will be printed when sending the first notification fails. */ return TRUE; } static void g_fdo_notification_backend_send_notification (GNotificationBackend *backend, const gchar *id, GNotification *notification) { GFdoNotificationBackend *self = G_FDO_NOTIFICATION_BACKEND (backend); FreedesktopNotification *n, *tmp; if (self->bus_name_id == 0) { self->bus_name_id = g_bus_watch_name_on_connection (backend->dbus_connection, "org.freedesktop.Notifications", G_BUS_NAME_WATCHER_FLAGS_NONE, NULL, name_vanished_handler_cb, backend, NULL); } if (self->notify_subscription == 0) { self->notify_subscription = g_dbus_connection_signal_subscribe (backend->dbus_connection, "org.freedesktop.Notifications", "org.freedesktop.Notifications", NULL, "/org/freedesktop/Notifications", NULL, G_DBUS_SIGNAL_FLAGS_NONE, notify_signal, backend, NULL); } n = freedesktop_notification_new (self, id, notification); tmp = g_fdo_notification_backend_find_notification (self, id); if (tmp) n->notify_id = tmp->notify_id; call_notify (backend->dbus_connection, backend->application, n->notify_id, notification, notification_sent, n); } static void g_fdo_notification_backend_withdraw_notification (GNotificationBackend *backend, const gchar *id) { GFdoNotificationBackend *self = G_FDO_NOTIFICATION_BACKEND (backend); FreedesktopNotification *n; n = g_fdo_notification_backend_find_notification (self, id); if (n) { if (n->notify_id > 0) { g_dbus_connection_call (backend->dbus_connection, "org.freedesktop.Notifications", "/org/freedesktop/Notifications", "org.freedesktop.Notifications", "CloseNotification", g_variant_new ("(u)", n->notify_id), NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); } self->notifications = g_slist_remove (self->notifications, n); freedesktop_notification_free (n); } } static void g_fdo_notification_backend_init (GFdoNotificationBackend *backend) { } static void g_fdo_notification_backend_class_init (GFdoNotificationBackendClass *class) { GObjectClass *object_class = G_OBJECT_CLASS (class); GNotificationBackendClass *backend_class = G_NOTIFICATION_BACKEND_CLASS (class); object_class->dispose = g_fdo_notification_backend_dispose; backend_class->is_supported = g_fdo_notification_backend_is_supported; backend_class->send_notification = g_fdo_notification_backend_send_notification; backend_class->withdraw_notification = g_fdo_notification_backend_withdraw_notification; }