Add ensure_required_types constructor
[platform/upstream/glib.git] / gio / gfdonotificationbackend.c
1 /*
2  * Copyright © 2013 Lars Uebernickel
3  *
4  * SPDX-License-Identifier: LGPL-2.1-or-later
5  *
6  * This library is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Lesser General Public
8  * License as published by the Free Software Foundation; either
9  * version 2.1 of the License, or (at your option) any later version.
10  *
11  * This library is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General
17  * Public License along with this library; if not, see <http://www.gnu.org/licenses/>.
18  *
19  * Authors: Lars Uebernickel <lars@uebernic.de>
20  */
21
22 #include "config.h"
23
24 #include "gnotificationbackend.h"
25
26 #include "gapplication.h"
27 #include "giomodule-priv.h"
28 #include "gnotification-private.h"
29 #include "gdbusconnection.h"
30 #include "gdbusnamewatching.h"
31 #include "gactiongroup.h"
32 #include "gaction.h"
33 #include "gthemedicon.h"
34 #include "gfileicon.h"
35 #include "gfile.h"
36 #include "gdbusutils.h"
37
38 #define G_TYPE_FDO_NOTIFICATION_BACKEND  (g_fdo_notification_backend_get_type ())
39 #define G_FDO_NOTIFICATION_BACKEND(o)    (G_TYPE_CHECK_INSTANCE_CAST ((o), G_TYPE_FDO_NOTIFICATION_BACKEND, GFdoNotificationBackend))
40
41 typedef struct _GFdoNotificationBackend GFdoNotificationBackend;
42 typedef GNotificationBackendClass       GFdoNotificationBackendClass;
43
44 struct _GFdoNotificationBackend
45 {
46   GNotificationBackend parent;
47
48   guint   bus_name_id;
49
50   guint   notify_subscription;
51   GSList *notifications;
52 };
53
54 GType g_fdo_notification_backend_get_type (void);
55
56 G_DEFINE_TYPE_WITH_CODE (GFdoNotificationBackend, g_fdo_notification_backend, G_TYPE_NOTIFICATION_BACKEND,
57   _g_io_modules_ensure_extension_points_registered ();
58   g_io_extension_point_implement (G_NOTIFICATION_BACKEND_EXTENSION_POINT_NAME,
59                                  g_define_type_id, "freedesktop", 0))
60
61 typedef struct
62 {
63   GFdoNotificationBackend *backend;
64   gchar *id;
65   guint32 notify_id;
66   gchar *default_action;  /* (nullable) (owned) */
67   GVariant *default_action_target;  /* (nullable) (owned), not floating */
68 } FreedesktopNotification;
69
70 static void
71 freedesktop_notification_free (gpointer data)
72 {
73   FreedesktopNotification *n = data;
74
75   g_object_unref (n->backend);
76   g_free (n->id);
77   g_free (n->default_action);
78   if (n->default_action_target)
79     g_variant_unref (n->default_action_target);
80
81   g_slice_free (FreedesktopNotification, n);
82 }
83
84 static FreedesktopNotification *
85 freedesktop_notification_new (GFdoNotificationBackend *backend,
86                               const gchar             *id,
87                               GNotification           *notification)
88 {
89   FreedesktopNotification *n;
90
91   n = g_slice_new0 (FreedesktopNotification);
92   n->backend = g_object_ref (backend);
93   n->id = g_strdup (id);
94   n->notify_id = 0;
95   g_notification_get_default_action (notification,
96                                      &n->default_action,
97                                      &n->default_action_target);
98
99   return n;
100 }
101
102 static FreedesktopNotification *
103 g_fdo_notification_backend_find_notification (GFdoNotificationBackend *backend,
104                                               const gchar             *id)
105 {
106   GSList *it;
107
108   for (it = backend->notifications; it != NULL; it = it->next)
109     {
110       FreedesktopNotification *n = it->data;
111       if (g_str_equal (n->id, id))
112         return n;
113     }
114
115   return NULL;
116 }
117
118 static FreedesktopNotification *
119 g_fdo_notification_backend_find_notification_by_notify_id (GFdoNotificationBackend *backend,
120                                                            guint32                  id)
121 {
122   GSList *it;
123
124   for (it = backend->notifications; it != NULL; it = it->next)
125     {
126       FreedesktopNotification *n = it->data;
127       if (n->notify_id == id)
128         return n;
129     }
130
131   return NULL;
132 }
133
134 static gboolean
135 activate_action (GFdoNotificationBackend *backend,
136                  const gchar             *name,
137                  GVariant                *parameter)
138 {
139   GNotificationBackend *g_backend = G_NOTIFICATION_BACKEND (backend);
140
141   /* Callers should not provide a floating variant here */
142   g_assert (parameter == NULL || !g_variant_is_floating (parameter));
143
144   if (name != NULL &&
145       g_str_has_prefix (name, "app."))
146     {
147       const GVariantType *parameter_type = NULL;
148       const gchar *action_name = name + strlen ("app.");
149
150       /* @name and @parameter come as untrusted input over D-Bus, so validate them first */
151       if (g_action_group_query_action (G_ACTION_GROUP (g_backend->application),
152                                        action_name, NULL, &parameter_type,
153                                        NULL, NULL, NULL) &&
154           ((parameter_type == NULL && parameter == NULL) ||
155            (parameter_type != NULL && parameter != NULL && g_variant_is_of_type (parameter, parameter_type))))
156         {
157           g_action_group_activate_action (G_ACTION_GROUP (g_backend->application), action_name, parameter);
158           return TRUE;
159         }
160     }
161   else if (name == NULL)
162     {
163       g_application_activate (g_backend->application);
164       return TRUE;
165     }
166
167   return FALSE;
168 }
169
170 static void
171 notify_signal (GDBusConnection *connection,
172                const gchar     *sender_name,
173                const gchar     *object_path,
174                const gchar     *interface_name,
175                const gchar     *signal_name,
176                GVariant        *parameters,
177                gpointer         user_data)
178 {
179   GFdoNotificationBackend *backend = user_data;
180   guint32 id = 0;
181   const gchar *action = NULL;
182   FreedesktopNotification *n;
183   gboolean notification_closed = TRUE;
184
185   if (g_str_equal (signal_name, "NotificationClosed") &&
186       g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(uu)")))
187     {
188       g_variant_get (parameters, "(uu)", &id, NULL);
189     }
190   else if (g_str_equal (signal_name, "ActionInvoked") &&
191            g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(us)")))
192     {
193       g_variant_get (parameters, "(u&s)", &id, &action);
194     }
195   else
196     return;
197
198   n = g_fdo_notification_backend_find_notification_by_notify_id (backend, id);
199   if (n == NULL)
200     return;
201
202   if (action)
203     {
204       if (g_str_equal (action, "default"))
205         {
206           if (!activate_action (backend, n->default_action, n->default_action_target))
207             notification_closed = FALSE;
208         }
209       else
210         {
211           gchar *name = NULL;
212           GVariant *target = NULL;
213
214           if (!g_action_parse_detailed_name (action, &name, &target, NULL) ||
215               !activate_action (backend, name, target))
216             notification_closed = FALSE;
217
218           g_free (name);
219           g_clear_pointer (&target, g_variant_unref);
220         }
221     }
222
223   /* Remove the notification, as it’s either been explicitly closed
224    * (`NotificationClosed` signal) or has been closed as a result of activating
225    * an action successfully. GLib doesn’t currently support the `resident` hint
226    * on notifications which would allow them to stay around after having an
227    * action invoked on them (see
228    * https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html#idm45877717456448)
229    *
230    * First, get the notification again in case the action redrew it */
231   if (notification_closed)
232     {
233       n = g_fdo_notification_backend_find_notification_by_notify_id (backend, id);
234       if (n != NULL)
235         {
236           backend->notifications = g_slist_remove (backend->notifications, n);
237           freedesktop_notification_free (n);
238         }
239     }
240 }
241
242 static void
243 name_vanished_handler_cb (GDBusConnection *connection,
244                           const gchar     *name,
245                           gpointer         user_data)
246 {
247   GFdoNotificationBackend *backend = user_data;
248
249   if (backend->notifications)
250     {
251       g_slist_free_full (backend->notifications, freedesktop_notification_free);
252       backend->notifications = NULL;
253     }
254 }
255
256 /* Converts a GNotificationPriority to an urgency level as defined by
257  * the freedesktop spec (0: low, 1: normal, 2: critical).
258  */
259 static guchar
260 urgency_from_priority (GNotificationPriority priority)
261 {
262   switch (priority)
263     {
264     case G_NOTIFICATION_PRIORITY_LOW:
265       return 0;
266
267     default:
268     case G_NOTIFICATION_PRIORITY_NORMAL:
269     case G_NOTIFICATION_PRIORITY_HIGH:
270       return 1;
271
272     case G_NOTIFICATION_PRIORITY_URGENT:
273       return 2;
274     }
275 }
276
277 static void
278 call_notify (GDBusConnection     *con,
279              GApplication        *app,
280              guint32              replace_id,
281              GNotification       *notification,
282              GAsyncReadyCallback  callback,
283              gpointer             user_data)
284 {
285   GVariantBuilder action_builder;
286   guint n_buttons;
287   guint i;
288   GVariantBuilder hints_builder;
289   GIcon *icon;
290   GVariant *parameters;
291   const gchar *app_name;
292   const gchar *body;
293   guchar urgency;
294
295   g_variant_builder_init (&action_builder, G_VARIANT_TYPE_STRING_ARRAY);
296   if (g_notification_get_default_action (notification, NULL, NULL))
297     {
298       g_variant_builder_add (&action_builder, "s", "default");
299       g_variant_builder_add (&action_builder, "s", "");
300     }
301
302   n_buttons = g_notification_get_n_buttons (notification);
303   for (i = 0; i < n_buttons; i++)
304     {
305       gchar *label;
306       gchar *action;
307       GVariant *target;
308       gchar *detailed_name;
309
310       g_notification_get_button (notification, i, &label, &action, &target);
311       detailed_name = g_action_print_detailed_name (action, target);
312
313       /* Actions named 'default' collide with libnotify's naming of the
314        * default action. Rewriting them to something unique is enough,
315        * because those actions can never be activated (they aren't
316        * prefixed with 'app.').
317        */
318       if (g_str_equal (detailed_name, "default"))
319         {
320           g_free (detailed_name);
321           detailed_name = g_dbus_generate_guid ();
322         }
323
324       g_variant_builder_add_value (&action_builder, g_variant_new_take_string (detailed_name));
325       g_variant_builder_add_value (&action_builder, g_variant_new_take_string (label));
326
327       g_free (action);
328       if (target)
329         g_variant_unref (target);
330     }
331
332   g_variant_builder_init (&hints_builder, G_VARIANT_TYPE ("a{sv}"));
333   g_variant_builder_add (&hints_builder, "{sv}", "desktop-entry",
334                          g_variant_new_string (g_application_get_application_id (app)));
335   urgency = urgency_from_priority (g_notification_get_priority (notification));
336   g_variant_builder_add (&hints_builder, "{sv}", "urgency", g_variant_new_byte (urgency));
337   if (g_notification_get_category (notification))
338     {
339       g_variant_builder_add (&hints_builder, "{sv}", "category",
340                              g_variant_new_string (g_notification_get_category (notification)));
341     }
342
343   icon = g_notification_get_icon (notification);
344   if (icon != NULL)
345     {
346       if (G_IS_FILE_ICON (icon))
347         {
348            GFile *file;
349
350            file = g_file_icon_get_file (G_FILE_ICON (icon));
351            g_variant_builder_add (&hints_builder, "{sv}", "image-path",
352                                   g_variant_new_take_string (g_file_get_path (file)));
353         }
354       else if (G_IS_THEMED_ICON (icon))
355         {
356            const gchar* const* icon_names = g_themed_icon_get_names(G_THEMED_ICON (icon));
357            /* Take first name from GThemedIcon */
358            g_variant_builder_add (&hints_builder, "{sv}", "image-path",
359                                   g_variant_new_string (icon_names[0]));
360         }
361     }
362
363   app_name = g_get_application_name ();
364   body = g_notification_get_body (notification);
365
366   parameters = g_variant_new ("(susssasa{sv}i)",
367                               app_name ? app_name : "",
368                               replace_id,
369                               "",           /* app icon */
370                               g_notification_get_title (notification),
371                               body ? body : "",
372                               &action_builder,
373                               &hints_builder,
374                               -1);          /* expire_timeout */
375
376   g_dbus_connection_call (con, "org.freedesktop.Notifications", "/org/freedesktop/Notifications",
377                           "org.freedesktop.Notifications", "Notify",
378                           parameters, G_VARIANT_TYPE ("(u)"),
379                           G_DBUS_CALL_FLAGS_NONE, -1, NULL,
380                           callback, user_data);
381 }
382
383 static void
384 notification_sent (GObject      *source_object,
385                    GAsyncResult *result,
386                    gpointer      user_data)
387 {
388   FreedesktopNotification *n = user_data;
389   GVariant *val;
390   GError *error = NULL;
391   static gboolean warning_printed = FALSE;
392
393   val = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source_object), result, &error);
394   if (val)
395     {
396       GFdoNotificationBackend *backend = n->backend;
397       FreedesktopNotification *match;
398
399       g_variant_get (val, "(u)", &n->notify_id);
400       g_variant_unref (val);
401
402       match = g_fdo_notification_backend_find_notification_by_notify_id (backend, n->notify_id);
403       if (match != NULL)
404         {
405           backend->notifications = g_slist_remove (backend->notifications, match);
406           freedesktop_notification_free (match);
407         }
408       backend->notifications = g_slist_prepend (backend->notifications, n);
409     }
410   else
411     {
412       if (!warning_printed)
413         {
414           g_warning ("unable to send notifications through org.freedesktop.Notifications: %s",
415                      error->message);
416           warning_printed = TRUE;
417         }
418
419       freedesktop_notification_free (n);
420       g_error_free (error);
421     }
422 }
423
424 static void
425 g_fdo_notification_backend_dispose (GObject *object)
426 {
427   GFdoNotificationBackend *backend = G_FDO_NOTIFICATION_BACKEND (object);
428
429   if (backend->bus_name_id)
430     {
431       g_bus_unwatch_name (backend->bus_name_id);
432       backend->bus_name_id = 0;
433     }
434
435   if (backend->notify_subscription)
436     {
437       GDBusConnection *session_bus;
438
439       session_bus = G_NOTIFICATION_BACKEND (backend)->dbus_connection;
440       g_dbus_connection_signal_unsubscribe (session_bus, backend->notify_subscription);
441       backend->notify_subscription = 0;
442     }
443
444   if (backend->notifications)
445     {
446       g_slist_free_full (backend->notifications, freedesktop_notification_free);
447       backend->notifications = NULL;
448     }
449
450   G_OBJECT_CLASS (g_fdo_notification_backend_parent_class)->dispose (object);
451 }
452
453 static gboolean
454 g_fdo_notification_backend_is_supported (void)
455 {
456   /* This is the fallback backend with the lowest priority. To avoid an
457    * unnecessary synchronous dbus call to check for
458    * org.freedesktop.Notifications, this function always succeeds. A
459    * warning will be printed when sending the first notification fails.
460    */
461   return TRUE;
462 }
463
464 static void
465 g_fdo_notification_backend_send_notification (GNotificationBackend *backend,
466                                               const gchar          *id,
467                                               GNotification        *notification)
468 {
469   GFdoNotificationBackend *self = G_FDO_NOTIFICATION_BACKEND (backend);
470   FreedesktopNotification *n, *tmp;
471
472   if (self->bus_name_id == 0)
473     {
474       self->bus_name_id = g_bus_watch_name_on_connection (backend->dbus_connection,
475                                                           "org.freedesktop.Notifications",
476                                                           G_BUS_NAME_WATCHER_FLAGS_NONE,
477                                                           NULL,
478                                                           name_vanished_handler_cb,
479                                                           backend,
480                                                           NULL);
481     }
482
483   if (self->notify_subscription == 0)
484     {
485       self->notify_subscription =
486         g_dbus_connection_signal_subscribe (backend->dbus_connection,
487                                             "org.freedesktop.Notifications",
488                                             "org.freedesktop.Notifications", NULL,
489                                             "/org/freedesktop/Notifications", NULL,
490                                             G_DBUS_SIGNAL_FLAGS_NONE,
491                                             notify_signal, backend, NULL);
492     }
493
494   n = freedesktop_notification_new (self, id, notification);
495
496   tmp = g_fdo_notification_backend_find_notification (self, id);
497   if (tmp)
498     n->notify_id = tmp->notify_id;
499
500   call_notify (backend->dbus_connection, backend->application, n->notify_id, notification, notification_sent, n);
501 }
502
503 static void
504 g_fdo_notification_backend_withdraw_notification (GNotificationBackend *backend,
505                                                   const gchar          *id)
506 {
507   GFdoNotificationBackend *self = G_FDO_NOTIFICATION_BACKEND (backend);
508   FreedesktopNotification *n;
509
510   n = g_fdo_notification_backend_find_notification (self, id);
511   if (n)
512     {
513       if (n->notify_id > 0)
514         {
515           g_dbus_connection_call (backend->dbus_connection,
516                                   "org.freedesktop.Notifications",
517                                   "/org/freedesktop/Notifications",
518                                   "org.freedesktop.Notifications", "CloseNotification",
519                                   g_variant_new ("(u)", n->notify_id), NULL,
520                                   G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
521         }
522
523       self->notifications = g_slist_remove (self->notifications, n);
524       freedesktop_notification_free (n);
525     }
526 }
527
528 static void
529 g_fdo_notification_backend_init (GFdoNotificationBackend *backend)
530 {
531 }
532
533 static void
534 g_fdo_notification_backend_class_init (GFdoNotificationBackendClass *class)
535 {
536   GObjectClass *object_class = G_OBJECT_CLASS (class);
537   GNotificationBackendClass *backend_class = G_NOTIFICATION_BACKEND_CLASS (class);
538
539   object_class->dispose = g_fdo_notification_backend_dispose;
540
541   backend_class->is_supported = g_fdo_notification_backend_is_supported;
542   backend_class->send_notification = g_fdo_notification_backend_send_notification;
543   backend_class->withdraw_notification = g_fdo_notification_backend_withdraw_notification;
544 }