1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3  * Copyright (C) 2017 Red Hat, Inc. (www.redhat.com)
4  *
5  * This library is free software: you can redistribute it and/or modify it
6  * under the terms of the GNU Lesser General Public License as published by
7  * the Free Software Foundation.
8  *
9  * This library is distributed in the hope that it will be useful, but
10  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11  * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
12  * for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public License
15  * along with this library. If not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 /**
19  * SECTION: e-source-registry-watcher
20  * @include: libedataserver/libedataserver.h
21  * @short_description: Watch changes in #ESource-s
22  *
23  * #ESourceRegistryWatcher watches for changes in an #ESourceRegistry
24  * and notifies about newly added and enabled #ESource instances, the same
25  * as about removed or disabled. The amount of notifications can be filtered
26  * with #ESourceRegistryWatcher::filter signal.
27  *
28  * The watcher listens only for changes, thus it is not pre-populated after
29  * its creation. That's because the owner usually wants to subscribe to
30  * the #ESourceRegistryWatcher::filter, #ESourceRegistryWatcher::appeared
31  * and #ESourceRegistryWatcher::disappeared signals. The owner should
32  * call e_source_registry_watcher_reclaim() when it has all the needed
33  * signal handlers connected.
34  **/
35 
36 #include "evolution-data-server-config.h"
37 
38 #include "e-source-registry.h"
39 #include "e-source.h"
40 #include "e-source-collection.h"
41 
42 #include "e-source-registry-watcher.h"
43 
44 struct _ESourceRegistryWatcherPrivate {
45 	ESourceRegistry *registry;
46 	gchar *extension_name;
47 
48 	GHashTable *known_uids; /* gchar * UID ~> ESource */
49 
50 	GRecMutex lock;
51 
52 	gulong added_id;
53 	gulong enabled_id;
54 	gulong disabled_id;
55 	gulong removed_id;
56 	gulong changed_id;
57 };
58 
59 G_DEFINE_TYPE_WITH_PRIVATE (ESourceRegistryWatcher, e_source_registry_watcher, G_TYPE_OBJECT)
60 
61 enum {
62 	PROP_0,
63 	PROP_EXTENSION_NAME,
64 	PROP_REGISTRY
65 };
66 
67 enum {
68 	FILTER,
69 	APPEARED,
70 	DISAPPEARED,
71 	LAST_SIGNAL
72 };
73 
74 static guint signals[LAST_SIGNAL];
75 
76 static gboolean
source_registry_watcher_try_remove(ESourceRegistryWatcher * watcher,ESource * source)77 source_registry_watcher_try_remove (ESourceRegistryWatcher *watcher,
78 				    ESource *source)
79 {
80 	gboolean removed;
81 
82 	g_return_val_if_fail (E_IS_SOURCE_REGISTRY_WATCHER (watcher), FALSE);
83 	g_return_val_if_fail (E_IS_SOURCE (source), FALSE);
84 
85 	g_rec_mutex_lock (&watcher->priv->lock);
86 	removed = g_hash_table_remove (watcher->priv->known_uids, e_source_get_uid (source));
87 	g_rec_mutex_unlock (&watcher->priv->lock);
88 
89 	if (removed)
90 		g_signal_emit (watcher, signals[DISAPPEARED], 0, source);
91 
92 	return removed;
93 }
94 
95 static gboolean
source_registry_watcher_try_add(ESourceRegistryWatcher * watcher,ESource * source,gboolean with_remove_check,gboolean skip_appeared_emit)96 source_registry_watcher_try_add (ESourceRegistryWatcher *watcher,
97 				 ESource *source,
98 				 gboolean with_remove_check,
99 				 gboolean skip_appeared_emit)
100 {
101 	gboolean can_include = TRUE;
102 	gboolean added = FALSE;
103 	gchar *uid;
104 
105 	g_return_val_if_fail (E_IS_SOURCE_REGISTRY_WATCHER (watcher), FALSE);
106 	g_return_val_if_fail (E_IS_SOURCE (source), FALSE);
107 
108 	uid = e_source_dup_uid (source);
109 	if (!uid)
110 		return FALSE;
111 
112 	/* Check whether anything is listening, because glib overrides the result
113 	   value with FALSE, when there's nothing listening. */
114 	if (g_signal_has_handler_pending (watcher, signals[FILTER], 0, FALSE))
115 		g_signal_emit (watcher, signals[FILTER], 0, source, &can_include);
116 
117 	if (!can_include) {
118 		if (with_remove_check)
119 			source_registry_watcher_try_remove (watcher, source);
120 
121 		g_free (uid);
122 		return FALSE;
123 	}
124 
125 	g_rec_mutex_lock (&watcher->priv->lock);
126 
127 	if (!g_hash_table_contains (watcher->priv->known_uids, uid)) {
128 		g_hash_table_insert (watcher->priv->known_uids, uid, g_object_ref (source));
129 		added = TRUE;
130 	} else {
131 		g_free (uid);
132 	}
133 
134 	g_rec_mutex_unlock (&watcher->priv->lock);
135 
136 	if (added && !skip_appeared_emit)
137 		g_signal_emit (watcher, signals[APPEARED], 0, source);
138 
139 	return added;
140 }
141 
142 static void
source_registry_watcher_reclaim_internal(ESourceRegistryWatcher * watcher,gboolean merge_like)143 source_registry_watcher_reclaim_internal (ESourceRegistryWatcher *watcher,
144 					  gboolean merge_like)
145 {
146 	GHashTable *old_known_uids = NULL;
147 	GList *enabled, *link;
148 
149 	g_return_if_fail (E_IS_SOURCE_REGISTRY_WATCHER (watcher));
150 
151 	g_rec_mutex_lock (&watcher->priv->lock);
152 	if (merge_like) {
153 		old_known_uids = watcher->priv->known_uids;
154 		watcher->priv->known_uids = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
155 	} else {
156 		g_hash_table_remove_all (watcher->priv->known_uids);
157 	}
158 
159 	enabled = e_source_registry_list_enabled (watcher->priv->registry, watcher->priv->extension_name);
160 	for (link = enabled; link; link = g_list_next (link)) {
161 		ESource *source = link->data;
162 		gboolean skip_appeared_emit;
163 
164 		skip_appeared_emit = old_known_uids && g_hash_table_contains (old_known_uids, e_source_get_uid (source));
165 
166 		if (source_registry_watcher_try_add (watcher, source, FALSE, skip_appeared_emit) && old_known_uids)
167 			g_hash_table_remove (old_known_uids, e_source_get_uid (source));
168 	}
169 	g_list_free_full (enabled, g_object_unref);
170 
171 	g_rec_mutex_unlock (&watcher->priv->lock);
172 
173 	if (old_known_uids) {
174 		GHashTableIter iter;
175 		gpointer value;
176 
177 		g_hash_table_iter_init (&iter, old_known_uids);
178 		while (g_hash_table_iter_next (&iter, NULL, &value)) {
179 			ESource *source = value;
180 
181 			g_signal_emit (watcher, signals[DISAPPEARED], 0, source);
182 		}
183 
184 		g_hash_table_destroy (old_known_uids);
185 	}
186 }
187 
188 static void
source_registry_watcher_source_added_or_enabled_cb(ESourceRegistry * registry,ESource * source,gpointer user_data)189 source_registry_watcher_source_added_or_enabled_cb (ESourceRegistry *registry,
190 						    ESource *source,
191 						    gpointer user_data)
192 {
193 	ESourceRegistryWatcher *watcher = user_data;
194 
195 	g_return_if_fail (E_IS_SOURCE_REGISTRY (registry));
196 	g_return_if_fail (E_IS_SOURCE (source));
197 	g_return_if_fail (E_IS_SOURCE_REGISTRY_WATCHER (watcher));
198 
199 	if (e_source_registry_check_enabled (registry, source))
200 		source_registry_watcher_try_add (watcher, source, TRUE, FALSE);
201 }
202 
203 static void
source_registry_watcher_source_removed_or_disabled_cb(ESourceRegistry * registry,ESource * source,gpointer user_data)204 source_registry_watcher_source_removed_or_disabled_cb (ESourceRegistry *registry,
205 						       ESource *source,
206 						       gpointer user_data)
207 {
208 	ESourceRegistryWatcher *watcher = user_data;
209 
210 	g_return_if_fail (E_IS_SOURCE_REGISTRY (registry));
211 	g_return_if_fail (E_IS_SOURCE (source));
212 	g_return_if_fail (E_IS_SOURCE_REGISTRY_WATCHER (watcher));
213 
214 	source_registry_watcher_try_remove (watcher, source);
215 }
216 
217 static void
source_registry_watcher_source_changed_cb(ESourceRegistry * registry,ESource * source,gpointer user_data)218 source_registry_watcher_source_changed_cb (ESourceRegistry *registry,
219 					   ESource *source,
220 					   gpointer user_data)
221 {
222 	ESourceRegistryWatcher *watcher = user_data;
223 
224 	g_return_if_fail (E_IS_SOURCE_REGISTRY (registry));
225 	g_return_if_fail (E_IS_SOURCE (source));
226 	g_return_if_fail (E_IS_SOURCE_REGISTRY_WATCHER (watcher));
227 
228 	if (!watcher->priv->extension_name ||
229 	    e_source_has_extension (source, watcher->priv->extension_name)) {
230 		if (e_source_registry_check_enabled (registry, source))
231 			source_registry_watcher_try_add (watcher, source, TRUE, FALSE);
232 		else
233 			source_registry_watcher_try_remove (watcher, source);
234 	}
235 }
236 
237 static void
source_registry_watcher_set_registry(ESourceRegistryWatcher * watcher,ESourceRegistry * registry)238 source_registry_watcher_set_registry (ESourceRegistryWatcher *watcher,
239 				      ESourceRegistry *registry)
240 {
241 	g_return_if_fail (E_IS_SOURCE_REGISTRY (registry));
242 	g_return_if_fail (watcher->priv->registry == NULL);
243 
244 	watcher->priv->registry = g_object_ref (registry);
245 }
246 
247 static void
source_registry_watcher_set_extension_name(ESourceRegistryWatcher * watcher,const gchar * extension_name)248 source_registry_watcher_set_extension_name (ESourceRegistryWatcher *watcher,
249 					    const gchar *extension_name)
250 {
251 	if (g_strcmp0 (watcher->priv->extension_name, extension_name) != 0) {
252 		g_free (watcher->priv->extension_name);
253 		watcher->priv->extension_name = g_strdup (extension_name);
254 	}
255 }
256 
257 static void
source_registry_watcher_set_property(GObject * object,guint property_id,const GValue * value,GParamSpec * pspec)258 source_registry_watcher_set_property (GObject *object,
259 				      guint property_id,
260 				      const GValue *value,
261 				      GParamSpec *pspec)
262 {
263 	switch (property_id) {
264 		case PROP_EXTENSION_NAME:
265 			source_registry_watcher_set_extension_name (
266 				E_SOURCE_REGISTRY_WATCHER (object),
267 				g_value_get_string (value));
268 			return;
269 
270 		case PROP_REGISTRY:
271 			source_registry_watcher_set_registry (
272 				E_SOURCE_REGISTRY_WATCHER (object),
273 				g_value_get_object (value));
274 			return;
275 	}
276 
277 	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
278 }
279 
280 static void
source_registry_watcher_get_property(GObject * object,guint property_id,GValue * value,GParamSpec * pspec)281 source_registry_watcher_get_property (GObject *object,
282 				      guint property_id,
283 				      GValue *value,
284 				      GParamSpec *pspec)
285 {
286 	switch (property_id) {
287 		case PROP_EXTENSION_NAME:
288 			g_value_set_string (
289 				value,
290 				e_source_registry_watcher_get_extension_name (
291 				E_SOURCE_REGISTRY_WATCHER (object)));
292 			return;
293 
294 		case PROP_REGISTRY:
295 			g_value_set_object (
296 				value,
297 				e_source_registry_watcher_get_registry (
298 				E_SOURCE_REGISTRY_WATCHER (object)));
299 			return;
300 	}
301 
302 	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
303 }
304 
305 static void
source_registry_watcher_constructed(GObject * object)306 source_registry_watcher_constructed (GObject *object)
307 {
308 	ESourceRegistryWatcher *watcher = E_SOURCE_REGISTRY_WATCHER (object);
309 
310 	/* Chain up to parent's method. */
311 	G_OBJECT_CLASS (e_source_registry_watcher_parent_class)->constructed (object);
312 
313 	g_return_if_fail (watcher->priv->registry != NULL);
314 
315 	watcher->priv->added_id = g_signal_connect (watcher->priv->registry, "source-added",
316 		G_CALLBACK (source_registry_watcher_source_added_or_enabled_cb), watcher);
317 
318 	watcher->priv->enabled_id = g_signal_connect (watcher->priv->registry, "source-enabled",
319 		G_CALLBACK (source_registry_watcher_source_added_or_enabled_cb), watcher);
320 
321 	watcher->priv->disabled_id = g_signal_connect (watcher->priv->registry, "source-disabled",
322 		G_CALLBACK (source_registry_watcher_source_removed_or_disabled_cb), watcher);
323 
324 	watcher->priv->removed_id = g_signal_connect (watcher->priv->registry, "source-removed",
325 		G_CALLBACK (source_registry_watcher_source_removed_or_disabled_cb), watcher);
326 
327 	watcher->priv->changed_id = g_signal_connect (watcher->priv->registry, "source-changed",
328 		G_CALLBACK (source_registry_watcher_source_changed_cb), watcher);
329 }
330 
331 static void
source_registry_watcher_dispose(GObject * object)332 source_registry_watcher_dispose (GObject *object)
333 {
334 	ESourceRegistryWatcher *watcher = E_SOURCE_REGISTRY_WATCHER (object);
335 
336 #define unset_handler(x) G_STMT_START { \
337 	if (x) { \
338 		g_signal_handler_disconnect (watcher->priv->registry, x); \
339 		x = 0; \
340 	} } G_STMT_END
341 
342 	unset_handler (watcher->priv->added_id);
343 	unset_handler (watcher->priv->enabled_id);
344 	unset_handler (watcher->priv->disabled_id);
345 	unset_handler (watcher->priv->removed_id);
346 	unset_handler (watcher->priv->changed_id);
347 
348 #undef unset_handler
349 
350 	g_clear_object (&watcher->priv->registry);
351 
352 	g_hash_table_remove_all (watcher->priv->known_uids);
353 
354 	/* Chain up to parent's method. */
355 	G_OBJECT_CLASS (e_source_registry_watcher_parent_class)->dispose (object);
356 }
357 
358 static void
source_registry_watcher_finalize(GObject * object)359 source_registry_watcher_finalize (GObject *object)
360 {
361 	ESourceRegistryWatcher *watcher = E_SOURCE_REGISTRY_WATCHER (object);
362 
363 	g_hash_table_destroy (watcher->priv->known_uids);
364 	g_free (watcher->priv->extension_name);
365 	g_rec_mutex_clear (&watcher->priv->lock);
366 
367 	/* Chain up to parent's method. */
368 	G_OBJECT_CLASS (e_source_registry_watcher_parent_class)->finalize (object);
369 }
370 
371 static void
e_source_registry_watcher_class_init(ESourceRegistryWatcherClass * klass)372 e_source_registry_watcher_class_init (ESourceRegistryWatcherClass *klass)
373 {
374 	GObjectClass *object_class;
375 
376 	object_class = G_OBJECT_CLASS (klass);
377 	object_class->set_property = source_registry_watcher_set_property;
378 	object_class->get_property = source_registry_watcher_get_property;
379 	object_class->constructed = source_registry_watcher_constructed;
380 	object_class->dispose = source_registry_watcher_dispose;
381 	object_class->finalize = source_registry_watcher_finalize;
382 
383 	/**
384 	 * ESourceRegistryWatcher:extension-name:
385 	 *
386 	 * Optional extension name, to consider sources with only.
387 	 * It can be %NULL, to check for all sources. This is
388 	 * a complementary filter to #ESourceRegistryWatcher::filter
389 	 * signal.
390 	 *
391 	 * Since: 3.26
392 	 **/
393 	g_object_class_install_property (
394 		object_class,
395 		PROP_EXTENSION_NAME,
396 		g_param_spec_string (
397 			"extension-name",
398 			"ExtensionName",
399 			NULL,
400 			NULL,
401 			G_PARAM_READWRITE |
402 			G_PARAM_CONSTRUCT_ONLY |
403 			G_PARAM_STATIC_STRINGS));
404 
405 	/**
406 	 * ESourceRegistryWatcher:registry:
407 	 *
408 	 * The #ESourceRegistry manages #ESource instances.
409 	 *
410 	 * Since: 3.26
411 	 **/
412 	g_object_class_install_property (
413 		object_class,
414 		PROP_REGISTRY,
415 		g_param_spec_object (
416 			"registry",
417 			"Registry",
418 			"Data source registry",
419 			E_TYPE_SOURCE_REGISTRY,
420 			G_PARAM_READWRITE |
421 			G_PARAM_CONSTRUCT_ONLY |
422 			G_PARAM_STATIC_STRINGS));
423 
424 	/**
425 	 * ESourceRegistryWatcher::filter:
426 	 * @watcher: the #ESourceRegistryWatcher that received the signal
427 	 * @source: the #ESource to filter
428 	 *
429 	 * A filter signal which verifies whether the @source can be considered
430 	 * for inclusion in the watcher or not. If none is set then all the sources
431 	 * are included.
432 	 *
433 	 * Returns: %TRUE, when the @source can be included, %FALSE otherwise.
434 	 *
435 	 * Since: 3.26
436 	 **/
437 	signals[FILTER] = g_signal_new (
438 		"filter",
439 		G_TYPE_FROM_CLASS (klass),
440 		G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
441 		G_STRUCT_OFFSET (ESourceRegistryWatcherClass, filter),
442 		NULL, NULL, NULL,
443 		G_TYPE_BOOLEAN, 1,
444 		E_TYPE_SOURCE);
445 
446 	/**
447 	 * ESourceRegistryWatcher::appeared:
448 	 * @watcher: the #ESourceRegistryWatcher that received the signal
449 	 * @source: the #ESource which appeared
450 	 *
451 	 * A signal emitted when the @source is enabled or added and it had been
452 	 * considered for inclusion with the @ESourceRegistryWatcher::filter signal.
453 	 *
454 	 * Since: 3.26
455 	 **/
456 	signals[APPEARED] = g_signal_new (
457 		"appeared",
458 		G_TYPE_FROM_CLASS (klass),
459 		G_SIGNAL_RUN_LAST,
460 		G_STRUCT_OFFSET (ESourceRegistryWatcherClass, appeared),
461 		NULL, NULL, NULL,
462 		G_TYPE_NONE, 1,
463 		E_TYPE_SOURCE);
464 
465 	/**
466 	 * ESourceRegistryWatcher::disappeared:
467 	 * @watcher: the #ESourceRegistryWatcher that received the signal
468 	 * @source: the #ESource which disappeared
469 	 *
470 	 * A signal emitted when the @source is disabled or removed and it had been
471 	 * considered for inclusion with the @ESourceRegistryWatcher::filter signal
472 	 * earlier.
473 	 *
474 	 * Since: 3.26
475 	 **/
476 	signals[DISAPPEARED] = g_signal_new (
477 		"disappeared",
478 		G_TYPE_FROM_CLASS (klass),
479 		G_SIGNAL_RUN_LAST,
480 		G_STRUCT_OFFSET (ESourceRegistryWatcherClass, disappeared),
481 		NULL, NULL, NULL,
482 		G_TYPE_NONE, 1,
483 		E_TYPE_SOURCE);
484 }
485 
486 static void
e_source_registry_watcher_init(ESourceRegistryWatcher * watcher)487 e_source_registry_watcher_init (ESourceRegistryWatcher *watcher)
488 {
489 	watcher->priv = e_source_registry_watcher_get_instance_private (watcher);
490 
491 	g_rec_mutex_init (&watcher->priv->lock);
492 
493 	watcher->priv->known_uids = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
494 }
495 
496 /**
497  * e_source_registry_watcher_new:
498  * @registry: an #ESourceRegistry
499  * @extension_name: (nullable): optional extension name to filter sources with, or %NULL
500  *
501  * Creates a new #ESourceRegistryWatcher instance.
502  *
503  * The @extension_name can be used as a complementary filter
504  * to #ESourceRegistryWatcher::filter signal.
505  *
506  * Returns: (transfer full): an #ESourceRegistryWatcher
507  *
508  * Since: 3.26
509  **/
510 ESourceRegistryWatcher *
e_source_registry_watcher_new(ESourceRegistry * registry,const gchar * extension_name)511 e_source_registry_watcher_new (ESourceRegistry *registry,
512 			       const gchar *extension_name)
513 {
514 	g_return_val_if_fail (E_IS_SOURCE_REGISTRY (registry), NULL);
515 
516 	return g_object_new (E_TYPE_SOURCE_REGISTRY_WATCHER,
517 		"registry", registry,
518 		"extension-name", extension_name,
519 		NULL);
520 }
521 
522 /**
523  * e_source_registry_watcher_get_registry:
524  * @watcher: an #ESourceRegistryWatcher
525  *
526  * Returns the #ESourceRegistry passed to e_source_registry_watcher_new().
527  *
528  * Returns: (transfer none): an #ESourceRegistry
529  *
530  * Since: 3.26
531  **/
532 ESourceRegistry *
e_source_registry_watcher_get_registry(ESourceRegistryWatcher * watcher)533 e_source_registry_watcher_get_registry (ESourceRegistryWatcher *watcher)
534 {
535 	g_return_val_if_fail (E_IS_SOURCE_REGISTRY_WATCHER (watcher), NULL);
536 
537 	return watcher->priv->registry;
538 }
539 
540 /**
541  * e_source_registry_watcher_get_extension_name:
542  * @watcher: an #ESourceRegistryWatcher
543  *
544  * Returns: (nullable): The extension name passed to e_source_registry_watcher_new().
545  *
546  * Since: 3.26
547  **/
548 const gchar *
e_source_registry_watcher_get_extension_name(ESourceRegistryWatcher * watcher)549 e_source_registry_watcher_get_extension_name (ESourceRegistryWatcher *watcher)
550 {
551 	g_return_val_if_fail (E_IS_SOURCE_REGISTRY_WATCHER (watcher), NULL);
552 
553 	return watcher->priv->extension_name;
554 }
555 
556 /**
557  * e_source_registry_watcher_reclaim:
558  * @watcher: an #ESourceRegistryWatcher
559  *
560  * Reclaims all available sources satisfying the #ESourceRegistryWatcher::filter
561  * signal. It doesn't notify about disappeared sources, it notifies only
562  * on those appeared.
563  *
564  * Since: 3.26
565  **/
566 void
e_source_registry_watcher_reclaim(ESourceRegistryWatcher * watcher)567 e_source_registry_watcher_reclaim (ESourceRegistryWatcher *watcher)
568 {
569 	g_return_if_fail (E_IS_SOURCE_REGISTRY_WATCHER (watcher));
570 
571 	source_registry_watcher_reclaim_internal (watcher, FALSE);
572 }
573