1 /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
2 /*
3 * Copyright © 2011 – 2017 Red Hat, Inc.
4 *
5 * This library is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU Lesser General Public
7 * License as published by the Free Software Foundation; either
8 * version 2 of the License, or (at your option) any later version.
9 *
10 * This library is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * Lesser General Public License for more details.
14 *
15 * You should have received a copy of the GNU Lesser General
16 * Public License along with this library; if not, see <http://www.gnu.org/licenses/>.
17 */
18
19 #include "config.h"
20 #include <glib/gi18n-lib.h>
21
22 #include "goaclient.h"
23 #include "goaerror.h"
24
25 G_LOCK_DEFINE_STATIC (init_lock);
26
27 /**
28 * SECTION:goaclient
29 * @title: GoaClient
30 * @short_description: Object for accessing account information
31 *
32 * #GoaClient is used for accessing the GNOME Online Accounts service
33 * from a client program.
34 */
35
36 /**
37 * GoaClient:
38 *
39 * The #GoaClient structure contains only private data and should
40 * only be accessed using the provided API.
41 */
42 struct _GoaClient
43 {
44 GObject parent_instance;
45
46 gboolean is_initialized;
47 GError *initialization_error;
48
49 GDBusObjectManager *object_manager;
50 };
51
52 enum
53 {
54 PROP_0,
55 PROP_OBJECT_MANAGER
56 };
57
58 enum
59 {
60 ACCOUNT_ADDED_SIGNAL,
61 ACCOUNT_REMOVED_SIGNAL,
62 ACCOUNT_CHANGED_SIGNAL,
63 LAST_SIGNAL
64 };
65
66 static guint signals[LAST_SIGNAL] = { 0 };
67
68 static void initable_iface_init (GInitableIface *initable_iface);
69 static void async_initable_iface_init (GAsyncInitableIface *async_initable_iface);
70
71 static void on_object_added (GDBusObjectManager *manager,
72 GDBusObject *object,
73 gpointer user_data);
74 static void on_object_removed (GDBusObjectManager *manager,
75 GDBusObject *object,
76 gpointer user_data);
77 static void on_interface_proxy_properties_changed (GDBusObjectManagerClient *manager,
78 GDBusObjectProxy *object_proxy,
79 GDBusProxy *interface_proxy,
80 GVariant *changed_properties,
81 const gchar* const *invalidated_properties,
82 gpointer user_data);
83 static void on_interface_added (GDBusObjectManager *manager,
84 GDBusObject *object,
85 GDBusInterface *interface,
86 gpointer user_data);
87 static void on_interface_removed (GDBusObjectManager *manager,
88 GDBusObject *object,
89 GDBusInterface *interface,
90 gpointer user_data);
91
92 G_DEFINE_TYPE_WITH_CODE (GoaClient, goa_client, G_TYPE_OBJECT,
93 G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init)
94 G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE, async_initable_iface_init)
95 );
96
97 static void
goa_client_finalize(GObject * object)98 goa_client_finalize (GObject *object)
99 {
100 GoaClient *self = GOA_CLIENT (object);
101
102 if (self->initialization_error != NULL)
103 g_error_free (self->initialization_error);
104
105 if (self->object_manager != NULL)
106 {
107 g_signal_handlers_disconnect_by_func (self->object_manager, G_CALLBACK (on_object_added), self);
108 g_signal_handlers_disconnect_by_func (self->object_manager, G_CALLBACK (on_object_removed), self);
109 g_signal_handlers_disconnect_by_func (self->object_manager, G_CALLBACK (on_interface_proxy_properties_changed), self);
110 g_signal_handlers_disconnect_by_func (self->object_manager, G_CALLBACK (on_interface_added), self);
111 g_signal_handlers_disconnect_by_func (self->object_manager, G_CALLBACK (on_interface_removed), self);
112 g_object_unref (self->object_manager);
113 }
114
115 G_OBJECT_CLASS (goa_client_parent_class)->finalize (object);
116 }
117
118 static void
goa_client_init(GoaClient * self)119 goa_client_init (GoaClient *self)
120 {
121 static volatile GQuark goa_error_domain = 0;
122 /* this will force associating errors in the GOA_ERROR error domain
123 * with org.freedesktop.Goa.Error.* errors via g_dbus_error_register_error_domain().
124 */
125 goa_error_domain = GOA_ERROR;
126 goa_error_domain; /* shut up -Wunused-but-set-variable */
127 }
128
129 static void
goa_client_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)130 goa_client_get_property (GObject *object,
131 guint prop_id,
132 GValue *value,
133 GParamSpec *pspec)
134 {
135 GoaClient *self = GOA_CLIENT (object);
136
137 switch (prop_id)
138 {
139 case PROP_OBJECT_MANAGER:
140 g_value_set_object (value, goa_client_get_object_manager (self));
141 break;
142
143 default:
144 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
145 break;
146 }
147 }
148
149 static void
goa_client_class_init(GoaClientClass * klass)150 goa_client_class_init (GoaClientClass *klass)
151 {
152 GObjectClass *gobject_class;
153
154 gobject_class = G_OBJECT_CLASS (klass);
155 gobject_class->finalize = goa_client_finalize;
156 gobject_class->get_property = goa_client_get_property;
157
158 /**
159 * GoaClient:object-manager:
160 *
161 * The #GDBusObjectManager used by the #GoaClient instance.
162 */
163 g_object_class_install_property (gobject_class,
164 PROP_OBJECT_MANAGER,
165 g_param_spec_object ("object-manager",
166 "object manager",
167 "The GDBusObjectManager used by the GoaClient",
168 G_TYPE_DBUS_OBJECT_MANAGER,
169 G_PARAM_READABLE |
170 G_PARAM_STATIC_STRINGS));
171
172 /**
173 * GoaClient::account-added:
174 * @client: The #GoaClient object emitting the signal.
175 * @object: The #GoaObject for the added account.
176 *
177 * Emitted when @object has been added. See
178 * goa_client_get_accounts() for information about how to use this
179 * object.
180 */
181 signals[ACCOUNT_ADDED_SIGNAL] =
182 g_signal_new ("account-added",
183 G_TYPE_FROM_CLASS (klass),
184 G_SIGNAL_RUN_LAST,
185 0,
186 NULL,
187 NULL,
188 g_cclosure_marshal_VOID__OBJECT,
189 G_TYPE_NONE,
190 1,
191 GOA_TYPE_OBJECT);
192
193 /**
194 * GoaClient::account-removed:
195 * @client: The #GoaClient object emitting the signal.
196 * @object: The #GoaObject for the removed account.
197 *
198 * Emitted when @object has been removed.
199 */
200 signals[ACCOUNT_REMOVED_SIGNAL] =
201 g_signal_new ("account-removed",
202 G_TYPE_FROM_CLASS (klass),
203 G_SIGNAL_RUN_LAST,
204 0,
205 NULL,
206 NULL,
207 g_cclosure_marshal_VOID__OBJECT,
208 G_TYPE_NONE,
209 1,
210 GOA_TYPE_OBJECT);
211
212 /**
213 * GoaClient::account-changed:
214 * @client: The #GoaClient object emitting the signal.
215 * @object: The #GoaObject for the account with changes.
216 *
217 * Emitted when something on @object changes.
218 */
219 signals[ACCOUNT_CHANGED_SIGNAL] =
220 g_signal_new ("account-changed",
221 G_TYPE_FROM_CLASS (klass),
222 G_SIGNAL_RUN_LAST,
223 0,
224 NULL,
225 NULL,
226 g_cclosure_marshal_VOID__OBJECT,
227 G_TYPE_NONE,
228 1,
229 GOA_TYPE_OBJECT);
230
231 }
232
233 /**
234 * goa_client_new:
235 * @cancellable: A #GCancellable or %NULL.
236 * @callback: Function that will be called when the result is ready.
237 * @user_data: Data to pass to @callback.
238 *
239 * Asynchronously gets a #GoaClient. When the operation is
240 * finished, @callback will be invoked in the <link
241 * linkend="g-main-context-push-thread-default">thread-default main
242 * loop</link> of the thread you are calling this method from.
243 */
244 void
goa_client_new(GCancellable * cancellable,GAsyncReadyCallback callback,gpointer user_data)245 goa_client_new (GCancellable *cancellable,
246 GAsyncReadyCallback callback,
247 gpointer user_data)
248 {
249 g_async_initable_new_async (GOA_TYPE_CLIENT,
250 G_PRIORITY_DEFAULT,
251 cancellable,
252 callback,
253 user_data,
254 NULL);
255 }
256
257 /**
258 * goa_client_new_finish:
259 * @res: A #GAsyncResult.
260 * @error: Return location for error or %NULL.
261 *
262 * Finishes an operation started with goa_client_new().
263 *
264 * Returns: A #GoaClient or %NULL if @error is set. Free with
265 * g_object_unref() when done with it.
266 */
267 GoaClient *
goa_client_new_finish(GAsyncResult * res,GError ** error)268 goa_client_new_finish (GAsyncResult *res,
269 GError **error)
270 {
271 GObject *ret;
272 GObject *source_object;
273 source_object = g_async_result_get_source_object (res);
274 ret = g_async_initable_new_finish (G_ASYNC_INITABLE (source_object), res, error);
275 g_object_unref (source_object);
276 if (ret != NULL)
277 return GOA_CLIENT (ret);
278 else
279 return NULL;
280 }
281
282 /**
283 * goa_client_new_sync:
284 * @cancellable: (allow-none): A #GCancellable or %NULL.
285 * @error: (allow-none): Return location for error or %NULL.
286 *
287 * Synchronously gets a #GoaClient for the local system.
288 *
289 * Returns: A #GoaClient or %NULL if @error is set. Free with
290 * g_object_unref() when done with it.
291 */
292 GoaClient *
goa_client_new_sync(GCancellable * cancellable,GError ** error)293 goa_client_new_sync (GCancellable *cancellable,
294 GError **error)
295 {
296 GInitable *ret;
297 ret = g_initable_new (GOA_TYPE_CLIENT,
298 cancellable,
299 error,
300 NULL);
301 if (ret != NULL)
302 return GOA_CLIENT (ret);
303 else
304 return NULL;
305 }
306
307 /* ---------------------------------------------------------------------------------------------------- */
308
309 static gboolean
initable_init(GInitable * initable,GCancellable * cancellable,GError ** error)310 initable_init (GInitable *initable,
311 GCancellable *cancellable,
312 GError **error)
313 {
314 GoaClient *self = GOA_CLIENT (initable);
315 gboolean ret = FALSE;
316
317 /* This method needs to be idempotent to work with the singleton
318 * pattern. See the docs for g_initable_init(). We implement this by
319 * locking.
320 */
321 G_LOCK (init_lock);
322 if (self->is_initialized)
323 {
324 if (self->object_manager != NULL)
325 ret = TRUE;
326 else
327 g_assert (self->initialization_error != NULL);
328 goto out;
329 }
330 g_assert (self->initialization_error == NULL);
331
332 self->object_manager = goa_object_manager_client_new_for_bus_sync (G_BUS_TYPE_SESSION,
333 G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_NONE,
334 "org.gnome.OnlineAccounts",
335 "/org/gnome/OnlineAccounts",
336 cancellable,
337 &self->initialization_error);
338 if (self->object_manager == NULL)
339 goto out;
340 g_signal_connect (self->object_manager,
341 "object-added",
342 G_CALLBACK (on_object_added),
343 self);
344 g_signal_connect (self->object_manager,
345 "object-removed",
346 G_CALLBACK (on_object_removed),
347 self);
348 g_signal_connect (self->object_manager,
349 "interface-proxy-properties-changed",
350 G_CALLBACK (on_interface_proxy_properties_changed),
351 self);
352 g_signal_connect (self->object_manager,
353 "interface-added",
354 G_CALLBACK (on_interface_added),
355 self);
356 g_signal_connect (self->object_manager,
357 "interface-removed",
358 G_CALLBACK (on_interface_removed),
359 self);
360
361 ret = TRUE;
362
363 out:
364 self->is_initialized = TRUE;
365 if (!ret)
366 {
367 g_assert (self->initialization_error != NULL);
368 g_propagate_error (error, g_error_copy (self->initialization_error));
369 }
370 G_UNLOCK (init_lock);
371 return ret;
372 }
373
374 static void
initable_iface_init(GInitableIface * initable_iface)375 initable_iface_init (GInitableIface *initable_iface)
376 {
377 initable_iface->init = initable_init;
378 }
379
380 static void
async_initable_iface_init(GAsyncInitableIface * async_initable_iface)381 async_initable_iface_init (GAsyncInitableIface *async_initable_iface)
382 {
383 /* Use default implementation (e.g. run GInitable code in a thread) */
384 }
385
386 /**
387 * goa_client_get_object_manager:
388 * @self: A #GoaClient.
389 *
390 * Gets the #GDBusObjectManager used by @self.
391 *
392 * Returns: (transfer none): A #GDBusObjectManager. Do not free, the
393 * instance is owned by @self.
394 */
395 GDBusObjectManager *
goa_client_get_object_manager(GoaClient * self)396 goa_client_get_object_manager (GoaClient *self)
397 {
398 g_return_val_if_fail (GOA_IS_CLIENT (self), NULL);
399 return self->object_manager;
400 }
401
402 /**
403 * goa_client_get_manager:
404 * @self: A #GoaClient.
405 *
406 * Gets the #GoaManager for @self, if any.
407 *
408 * Returns: (nullable) (transfer none): A #GoaManager or %NULL. Do not
409 * free, the returned object belongs to @self.
410 */
411 GoaManager *
goa_client_get_manager(GoaClient * self)412 goa_client_get_manager (GoaClient *self)
413 {
414 GDBusObject *object;
415 GoaManager *manager = NULL;
416
417 object = g_dbus_object_manager_get_object (self->object_manager, "/org/gnome/OnlineAccounts/Manager");
418 if (object == NULL)
419 goto out;
420
421 manager = goa_object_peek_manager (GOA_OBJECT (object));
422
423 out:
424 g_clear_object (&object);
425 return manager;
426 }
427
428 /**
429 * goa_client_get_accounts:
430 * @self: A #GoaClient.
431 *
432 * Gets all accounts that @self knows about. The result is a list of
433 * #GoaObject instances where each object at least has an #GoaAccount
434 * interface (that can be obtained via the goa_object_get_account()
435 * method) but may also implement other interfaces such as
436 * #GoaMail or #GoaFiles.
437 *
438 * Returns: (transfer full) (element-type GoaObject): A list of
439 * #GoaObject instances that must be freed with g_list_free() after
440 * each element has been freed with g_object_unref().
441 */
442 GList *
goa_client_get_accounts(GoaClient * self)443 goa_client_get_accounts (GoaClient *self)
444 {
445 GList *ret = NULL;
446 GList *objects;
447 GList *l;
448
449 g_return_val_if_fail (GOA_IS_CLIENT (self), NULL);
450
451 objects = g_dbus_object_manager_get_objects (self->object_manager);
452 for (l = objects; l != NULL; l = l->next)
453 {
454 GoaObject *object = GOA_OBJECT (l->data);
455
456 if (goa_object_peek_account (object) != NULL)
457 ret = g_list_prepend (ret, g_object_ref (object));
458 }
459 g_list_free_full (objects, g_object_unref);
460
461 return ret;
462 }
463
464 /**
465 * goa_client_lookup_by_id:
466 * @self: A #GoaClient.
467 * @id: The ID to look for.
468 *
469 * Finds and returns the #GoaObject instance whose
470 * <link
471 * linkend="gdbus-property-org-gnome-OnlineAccounts-Account.Id">"Id"</link>
472 * D-Bus property matches @id.
473 *
474 * Returns: (transfer full): A #GoaObject. Free the returned
475 * object with g_object_unref().
476 *
477 * Since: 3.6
478 */
479 GoaObject *
goa_client_lookup_by_id(GoaClient * self,const gchar * id)480 goa_client_lookup_by_id (GoaClient *self,
481 const gchar *id)
482 {
483 GList *accounts;
484 GList *l;
485 GoaObject *ret = NULL;
486
487 accounts = goa_client_get_accounts (self);
488 for (l = accounts; l != NULL; l = g_list_next (l))
489 {
490 GoaAccount *account;
491 GoaObject *object = GOA_OBJECT (l->data);
492
493 account = goa_object_peek_account (object);
494 if (account == NULL)
495 continue;
496
497 if (g_strcmp0 (goa_account_get_id (account), id) == 0)
498 {
499 ret = g_object_ref (object);
500 break;
501 }
502 }
503
504 g_list_free_full (accounts, g_object_unref);
505 return ret;
506 }
507
508 /* ---------------------------------------------------------------------------------------------------- */
509
510 static void
on_object_added(GDBusObjectManager * manager,GDBusObject * object,gpointer user_data)511 on_object_added (GDBusObjectManager *manager,
512 GDBusObject *object,
513 gpointer user_data)
514 {
515 GoaClient *self = GOA_CLIENT (user_data);
516 if (goa_object_peek_account (GOA_OBJECT (object)) != NULL)
517 g_signal_emit (self, signals[ACCOUNT_ADDED_SIGNAL], 0, object);
518 }
519
520 static void
on_object_removed(GDBusObjectManager * manager,GDBusObject * object,gpointer user_data)521 on_object_removed (GDBusObjectManager *manager,
522 GDBusObject *object,
523 gpointer user_data)
524 {
525 GoaClient *self = GOA_CLIENT (user_data);
526 if (goa_object_peek_account (GOA_OBJECT (object)) != NULL)
527 g_signal_emit (self, signals[ACCOUNT_REMOVED_SIGNAL], 0, object);
528 }
529
530 static void
on_interface_proxy_properties_changed(GDBusObjectManagerClient * manager,GDBusObjectProxy * object_proxy,GDBusProxy * interface_proxy,GVariant * changed_properties,const gchar * const * invalidated_properties,gpointer user_data)531 on_interface_proxy_properties_changed (GDBusObjectManagerClient *manager,
532 GDBusObjectProxy *object_proxy,
533 GDBusProxy *interface_proxy,
534 GVariant *changed_properties,
535 const gchar* const *invalidated_properties,
536 gpointer user_data)
537 {
538 GoaClient *self = GOA_CLIENT (user_data);
539 if (goa_object_peek_account (GOA_OBJECT (object_proxy)) != NULL)
540 g_signal_emit (self, signals[ACCOUNT_CHANGED_SIGNAL], 0, object_proxy);
541 }
542
543 static void
on_interface_added(GDBusObjectManager * manager,GDBusObject * object,GDBusInterface * interface,gpointer user_data)544 on_interface_added (GDBusObjectManager *manager,
545 GDBusObject *object,
546 GDBusInterface *interface,
547 gpointer user_data)
548 {
549 GoaClient *self = GOA_CLIENT (user_data);
550 if (goa_object_peek_account (GOA_OBJECT (object)) != NULL)
551 g_signal_emit (self, signals[ACCOUNT_CHANGED_SIGNAL], 0, object);
552 }
553
554 static void
on_interface_removed(GDBusObjectManager * manager,GDBusObject * object,GDBusInterface * interface,gpointer user_data)555 on_interface_removed (GDBusObjectManager *manager,
556 GDBusObject *object,
557 GDBusInterface *interface,
558 gpointer user_data)
559 {
560 GoaClient *self = GOA_CLIENT (user_data);
561 if (goa_object_peek_account (GOA_OBJECT (object)) != NULL)
562 g_signal_emit (self, signals[ACCOUNT_CHANGED_SIGNAL], 0, object);
563 }
564