1 /*
2  *  Routines to copy information from a Gaim buddy list into an
3  *  Evolution addressbook.
4  *
5  *  I currently copy IM account names and buddy icons, provided you
6  *  don't already have a buddy icon defined for a person.
7  *
8  *  This works today (25 October 2004), but is pretty sure to break
9  *  later on as the Gaim buddylist file format shifts.
10  *
11  * This program is free software; you can redistribute it and/or modify it
12  * under the terms of the GNU Lesser General Public License as published by
13  * the Free Software Foundation.
14  *
15  * This program is distributed in the hope that it will be useful, but
16  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
17  * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
18  * for more details.
19  *
20  * You should have received a copy of the GNU Lesser General Public License
21  * along with this program; if not, see <http://www.gnu.org/licenses/>.
22  *
23  *
24  * Authors:
25  *		Nat Friedman <nat@novell.com>
26  *
27  * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
28  *
29  */
30 
31 #include "evolution-config.h"
32 
33 #include <libxml/tree.h>
34 #include <libxml/parser.h>
35 #include <libxml/xmlmemory.h>
36 
37 #include <gtk/gtk.h>
38 #include <glib/gi18n.h>
39 #include <string.h>
40 
41 #include <sys/time.h>
42 #include <sys/stat.h>
43 
44 #include <e-util/e-util.h>
45 
46 #include "bbdb.h"
47 
48 typedef struct {
49 	gchar *account_name;
50 	gchar *proto;
51 	gchar *alias;
52 	gchar *icon;
53 } GaimBuddy;
54 
55 /* Forward declarations for this file. */
56 static gboolean	bbdb_merge_buddy_to_contact	(EBookClient *client,
57 						 GaimBuddy *buddy,
58 						 EContact *contact);
59 static void	bbdb_get_gaim_buddy_list	(GQueue *out_buddies);
60 static gchar *	get_node_text			(xmlNodePtr node);
61 static gchar *	get_buddy_icon_from_setting	(xmlNodePtr setting);
62 static void	parse_buddy_group		(xmlNodePtr group,
63 						 GQueue *out_buddies,
64 						 GSList *blocked);
65 static EContactField
66 		proto_to_contact_field		(const gchar *proto);
67 
68 static void
free_gaim_body(GaimBuddy * gb)69 free_gaim_body (GaimBuddy *gb)
70 {
71 	if (gb != NULL) {
72 		g_free (gb->icon);
73 		g_free (gb->alias);
74 		g_free (gb->account_name);
75 		g_free (gb->proto);
76 		g_free (gb);
77 	}
78 }
79 
80 static gchar *
get_buddy_filename(void)81 get_buddy_filename (void)
82 {
83 	return g_build_filename (
84 		g_get_home_dir (), ".purple", "blist.xml", NULL);
85 }
86 
87 static gchar *
get_md5_as_string(const gchar * filename)88 get_md5_as_string (const gchar *filename)
89 {
90 	GMappedFile *mapped_file;
91 	const gchar *contents;
92 	gchar *digest;
93 	gsize length;
94 	GError *error = NULL;
95 
96 	g_return_val_if_fail (filename != NULL, NULL);
97 
98 	mapped_file = g_mapped_file_new (filename, FALSE, &error);
99 	if (mapped_file == NULL) {
100 		g_warning ("%s", error->message);
101 		return NULL;
102 	}
103 
104 	contents = g_mapped_file_get_contents (mapped_file);
105 	length = g_mapped_file_get_length (mapped_file);
106 
107 	digest = g_compute_checksum_for_data (
108 		G_CHECKSUM_MD5, (guchar *) contents, length);
109 
110 	g_mapped_file_unref (mapped_file);
111 
112 	return digest;
113 }
114 
115 void
bbdb_sync_buddy_list_check(void)116 bbdb_sync_buddy_list_check (void)
117 {
118 	struct stat statbuf;
119 	time_t last_sync_time;
120 	gchar *md5;
121 	gchar *blist_path;
122 	gchar *last_sync_str;
123 	GSettings *settings = e_util_ref_settings (CONF_SCHEMA);
124 
125 	blist_path = get_buddy_filename ();
126 	if (stat (blist_path, &statbuf) < 0) {
127 		g_free (blist_path);
128 		return;
129 	}
130 
131 	/* Reprocess the buddy list if it's been updated. */
132 	last_sync_str = g_settings_get_string (settings, CONF_KEY_GAIM_LAST_SYNC_TIME);
133 	if (last_sync_str == NULL || !strcmp ((const gchar *) last_sync_str, ""))
134 		last_sync_time = (time_t) 0;
135 	else
136 		last_sync_time = (time_t) g_ascii_strtoull (last_sync_str, NULL, 10);
137 
138 	g_free (last_sync_str);
139 
140 	if (statbuf.st_mtime <= last_sync_time) {
141 		g_object_unref (G_OBJECT (settings));
142 		g_free (blist_path);
143 		return;
144 	}
145 
146 	last_sync_str = g_settings_get_string (
147 		settings, CONF_KEY_GAIM_LAST_SYNC_MD5);
148 
149 	g_object_unref (settings);
150 
151 	md5 = get_md5_as_string (blist_path);
152 
153 	if (!last_sync_str || !*last_sync_str || !g_str_equal (md5, last_sync_str)) {
154 		fprintf (stderr, "bbdb: Buddy list has changed since last sync.\n");
155 
156 		bbdb_sync_buddy_list ();
157 	}
158 
159 	g_free (last_sync_str);
160 	g_free (blist_path);
161 	g_free (md5);
162 }
163 
164 static gboolean
store_last_sync_idle_cb(gpointer data)165 store_last_sync_idle_cb (gpointer data)
166 {
167 	GSettings *settings;
168 	gchar *md5;
169 	gchar *blist_path = get_buddy_filename ();
170 	time_t last_sync;
171 	gchar *last_sync_time;
172 
173 	time (&last_sync);
174 	last_sync_time = g_strdup_printf ("%ld", (glong) last_sync);
175 
176 	md5 = get_md5_as_string (blist_path);
177 
178 	settings = e_util_ref_settings (CONF_SCHEMA);
179 	g_settings_set_string (
180 		settings, CONF_KEY_GAIM_LAST_SYNC_TIME, last_sync_time);
181 	g_settings_set_string (
182 		settings, CONF_KEY_GAIM_LAST_SYNC_MD5, md5);
183 
184 	g_object_unref (G_OBJECT (settings));
185 
186 	g_free (last_sync_time);
187 	g_free (blist_path);
188 	g_free (md5);
189 
190 	return FALSE;
191 }
192 
193 static gboolean syncing = FALSE;
194 G_LOCK_DEFINE_STATIC (syncing);
195 
196 static gpointer
bbdb_sync_buddy_list_in_thread(gpointer data)197 bbdb_sync_buddy_list_in_thread (gpointer data)
198 {
199 	EBookClient *client;
200 	GQueue *buddies = data;
201 	GList *head, *link;
202 	GError *error = NULL;
203 
204 	g_return_val_if_fail (buddies != NULL, NULL);
205 
206 	client = bbdb_create_book_client (GAIM_ADDRESSBOOK, NULL, &error);
207 	if (error != NULL) {
208 		g_warning (
209 			"bbdb: Failed to get addressbook: %s",
210 			error->message);
211 		g_error_free (error);
212 		goto exit;
213 	}
214 
215 	printf ("bbdb: Synchronizing buddy list to contacts...\n");
216 
217 	/* Walk the buddy list */
218 
219 	head = g_queue_peek_head_link (buddies);
220 
221 	for (link = head; link != NULL; link = g_list_next (link)) {
222 		GaimBuddy *b = link->data;
223 		EBookQuery *query;
224 		gchar *query_string;
225 		GSList *contacts = NULL;
226 		EContact *c;
227 
228 		if (b->alias == NULL || strlen (b->alias) == 0) {
229 			g_free (b->alias);
230 			b->alias = g_strdup (b->account_name);
231 		}
232 
233 		/* Look for an exact match full name == buddy alias */
234 		query = e_book_query_field_test (
235 			E_CONTACT_FULL_NAME, E_BOOK_QUERY_IS, b->alias);
236 		query_string = e_book_query_to_string (query);
237 		e_book_query_unref (query);
238 		if (!e_book_client_get_contacts_sync (
239 			client, query_string, &contacts, NULL, NULL)) {
240 			g_free (query_string);
241 			continue;
242 		}
243 
244 		g_free (query_string);
245 
246 		if (contacts != NULL) {
247 
248 			/* FIXME: If there's more than one contact with this
249 			 * name, just give up; we're not smart enough for
250 			 * this. */
251 			if (contacts->next != NULL) {
252 				g_slist_free_full (
253 					contacts,
254 					(GDestroyNotify) g_object_unref);
255 				continue;
256 			}
257 
258 			c = E_CONTACT (contacts->data);
259 
260 			if (!bbdb_merge_buddy_to_contact (client, b, c)) {
261 				g_slist_free_full (
262 					contacts,
263 					(GDestroyNotify) g_object_unref);
264 				continue;
265 			}
266 
267 			/* Write it out to the addressbook */
268 			e_book_client_modify_contact_sync (
269 				client, c, E_BOOK_OPERATION_FLAG_NONE, NULL, &error);
270 
271 			if (error != NULL) {
272 				g_warning (
273 					"bbdb: Could not modify contact: %s",
274 					error->message);
275 				g_clear_error (&error);
276 			}
277 
278 			g_slist_free_full (
279 				contacts,
280 				(GDestroyNotify) g_object_unref);
281 			continue;
282 		}
283 
284 		/* Otherwise, create a new contact. */
285 		c = e_contact_new ();
286 		e_contact_set (c, E_CONTACT_FULL_NAME, (gpointer) b->alias);
287 		if (!bbdb_merge_buddy_to_contact (client, b, c)) {
288 			g_object_unref (c);
289 			continue;
290 		}
291 
292 		e_book_client_add_contact_sync (client, c, E_BOOK_OPERATION_FLAG_NONE, NULL, NULL, &error);
293 
294 		if (error != NULL) {
295 			g_warning (
296 				"bbdb: Failed to add new contact: %s",
297 				error->message);
298 			g_clear_error (&error);
299 			goto exit;
300 		}
301 
302 		g_object_unref (c);
303 	}
304 
305 	g_idle_add (store_last_sync_idle_cb, NULL);
306 
307 exit:
308 	printf ("bbdb: Done syncing buddy list to contacts.\n");
309 
310 	g_clear_object (&client);
311 
312 	g_queue_free_full (buddies, (GDestroyNotify) free_gaim_body);
313 
314 	G_LOCK (syncing);
315 	syncing = FALSE;
316 	G_UNLOCK (syncing);
317 
318 	return NULL;
319 }
320 
321 void
bbdb_sync_buddy_list(void)322 bbdb_sync_buddy_list (void)
323 {
324 	GQueue *buddies;
325 
326 	G_LOCK (syncing);
327 	if (syncing) {
328 		G_UNLOCK (syncing);
329 		printf ("bbdb: Already syncing buddy list, skipping this call\n");
330 		return;
331 	}
332 
333 	buddies = g_queue_new ();
334 	bbdb_get_gaim_buddy_list (buddies);
335 
336 	if (g_queue_is_empty (buddies)) {
337 		g_queue_free (buddies);
338 	} else {
339 		GThread *thread;
340 
341 		syncing = TRUE;
342 
343 		thread = g_thread_new (
344 			NULL, bbdb_sync_buddy_list_in_thread, buddies);
345 		g_thread_unref (thread);
346 	}
347 
348 	G_UNLOCK (syncing);
349 }
350 
351 static gboolean
im_list_contains_buddy(GList * ims,GaimBuddy * b)352 im_list_contains_buddy (GList *ims,
353                         GaimBuddy *b)
354 {
355 	GList *l;
356 
357 	for (l = ims; l != NULL; l = l->next) {
358 		gchar *im = (gchar *) l->data;
359 
360 		if (!strcmp (im, b->account_name))
361 			return TRUE;
362 	}
363 
364 	return FALSE;
365 }
366 
367 static gboolean
bbdb_merge_buddy_to_contact(EBookClient * client,GaimBuddy * b,EContact * c)368 bbdb_merge_buddy_to_contact (EBookClient *client,
369                              GaimBuddy *b,
370                              EContact *c)
371 {
372 	EContactField field;
373 	GList *ims;
374 	gboolean dirty = FALSE;
375 	EContactPhoto *photo = NULL;
376 	GError *error = NULL;
377 
378 	/* Set the IM account */
379 	field = proto_to_contact_field (b->proto);
380 	ims = e_contact_get (c, field);
381 	if (!im_list_contains_buddy (ims, b)) {
382 		ims = g_list_append (ims, g_strdup (b->account_name));
383 		e_contact_set (c, field, (gpointer) ims);
384 		dirty = TRUE;
385 	}
386 
387 	g_list_foreach (ims, (GFunc) g_free, NULL);
388 	g_list_free (ims);
389 	ims = NULL;
390 
391         /* Set the photo if it's not set */
392 	if (b->icon != NULL) {
393 		photo = e_contact_get (c, E_CONTACT_PHOTO);
394 		if (photo == NULL) {
395 			gchar *contents = NULL;
396 
397 			photo = e_contact_photo_new ();
398 			photo->type = E_CONTACT_PHOTO_TYPE_INLINED;
399 
400 			if (!g_file_get_contents (
401 				b->icon, &contents,
402 				&photo->data.inlined.length, &error)) {
403 				g_warning (
404 					"bbdb: Could not read buddy icon: "
405 					"%s\n", error->message);
406 				g_error_free (error);
407 				e_contact_photo_free (photo);
408 				return dirty;
409 			}
410 
411 			photo->data.inlined.data = (guchar *) contents;
412 			e_contact_set (c, E_CONTACT_PHOTO, (gpointer) photo);
413 			dirty = TRUE;
414 		}
415 	}
416 
417 	/* Clean up */
418 	if (photo != NULL)
419 		e_contact_photo_free (photo);
420 
421 	return dirty;
422 }
423 
424 static EContactField
proto_to_contact_field(const gchar * proto)425 proto_to_contact_field (const gchar *proto)
426 {
427 	if (!strcmp (proto,  "prpl-oscar"))
428 		return E_CONTACT_IM_AIM;
429 	if (!strcmp (proto, "prpl-novell"))
430 		return E_CONTACT_IM_GROUPWISE;
431 	if (!strcmp (proto, "prpl-msn"))
432 		return E_CONTACT_IM_MSN;
433 	if (!strcmp (proto, "prpl-icq"))
434 		return E_CONTACT_IM_ICQ;
435 	if (!strcmp (proto, "prpl-yahoo"))
436 		return E_CONTACT_IM_YAHOO;
437 	if (!strcmp (proto, "prpl-jabber"))
438 		return E_CONTACT_IM_JABBER;
439 	if (!strcmp (proto, "prpl-gg"))
440 		return E_CONTACT_IM_GADUGADU;
441 	if (!strcmp (proto, "prpl-matrix"))
442 		return E_CONTACT_IM_MATRIX;
443 
444 	return E_CONTACT_IM_AIM;
445 }
446 
447 static void
get_all_blocked(xmlNodePtr node,GSList ** blocked)448 get_all_blocked (xmlNodePtr node,
449                  GSList **blocked)
450 {
451 	xmlNodePtr child;
452 
453 	if (!node || !blocked)
454 		return;
455 
456 	for (child = node->children; child; child = child->next) {
457 		if (child->children)
458 			get_all_blocked (child, blocked);
459 
460 		if (!strcmp ((const gchar *) child->name, "block")) {
461 			gchar *name = get_node_text (child);
462 
463 			if (name)
464 				*blocked = g_slist_prepend (*blocked, name);
465 		}
466 	}
467 }
468 
469 static void
bbdb_get_gaim_buddy_list(GQueue * out_buddies)470 bbdb_get_gaim_buddy_list (GQueue *out_buddies)
471 {
472 	gchar *blist_path;
473 	xmlDocPtr buddy_xml;
474 	xmlNodePtr root, child, blist;
475 	GSList *blocked = NULL;
476 
477 	blist_path = get_buddy_filename ();
478 
479 	buddy_xml = xmlParseFile (blist_path);
480 	g_free (blist_path);
481 	if (!buddy_xml) {
482 		fprintf (stderr, "bbdb: Could not open Pidgin buddy list.\n");
483 		return;
484 	}
485 
486 	root = xmlDocGetRootElement (buddy_xml);
487 	if (strcmp ((const gchar *) root->name, "purple")) {
488 		fprintf (stderr, "bbdb: Could not parse Pidgin buddy list.\n");
489 		xmlFreeDoc (buddy_xml);
490 		return;
491 	}
492 
493 	for (child = root->children; child != NULL; child = child->next) {
494 		if (!strcmp ((const gchar *) child->name, "privacy")) {
495 			get_all_blocked (child, &blocked);
496 			break;
497 		}
498 	}
499 
500 	blist = NULL;
501 	for (child = root->children; child != NULL; child = child->next) {
502 		if (!strcmp ((const gchar *) child->name, "blist")) {
503 			blist = child;
504 			break;
505 		}
506 	}
507 	if (blist == NULL) {
508 		fprintf (
509 			stderr, "bbdb: Could not find 'blist' "
510 			"element in Pidgin buddy list.\n");
511 		xmlFreeDoc (buddy_xml);
512 		return;
513 	}
514 
515 	for (child = blist->children; child != NULL; child = child->next) {
516 		if (!strcmp ((const gchar *) child->name, "group"))
517 			parse_buddy_group (child, out_buddies, blocked);
518 	}
519 
520 	xmlFreeDoc (buddy_xml);
521 
522 	g_slist_foreach (blocked, (GFunc) g_free, NULL);
523 	g_slist_free (blocked);
524 }
525 
526 static gchar *
get_node_text(xmlNodePtr node)527 get_node_text (xmlNodePtr node)
528 {
529 	if (node->children == NULL || node->children->content == NULL ||
530 	    strcmp ((gchar *) node->children->name, "text"))
531 		return NULL;
532 
533 	return g_strdup ((gchar *) node->children->content);
534 }
535 
536 static gchar *
get_buddy_icon_from_setting(xmlNodePtr setting)537 get_buddy_icon_from_setting (xmlNodePtr setting)
538 {
539 	gchar *icon = NULL;
540 
541 	icon = get_node_text (setting);
542 	if (icon[0] != '/') {
543 		gchar *path;
544 
545 		path = g_build_path ("/", g_get_home_dir (), ".purple/icons", icon, NULL);
546 		g_free (icon);
547 		icon = path;
548 	}
549 
550 	return icon;
551 }
552 
553 static void
parse_contact(xmlNodePtr contact,GQueue * out_buddies,GSList * blocked)554 parse_contact (xmlNodePtr contact,
555                GQueue *out_buddies,
556                GSList *blocked)
557 {
558 	xmlNodePtr  child;
559 	xmlNodePtr  buddy = NULL;
560 	GaimBuddy  *gb;
561 	gboolean    is_blocked = FALSE;
562 
563 	for (child = contact->children; child != NULL; child = child->next) {
564 		if (!strcmp ((const gchar *) child->name, "buddy")) {
565 			buddy = child;
566 			break;
567 		}
568 	}
569 
570 	if (buddy == NULL) {
571 		fprintf (
572 			stderr, "bbdb: Could not find buddy in contact. "
573 			"Malformed Pidgin buddy list file.\n");
574 		return;
575 	}
576 
577 	gb = g_new0 (GaimBuddy, 1);
578 
579 	gb->proto = e_xml_get_string_prop_by_name (buddy, (const guchar *)"proto");
580 
581 	for (child = buddy->children; child != NULL && !is_blocked; child = child->next) {
582 		if (!strcmp ((const gchar *) child->name, "setting")) {
583 			gchar *setting_type;
584 
585 			setting_type = e_xml_get_string_prop_by_name (
586 				child, (const guchar *)"name");
587 
588 			if (!strcmp ((const gchar *) setting_type, "buddy_icon"))
589 				gb->icon = get_buddy_icon_from_setting (child);
590 
591 			g_free (setting_type);
592 		} else if (!strcmp ((const gchar *) child->name, "name")) {
593 			gb->account_name = get_node_text (child);
594 			is_blocked = g_slist_find_custom (
595 				blocked, gb->account_name,
596 				(GCompareFunc) strcmp) != NULL;
597 		} else if (!strcmp ((const gchar *) child->name, "alias"))
598 			gb->alias = get_node_text (child);
599 
600 	}
601 
602 	if (is_blocked)
603 		free_gaim_body (gb);
604 	else
605 		g_queue_push_tail (out_buddies, gb);
606 }
607 
608 static void
parse_buddy_group(xmlNodePtr group,GQueue * out_buddies,GSList * blocked)609 parse_buddy_group (xmlNodePtr group,
610                    GQueue *out_buddies,
611                    GSList *blocked)
612 {
613 	xmlNodePtr child;
614 
615 	for (child = group->children; child != NULL; child = child->next) {
616 		if (strcmp ((const gchar *) child->name, "contact"))
617 			continue;
618 
619 		parse_contact (child, out_buddies, blocked);
620 	}
621 }
622