1 /**
2  * @file buddyicon.c Buddy Icon API
3  * @ingroup core
4  */
5 
6 /* purple
7  *
8  * Purple is the legal property of its developers, whose names are too numerous
9  * to list here.  Please refer to the COPYRIGHT file distributed with this
10  * source distribution.
11  *
12  * This program is free software; you can redistribute it and/or modify
13  * it under the terms of the GNU General Public License as published by
14  * the Free Software Foundation; either version 2 of the License, or
15  * (at your option) any later version.
16  *
17  * This program is distributed in the hope that it will be useful,
18  * but WITHOUT ANY WARRANTY; without even the implied warranty of
19  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20  * GNU General Public License for more details.
21  *
22  * You should have received a copy of the GNU General Public License
23  * along with this program; if not, write to the Free Software
24  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02111-1301  USA
25  */
26 #define _PURPLE_BUDDYICON_C_
27 
28 #include "internal.h"
29 #include "buddyicon.h"
30 #include "conversation.h"
31 #include "dbus-maybe.h"
32 #include "debug.h"
33 #include "imgstore.h"
34 #include "util.h"
35 
36 /* NOTE: Instances of this struct are allocated without zeroing the memory, so
37  * NOTE: be sure to update purple_buddy_icon_new() if you add members. */
38 struct _PurpleBuddyIcon
39 {
40 	PurpleAccount *account;    /**< The account the user is on.          */
41 	PurpleStoredImage *img;    /**< The stored image containing
42 	                                the icon data.                       */
43 	char *username;            /**< The username the icon belongs to.    */
44 	char *checksum;            /**< The protocol checksum.               */
45 	int ref_count;             /**< The buddy icon reference count.      */
46 };
47 
48 /**
49  * This is the big grand daddy hash table that contains references to
50  * everybody's buddy icons.
51  *
52  * Key is a PurpleAccount.
53  * Value is another hash table, usually referred to as "icon_cache."
54  * For this inner hash table:
55  *    Key is the username of the buddy whose icon is being stored.
56  *    Value is the PurpleBuddyIcon for this buddy.
57  */
58 static GHashTable *account_cache = NULL;
59 
60 /**
61  * This hash table contains a bunch of PurpleStoredImages that are
62  * shared across all accounts.
63  *
64  * Key is the filename for this image as constructed by
65  * purple_util_get_image_filename().  So it is the base16 encoded
66  * sha-1 hash plus an appropriate file extension.  For example:
67  *   "0f4972d17d1e70e751c43c90c948e72efbff9796.gif"
68  *
69  * The value is a PurpleStoredImage containing the icon data.  These
70  * images are reference counted, and when the count reaches 0
71  * imgstore.c emits the image-deleting signal and we remove the image
72  * from the hash table (but it might still be saved on disk, if the
73  * icon is being used by offline accounts or some such).
74  */
75 static GHashTable *icon_data_cache = NULL;
76 
77 /**
78  * This hash table contains references counts for how many times each
79  * icon in the ~/.purple/icons/ directory is being used.  It's pretty
80  * crazy.  It maintains the reference count across sessions, too, so
81  * if you exit Pidgin then this hash table is reconstructed the next
82  * time Pidgin starts.
83  *
84  * Key is the filename for this image as constructed by
85  * purple_util_get_image_filename().  So it is the base16 encoded
86  * sha-1 hash plus an appropriate file extension.  For example:
87  *   "0f4972d17d1e70e751c43c90c948e72efbff9796.gif"
88  *
89  * The value is a GINT_TO_POINTER count of the number of times this
90  * icon is used.  So if four of your buddies are using an icon, and
91  * you have the icon set for two of your accounts, then this number
92  * will be six.  When this reference count reaches 0 the icon will
93  * be deleted from disk.
94  */
95 static GHashTable *icon_file_cache = NULL;
96 
97 /**
98  * This hash table is used for both custom buddy icons on PurpleBlistNodes and
99  * account icons.
100  */
101 static GHashTable *pointer_icon_cache = NULL;
102 
103 static char       *cache_dir     = NULL;
104 
105 /** "Should icons be cached to disk?" */
106 static gboolean    icon_caching  = TRUE;
107 
108 /* For ~/.gaim to ~/.purple migration. */
109 static char *old_icons_dir = NULL;
110 
111 static void delete_buddy_icon_settings(PurpleBlistNode *node, const char *setting_name);
112 
113 /*
114  * Begin functions for dealing with the on-disk icon cache
115  */
116 
117 static void
ref_filename(const char * filename)118 ref_filename(const char *filename)
119 {
120 	int refs;
121 
122 	g_return_if_fail(filename != NULL);
123 
124 	refs = GPOINTER_TO_INT(g_hash_table_lookup(icon_file_cache, filename));
125 
126 	g_hash_table_insert(icon_file_cache, g_strdup(filename),
127 	                    GINT_TO_POINTER(refs + 1));
128 }
129 
130 static void
unref_filename(const char * filename)131 unref_filename(const char *filename)
132 {
133 	int refs;
134 
135 	if (filename == NULL)
136 		return;
137 
138 	refs = GPOINTER_TO_INT(g_hash_table_lookup(icon_file_cache, filename));
139 
140 	if (refs == 1)
141 	{
142 		g_hash_table_remove(icon_file_cache, filename);
143 	}
144 	else
145 	{
146 		g_hash_table_insert(icon_file_cache, g_strdup(filename),
147 		                    GINT_TO_POINTER(refs - 1));
148 	}
149 }
150 
151 static void
purple_buddy_icon_data_cache(PurpleStoredImage * img)152 purple_buddy_icon_data_cache(PurpleStoredImage *img)
153 {
154 	const char *dirname;
155 	char *path;
156 
157 	g_return_if_fail(img != NULL);
158 
159 	if (!purple_buddy_icons_is_caching())
160 		return;
161 
162 	dirname  = purple_buddy_icons_get_cache_dir();
163 	path = g_build_filename(dirname, purple_imgstore_get_filename(img), NULL);
164 
165 	if (!g_file_test(dirname, G_FILE_TEST_IS_DIR))
166 	{
167 		purple_debug_info("buddyicon", "Creating icon cache directory.\n");
168 
169 		if (g_mkdir(dirname, S_IRUSR | S_IWUSR | S_IXUSR) < 0)
170 		{
171 			purple_debug_error("buddyicon",
172 			                   "Unable to create directory %s: %s\n",
173 			                   dirname, g_strerror(errno));
174 		}
175 	}
176 
177 	if (!g_file_test(path, G_FILE_TEST_IS_REGULAR))
178 	{
179 		purple_util_write_data_to_file_absolute(path, purple_imgstore_get_data(img),
180 							purple_imgstore_get_size(img));
181 	}
182 
183 	g_free(path);
184 }
185 
186 static void
purple_buddy_icon_data_uncache_file(const char * filename)187 purple_buddy_icon_data_uncache_file(const char *filename)
188 {
189 	const char *dirname;
190 	char *path;
191 
192 	g_return_if_fail(filename != NULL);
193 
194 	/* It's possible that there are other references to this icon
195 	 * cache file that are not currently loaded into memory. */
196 	if (GPOINTER_TO_INT(g_hash_table_lookup(icon_file_cache, filename)))
197 		return;
198 
199 	dirname  = purple_buddy_icons_get_cache_dir();
200 	path = g_build_filename(dirname, filename, NULL);
201 
202 	if (g_file_test(path, G_FILE_TEST_EXISTS))
203 	{
204 		if (g_unlink(path))
205 		{
206 			purple_debug_error("buddyicon", "Failed to delete %s: %s\n",
207 			                   path, g_strerror(errno));
208 		}
209 		else
210 		{
211 			purple_debug_info("buddyicon", "Deleted cache file: %s\n", path);
212 		}
213 	}
214 
215 	g_free(path);
216 }
217 
218 /*
219  * End functions for dealing with the on-disk icon cache
220  */
221 
222 /*
223  * Begin functions for dealing with the in-memory icon cache
224  */
225 
226 static gboolean
value_equals(gpointer key,gpointer value,gpointer user_data)227 value_equals(gpointer key, gpointer value, gpointer user_data)
228 {
229 	return (value == user_data);
230 }
231 
232 static void
image_deleting_cb(const PurpleStoredImage * img,gpointer data)233 image_deleting_cb(const PurpleStoredImage *img, gpointer data)
234 {
235 	const char *filename = purple_imgstore_get_filename(img);
236 
237 	/* If there's no filename, it can't be one of our images. */
238 	if (filename == NULL)
239 		return;
240 
241 	if (img == g_hash_table_lookup(icon_data_cache, filename))
242 	{
243 		purple_buddy_icon_data_uncache_file(filename);
244 		g_hash_table_remove(icon_data_cache, filename);
245 
246 		/* We could make this O(1) by using another hash table, but
247 		 * this is probably good enough. */
248 		g_hash_table_foreach_remove(pointer_icon_cache, value_equals, (gpointer)img);
249 	}
250 }
251 
252 static PurpleStoredImage *
purple_buddy_icon_data_new(guchar * icon_data,size_t icon_len,const char * filename)253 purple_buddy_icon_data_new(guchar *icon_data, size_t icon_len, const char *filename)
254 {
255 	char *file;
256 	PurpleStoredImage *img;
257 
258 	g_return_val_if_fail(icon_data != NULL, NULL);
259 	g_return_val_if_fail(icon_len  > 0,     NULL);
260 
261 	if (filename == NULL)
262 	{
263 		file = purple_util_get_image_filename(icon_data, icon_len);
264 		if (file == NULL)
265 		{
266 			g_free(icon_data);
267 			return NULL;
268 		}
269 	}
270 	else
271 		file = g_strdup(filename);
272 
273 	if ((img = g_hash_table_lookup(icon_data_cache, file)))
274 	{
275 		g_free(file);
276 		g_free(icon_data);
277 		return purple_imgstore_ref(img);
278 	}
279 
280 	img = purple_imgstore_add(icon_data, icon_len, file);
281 
282 	/* This will take ownership of file and g_free it either now or later. */
283 	g_hash_table_insert(icon_data_cache, file, img);
284 
285 	purple_buddy_icon_data_cache(img);
286 
287 	return img;
288 }
289 
290 /*
291  * End functions for dealing with the in-memory icon cache
292  */
293 
294 static PurpleBuddyIcon *
purple_buddy_icon_create(PurpleAccount * account,const char * username)295 purple_buddy_icon_create(PurpleAccount *account, const char *username)
296 {
297 	PurpleBuddyIcon *icon;
298 	GHashTable *icon_cache;
299 
300 	/* This does not zero.  See purple_buddy_icon_new() for
301 	 * information on which function allocates which member. */
302 	icon = g_slice_new(PurpleBuddyIcon);
303 	PURPLE_DBUS_REGISTER_POINTER(icon, PurpleBuddyIcon);
304 
305 	icon->account = account;
306 	icon->username = g_strdup(username);
307 	icon->checksum = NULL;
308 	icon->ref_count = 1;
309 
310 	icon_cache = g_hash_table_lookup(account_cache, account);
311 
312 	if (icon_cache == NULL)
313 	{
314 		icon_cache = g_hash_table_new(g_str_hash, g_str_equal);
315 
316 		g_hash_table_insert(account_cache, account, icon_cache);
317 	}
318 
319 	g_hash_table_insert(icon_cache,
320 	                    (char *)purple_buddy_icon_get_username(icon), icon);
321 	return icon;
322 }
323 
324 PurpleBuddyIcon *
purple_buddy_icon_new(PurpleAccount * account,const char * username,void * icon_data,size_t icon_len,const char * checksum)325 purple_buddy_icon_new(PurpleAccount *account, const char *username,
326                       void *icon_data, size_t icon_len,
327                       const char *checksum)
328 {
329 	PurpleBuddyIcon *icon;
330 
331 	g_return_val_if_fail(account   != NULL, NULL);
332 	g_return_val_if_fail(username  != NULL, NULL);
333 	g_return_val_if_fail(icon_data != NULL, NULL);
334 	g_return_val_if_fail(icon_len  > 0,    NULL);
335 
336 	/* purple_buddy_icons_find() does allocation, so be
337 	 * sure to update it as well when members are added. */
338 	icon = purple_buddy_icons_find(account, username);
339 
340 	/* purple_buddy_icon_create() sets account & username */
341 	if (icon == NULL)
342 		icon = purple_buddy_icon_create(account, username);
343 
344 	/* purple_buddy_icon_set_data() sets img, but it
345 	 * references img first, so we need to initialize it */
346 	icon->img = NULL;
347 	purple_buddy_icon_set_data(icon, icon_data, icon_len, checksum);
348 
349 	return icon;
350 }
351 
352 PurpleBuddyIcon *
purple_buddy_icon_ref(PurpleBuddyIcon * icon)353 purple_buddy_icon_ref(PurpleBuddyIcon *icon)
354 {
355 	g_return_val_if_fail(icon != NULL, NULL);
356 
357 	icon->ref_count++;
358 
359 	return icon;
360 }
361 
362 PurpleBuddyIcon *
purple_buddy_icon_unref(PurpleBuddyIcon * icon)363 purple_buddy_icon_unref(PurpleBuddyIcon *icon)
364 {
365 	if (icon == NULL)
366 		return NULL;
367 
368 	g_return_val_if_fail(icon->ref_count > 0, NULL);
369 
370 	icon->ref_count--;
371 
372 	if (icon->ref_count == 0)
373 	{
374 		GHashTable *icon_cache = g_hash_table_lookup(account_cache, purple_buddy_icon_get_account(icon));
375 
376 		if (icon_cache != NULL)
377 			g_hash_table_remove(icon_cache, purple_buddy_icon_get_username(icon));
378 
379 		g_free(icon->username);
380 		g_free(icon->checksum);
381 		purple_imgstore_unref(icon->img);
382 
383 		PURPLE_DBUS_UNREGISTER_POINTER(icon);
384 		g_slice_free(PurpleBuddyIcon, icon);
385 
386 		return NULL;
387 	}
388 
389 	return icon;
390 }
391 
392 void
purple_buddy_icon_update(PurpleBuddyIcon * icon)393 purple_buddy_icon_update(PurpleBuddyIcon *icon)
394 {
395 	PurpleConversation *conv;
396 	PurpleAccount *account;
397 	const char *username;
398 	PurpleBuddyIcon *icon_to_set;
399 	GSList *buddies;
400 
401 	g_return_if_fail(icon != NULL);
402 
403 	account  = purple_buddy_icon_get_account(icon);
404 	username = purple_buddy_icon_get_username(icon);
405 
406 	/* If no data exists (icon->img == NULL), then call the functions below
407 	 * with NULL to unset the icon.  They will then unref the icon and it should
408 	 * be destroyed.  The only way it wouldn't be destroyed is if someone
409 	 * else is holding a reference to it, in which case they can kill
410 	 * the icon when they realize it has no data. */
411 	icon_to_set = icon->img ? icon : NULL;
412 
413 	/* Ensure that icon remains valid throughout */
414 	purple_buddy_icon_ref(icon);
415 
416 	buddies = purple_find_buddies(account, username);
417 	while (buddies != NULL)
418 	{
419 		PurpleBuddy *buddy = (PurpleBuddy *)buddies->data;
420 		char *old_icon;
421 
422 		purple_buddy_set_icon(buddy, icon_to_set);
423 		old_icon = g_strdup(purple_blist_node_get_string((PurpleBlistNode *)buddy,
424 		                                                 "buddy_icon"));
425 		if (icon->img && purple_buddy_icons_is_caching())
426 		{
427 			const char *filename = purple_imgstore_get_filename(icon->img);
428 			purple_blist_node_set_string((PurpleBlistNode *)buddy,
429 			                             "buddy_icon",
430 			                             filename);
431 
432 			if (icon->checksum && *icon->checksum)
433 			{
434 				purple_blist_node_set_string((PurpleBlistNode *)buddy,
435 				                             "icon_checksum",
436 				                             icon->checksum);
437 			}
438 			else
439 			{
440 				purple_blist_node_remove_setting((PurpleBlistNode *)buddy,
441 				                                 "icon_checksum");
442 			}
443 			ref_filename(filename);
444 		}
445 		else if (!icon->img)
446 		{
447 			purple_blist_node_remove_setting((PurpleBlistNode *)buddy, "buddy_icon");
448 			purple_blist_node_remove_setting((PurpleBlistNode *)buddy, "icon_checksum");
449 		}
450 		unref_filename(old_icon);
451 		g_free(old_icon);
452 
453 		buddies = g_slist_delete_link(buddies, buddies);
454 	}
455 
456 	conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, username, account);
457 
458 	if (conv != NULL)
459 		purple_conv_im_set_icon(PURPLE_CONV_IM(conv), icon_to_set);
460 
461 	/* icon's refcount was incremented above */
462 	purple_buddy_icon_unref(icon);
463 }
464 
465 void
purple_buddy_icon_set_data(PurpleBuddyIcon * icon,guchar * data,size_t len,const char * checksum)466 purple_buddy_icon_set_data(PurpleBuddyIcon *icon, guchar *data,
467                            size_t len, const char *checksum)
468 {
469 	PurpleStoredImage *old_img;
470 
471 	g_return_if_fail(icon != NULL);
472 
473 	old_img = icon->img;
474 	icon->img = NULL;
475 
476 	if (data != NULL)
477 	{
478 		if (len > 0)
479 			icon->img = purple_buddy_icon_data_new(data, len, NULL);
480 		else
481 			g_free(data);
482 	}
483 
484 	g_free(icon->checksum);
485 	icon->checksum = g_strdup(checksum);
486 
487 	purple_buddy_icon_update(icon);
488 
489 	purple_imgstore_unref(old_img);
490 }
491 
492 PurpleAccount *
purple_buddy_icon_get_account(const PurpleBuddyIcon * icon)493 purple_buddy_icon_get_account(const PurpleBuddyIcon *icon)
494 {
495 	g_return_val_if_fail(icon != NULL, NULL);
496 
497 	return icon->account;
498 }
499 
500 const char *
purple_buddy_icon_get_username(const PurpleBuddyIcon * icon)501 purple_buddy_icon_get_username(const PurpleBuddyIcon *icon)
502 {
503 	g_return_val_if_fail(icon != NULL, NULL);
504 
505 	return icon->username;
506 }
507 
508 const char *
purple_buddy_icon_get_checksum(const PurpleBuddyIcon * icon)509 purple_buddy_icon_get_checksum(const PurpleBuddyIcon *icon)
510 {
511 	g_return_val_if_fail(icon != NULL, NULL);
512 
513 	return icon->checksum;
514 }
515 
516 gconstpointer
purple_buddy_icon_get_data(const PurpleBuddyIcon * icon,size_t * len)517 purple_buddy_icon_get_data(const PurpleBuddyIcon *icon, size_t *len)
518 {
519 	g_return_val_if_fail(icon != NULL, NULL);
520 
521 	if (icon->img)
522 	{
523 		if (len != NULL)
524 			*len = purple_imgstore_get_size(icon->img);
525 
526 		return purple_imgstore_get_data(icon->img);
527 	}
528 
529 	return NULL;
530 }
531 
532 const char *
purple_buddy_icon_get_extension(const PurpleBuddyIcon * icon)533 purple_buddy_icon_get_extension(const PurpleBuddyIcon *icon)
534 {
535 	if (icon->img != NULL)
536 		return purple_imgstore_get_extension(icon->img);
537 
538 	return NULL;
539 }
540 
541 void
purple_buddy_icons_set_for_user(PurpleAccount * account,const char * username,void * icon_data,size_t icon_len,const char * checksum)542 purple_buddy_icons_set_for_user(PurpleAccount *account, const char *username,
543                                 void *icon_data, size_t icon_len,
544                                 const char *checksum)
545 {
546 	GHashTable *icon_cache;
547 	PurpleBuddyIcon *icon = NULL;
548 
549 	g_return_if_fail(account  != NULL);
550 	g_return_if_fail(username != NULL);
551 
552 	icon_cache = g_hash_table_lookup(account_cache, account);
553 
554 	if (icon_cache != NULL)
555 		icon = g_hash_table_lookup(icon_cache, username);
556 
557 	if (icon != NULL)
558 		purple_buddy_icon_set_data(icon, icon_data, icon_len, checksum);
559 	else if (icon_data && icon_len > 0)
560 	{
561 		PurpleBuddyIcon *icon = purple_buddy_icon_new(account, username, icon_data, icon_len, checksum);
562 
563 		/* purple_buddy_icon_new() calls
564 		 * purple_buddy_icon_set_data(), which calls
565 		 * purple_buddy_icon_update(), which has the buddy list
566 		 * and conversations take references as appropriate.
567 		 * This function doesn't return icon, so we can't
568 		 * leave a reference dangling. */
569 		purple_buddy_icon_unref(icon);
570 	}
571 	else
572 	{
573 		/* If the buddy list or a conversation was holding a
574 		 * reference, we'd have found the icon in the cache.
575 		 * Since we know we're deleting the icon, we only
576 		 * need a subset of purple_buddy_icon_update(). */
577 
578 		GSList *buddies = purple_find_buddies(account, username);
579 		while (buddies != NULL)
580 		{
581 			PurpleBuddy *buddy = (PurpleBuddy *)buddies->data;
582 
583 			unref_filename(purple_blist_node_get_string((PurpleBlistNode *)buddy, "buddy_icon"));
584 			purple_blist_node_remove_setting((PurpleBlistNode *)buddy, "buddy_icon");
585 			purple_blist_node_remove_setting((PurpleBlistNode *)buddy, "icon_checksum");
586 
587 			buddies = g_slist_delete_link(buddies, buddies);
588 		}
589 	}
590 }
591 
purple_buddy_icon_get_full_path(PurpleBuddyIcon * icon)592 char *purple_buddy_icon_get_full_path(PurpleBuddyIcon *icon)
593 {
594 	char *path;
595 
596 	g_return_val_if_fail(icon != NULL, NULL);
597 
598 	if (icon->img == NULL)
599 		return NULL;
600 
601 	path = g_build_filename(purple_buddy_icons_get_cache_dir(),
602 	                        purple_imgstore_get_filename(icon->img), NULL);
603 	if (!g_file_test(path, G_FILE_TEST_EXISTS))
604 	{
605 		g_free(path);
606 		return NULL;
607 	}
608 	return path;
609 }
610 
611 const char *
purple_buddy_icons_get_checksum_for_user(PurpleBuddy * buddy)612 purple_buddy_icons_get_checksum_for_user(PurpleBuddy *buddy)
613 {
614 	return purple_blist_node_get_string((PurpleBlistNode*)buddy,
615 	                                    "icon_checksum");
616 }
617 
618 static gboolean
read_icon_file(const char * path,guchar ** data,size_t * len)619 read_icon_file(const char *path, guchar **data, size_t *len)
620 {
621 	GError *err = NULL;
622 
623 	if (!g_file_get_contents(path, (gchar **)data, len, &err))
624 	{
625 		purple_debug_error("buddyicon", "Error reading %s: %s\n",
626 		                   path, err->message);
627 		g_error_free(err);
628 
629 		return FALSE;
630 	}
631 
632 	return TRUE;
633 }
634 
635 PurpleBuddyIcon *
purple_buddy_icons_find(PurpleAccount * account,const char * username)636 purple_buddy_icons_find(PurpleAccount *account, const char *username)
637 {
638 	GHashTable *icon_cache;
639 	PurpleBuddyIcon *icon = NULL;
640 
641 	g_return_val_if_fail(account  != NULL, NULL);
642 	g_return_val_if_fail(username != NULL, NULL);
643 
644 	icon_cache = g_hash_table_lookup(account_cache, account);
645 
646 	if ((icon_cache == NULL) || ((icon = g_hash_table_lookup(icon_cache, username)) == NULL))
647 	{
648 		PurpleBuddy *b = purple_find_buddy(account, username);
649 		const char *protocol_icon_file;
650 		const char *dirname;
651 		gboolean caching;
652 		guchar *data;
653 		size_t len;
654 
655 		if (!b)
656 			return NULL;
657 
658 		protocol_icon_file = purple_blist_node_get_string((PurpleBlistNode*)b, "buddy_icon");
659 
660 		if (protocol_icon_file == NULL)
661 			return NULL;
662 
663 		dirname = purple_buddy_icons_get_cache_dir();
664 
665 		caching = purple_buddy_icons_is_caching();
666 		/* By disabling caching temporarily, we avoid a loop
667 		 * and don't have to add special code through several
668 		 * functions. */
669 		purple_buddy_icons_set_caching(FALSE);
670 
671 		if (protocol_icon_file != NULL)
672 		{
673 			char *path = g_build_filename(dirname, protocol_icon_file, NULL);
674 			if (read_icon_file(path, &data, &len))
675 			{
676 				const char *checksum;
677 
678 				icon = purple_buddy_icon_create(account, username);
679 				icon->img = NULL;
680 				checksum = purple_blist_node_get_string((PurpleBlistNode*)b, "icon_checksum");
681 				purple_buddy_icon_set_data(icon, data, len, checksum);
682 			}
683 			else
684 				delete_buddy_icon_settings((PurpleBlistNode*)b, "buddy_icon");
685 
686 			g_free(path);
687 		}
688 
689 		purple_buddy_icons_set_caching(caching);
690 	}
691 
692 	return (icon ? purple_buddy_icon_ref(icon) : NULL);
693 }
694 
695 PurpleStoredImage *
purple_buddy_icons_find_account_icon(PurpleAccount * account)696 purple_buddy_icons_find_account_icon(PurpleAccount *account)
697 {
698 	PurpleStoredImage *img;
699 	const char *account_icon_file;
700 	const char *dirname;
701 	char *path;
702 	guchar *data;
703 	size_t len;
704 
705 	g_return_val_if_fail(account != NULL, NULL);
706 
707 	if ((img = g_hash_table_lookup(pointer_icon_cache, account)))
708 	{
709 		return purple_imgstore_ref(img);
710 	}
711 
712 	account_icon_file = purple_account_get_string(account, "buddy_icon", NULL);
713 
714 	if (account_icon_file == NULL)
715 		return NULL;
716 
717 	dirname = purple_buddy_icons_get_cache_dir();
718 	path = g_build_filename(dirname, account_icon_file, NULL);
719 
720 	if (read_icon_file(path, &data, &len))
721 	{
722 		g_free(path);
723 		img = purple_buddy_icons_set_account_icon(account, data, len);
724 		return purple_imgstore_ref(img);
725 	}
726 	g_free(path);
727 
728 	return NULL;
729 }
730 
731 PurpleStoredImage *
purple_buddy_icons_set_account_icon(PurpleAccount * account,guchar * icon_data,size_t icon_len)732 purple_buddy_icons_set_account_icon(PurpleAccount *account,
733                                     guchar *icon_data, size_t icon_len)
734 {
735 	PurpleStoredImage *old_img;
736 	PurpleStoredImage *img = NULL;
737 	char *old_icon;
738 
739 	if (icon_data != NULL && icon_len > 0)
740 	{
741 		img = purple_buddy_icon_data_new(icon_data, icon_len, NULL);
742 	}
743 
744 	old_icon = g_strdup(purple_account_get_string(account, "buddy_icon", NULL));
745 	if (img && purple_buddy_icons_is_caching())
746 	{
747 		const char *filename = purple_imgstore_get_filename(img);
748 		purple_account_set_string(account, "buddy_icon", filename);
749 		purple_account_set_int(account, "buddy_icon_timestamp", time(NULL));
750 		ref_filename(filename);
751 	}
752 	else
753 	{
754 		purple_account_set_string(account, "buddy_icon", NULL);
755 		purple_account_set_int(account, "buddy_icon_timestamp", 0);
756 	}
757 	unref_filename(old_icon);
758 
759 	old_img = g_hash_table_lookup(pointer_icon_cache, account);
760 
761 	if (img)
762 		g_hash_table_insert(pointer_icon_cache, account, img);
763 	else
764 		g_hash_table_remove(pointer_icon_cache, account);
765 
766 	if (purple_account_is_connected(account))
767 	{
768 		PurpleConnection *gc;
769 		PurplePluginProtocolInfo *prpl_info;
770 
771 		gc = purple_account_get_connection(account);
772 		prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(purple_connection_get_prpl(gc));
773 
774 		if (prpl_info && prpl_info->set_buddy_icon)
775 			prpl_info->set_buddy_icon(gc, img);
776 	}
777 
778 	if (old_img)
779 		purple_imgstore_unref(old_img);
780 	else if (old_icon)
781 	{
782 		/* The old icon may not have been loaded into memory.  In that
783 		 * case, we'll need to uncache the filename.  The filenames
784 		 * are ref-counted, so this is safe. */
785 		purple_buddy_icon_data_uncache_file(old_icon);
786 	}
787 	g_free(old_icon);
788 
789 	return img;
790 }
791 
792 time_t
purple_buddy_icons_get_account_icon_timestamp(PurpleAccount * account)793 purple_buddy_icons_get_account_icon_timestamp(PurpleAccount *account)
794 {
795 	time_t ret;
796 
797 	g_return_val_if_fail(account != NULL, 0);
798 
799 	ret = purple_account_get_int(account, "buddy_icon_timestamp", 0);
800 
801 	/* This deals with migration cases. */
802 	if (ret == 0 && purple_account_get_string(account, "buddy_icon", NULL) != NULL)
803 	{
804 		ret = time(NULL);
805 		purple_account_set_int(account, "buddy_icon_timestamp", ret);
806 	}
807 
808 	return ret;
809 }
810 
811 gboolean
purple_buddy_icons_node_has_custom_icon(PurpleBlistNode * node)812 purple_buddy_icons_node_has_custom_icon(PurpleBlistNode *node)
813 {
814 	g_return_val_if_fail(node != NULL, FALSE);
815 
816 	return (purple_blist_node_get_string(node, "custom_buddy_icon") != NULL);
817 }
818 
819 PurpleStoredImage *
purple_buddy_icons_node_find_custom_icon(PurpleBlistNode * node)820 purple_buddy_icons_node_find_custom_icon(PurpleBlistNode *node)
821 {
822 	char *path;
823 	size_t len;
824 	guchar *data;
825 	PurpleStoredImage *img;
826 	const char *custom_icon_file, *dirname;
827 
828 	g_return_val_if_fail(node != NULL, NULL);
829 
830 	if ((img = g_hash_table_lookup(pointer_icon_cache, node)))
831 	{
832 		return purple_imgstore_ref(img);
833 	}
834 
835 	custom_icon_file = purple_blist_node_get_string(node,
836 	                                                "custom_buddy_icon");
837 
838 	if (custom_icon_file == NULL)
839 		return NULL;
840 
841 	dirname = purple_buddy_icons_get_cache_dir();
842 	path = g_build_filename(dirname, custom_icon_file, NULL);
843 
844 	if (read_icon_file(path, &data, &len))
845 	{
846 		g_free(path);
847 		img = purple_buddy_icons_node_set_custom_icon(node, data, len);
848 		return purple_imgstore_ref(img);
849 	}
850 	g_free(path);
851 
852 	return NULL;
853 }
854 
855 PurpleStoredImage *
purple_buddy_icons_node_set_custom_icon(PurpleBlistNode * node,guchar * icon_data,size_t icon_len)856 purple_buddy_icons_node_set_custom_icon(PurpleBlistNode *node,
857                                         guchar *icon_data, size_t icon_len)
858 {
859 	char *old_icon;
860 	PurpleStoredImage *old_img;
861 	PurpleStoredImage *img = NULL;
862 
863 	g_return_val_if_fail(node != NULL, NULL);
864 
865 	if (!PURPLE_BLIST_NODE_IS_CONTACT(node) &&
866 	    !PURPLE_BLIST_NODE_IS_CHAT(node) &&
867 	    !PURPLE_BLIST_NODE_IS_GROUP(node)) {
868 		return NULL;
869 	}
870 
871 	old_img = g_hash_table_lookup(pointer_icon_cache, node);
872 
873 	if (icon_data != NULL && icon_len > 0) {
874 		img = purple_buddy_icon_data_new(icon_data, icon_len, NULL);
875 	}
876 
877 	old_icon = g_strdup(purple_blist_node_get_string(node,
878 	                                                 "custom_buddy_icon"));
879 	if (img && purple_buddy_icons_is_caching()) {
880 		const char *filename = purple_imgstore_get_filename(img);
881 		purple_blist_node_set_string(node, "custom_buddy_icon",
882 		                             filename);
883 		ref_filename(filename);
884 	} else {
885 		purple_blist_node_remove_setting(node, "custom_buddy_icon");
886 	}
887 	unref_filename(old_icon);
888 
889 	if (img)
890 		g_hash_table_insert(pointer_icon_cache, node, img);
891 	else
892 		g_hash_table_remove(pointer_icon_cache, node);
893 
894 	if (PURPLE_BLIST_NODE_IS_CONTACT(node)) {
895 		PurpleBlistNode *child;
896 		for (child = purple_blist_node_get_first_child(node);
897 		     child;
898 			 child = purple_blist_node_get_sibling_next(child))
899 		{
900 			PurpleBuddy *buddy;
901 			PurpleConversation *conv;
902 
903 			if (!PURPLE_BLIST_NODE_IS_BUDDY(child))
904 				continue;
905 
906 			buddy = (PurpleBuddy *)child;
907 
908 			conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, purple_buddy_get_name(buddy), purple_buddy_get_account(buddy));
909 			if (conv)
910 				purple_conversation_update(conv, PURPLE_CONV_UPDATE_ICON);
911 
912 			/* Is this call necessary anymore? Can the buddies
913 			 * themselves need updating when the custom buddy
914 			 * icon changes? */
915 			purple_blist_update_node_icon((PurpleBlistNode*)buddy);
916 		}
917 	} else if (PURPLE_BLIST_NODE_IS_CHAT(node)) {
918 		PurpleConversation *conv = NULL;
919 
920 		conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_CHAT, purple_chat_get_name((PurpleChat*)node), purple_chat_get_account((PurpleChat*)node));
921 		if (conv) {
922 			purple_conversation_update(conv, PURPLE_CONV_UPDATE_ICON);
923 		}
924 	}
925 
926 	purple_blist_update_node_icon(node);
927 
928 	if (old_img) {
929 		purple_imgstore_unref(old_img);
930 	} else if (old_icon) {
931 		/* The old icon may not have been loaded into memory.  In that
932 		 * case, we'll need to uncache the filename.  The filenames
933 		 * are ref-counted, so this is safe. */
934 		purple_buddy_icon_data_uncache_file(old_icon);
935 	}
936 	g_free(old_icon);
937 
938 	return img;
939 }
940 
941 PurpleStoredImage *
purple_buddy_icons_node_set_custom_icon_from_file(PurpleBlistNode * node,const gchar * filename)942 purple_buddy_icons_node_set_custom_icon_from_file(PurpleBlistNode *node,
943                                                   const gchar *filename)
944 {
945 	size_t len = 0;
946 	guchar *data = NULL;
947 
948 	g_return_val_if_fail(node != NULL, NULL);
949 
950 	if (!PURPLE_BLIST_NODE_IS_CONTACT(node) &&
951 	    !PURPLE_BLIST_NODE_IS_CHAT(node) &&
952 	    !PURPLE_BLIST_NODE_IS_GROUP(node)) {
953 		return NULL;
954 	}
955 
956 	if (filename != NULL) {
957 		if (!read_icon_file(filename, &data, &len)) {
958 			return NULL;
959 		}
960 	}
961 
962 	return purple_buddy_icons_node_set_custom_icon(node, data, len);
963 }
964 
965 gboolean
purple_buddy_icons_has_custom_icon(PurpleContact * contact)966 purple_buddy_icons_has_custom_icon(PurpleContact *contact)
967 {
968 	return purple_buddy_icons_node_has_custom_icon((PurpleBlistNode*)contact);
969 }
970 
971 PurpleStoredImage *
purple_buddy_icons_find_custom_icon(PurpleContact * contact)972 purple_buddy_icons_find_custom_icon(PurpleContact *contact)
973 {
974 	return purple_buddy_icons_node_find_custom_icon((PurpleBlistNode*)contact);
975 }
976 
977 PurpleStoredImage *
purple_buddy_icons_set_custom_icon(PurpleContact * contact,guchar * icon_data,size_t icon_len)978 purple_buddy_icons_set_custom_icon(PurpleContact *contact, guchar *icon_data,
979                                    size_t icon_len)
980 {
981 	return purple_buddy_icons_node_set_custom_icon((PurpleBlistNode*)contact, icon_data, icon_len);
982 }
983 
984 void
_purple_buddy_icon_set_old_icons_dir(const char * dirname)985 _purple_buddy_icon_set_old_icons_dir(const char *dirname)
986 {
987 	old_icons_dir = g_strdup(dirname);
988 }
989 
990 static void
delete_buddy_icon_settings(PurpleBlistNode * node,const char * setting_name)991 delete_buddy_icon_settings(PurpleBlistNode *node, const char *setting_name)
992 {
993 	purple_blist_node_remove_setting(node, setting_name);
994 
995 	if (purple_strequal(setting_name, "buddy_icon"))
996 	{
997 		purple_blist_node_remove_setting(node, "avatar_hash");
998 		purple_blist_node_remove_setting(node, "icon_checksum");
999 	}
1000 }
1001 
1002 static void
migrate_buddy_icon(PurpleBlistNode * node,const char * setting_name,const char * dirname,const char * filename)1003 migrate_buddy_icon(PurpleBlistNode *node, const char *setting_name,
1004                    const char *dirname, const char *filename)
1005 {
1006 	char *path;
1007 
1008 	if (filename[0] != '/')
1009 	{
1010 		path = g_build_filename(dirname, filename, NULL);
1011 		if (g_file_test(path, G_FILE_TEST_EXISTS))
1012 		{
1013 			g_free(path);
1014 			return;
1015 		}
1016 		g_free(path);
1017 
1018 		path = g_build_filename(old_icons_dir, filename, NULL);
1019 	}
1020 	else
1021 		path = g_strdup(filename);
1022 
1023 	if (g_file_test(path, G_FILE_TEST_EXISTS))
1024 	{
1025 		guchar *icon_data;
1026 		size_t icon_len;
1027 		FILE *file;
1028 		char *new_filename;
1029 
1030 		if (!read_icon_file(path, &icon_data, &icon_len))
1031 		{
1032 			g_free(path);
1033 			delete_buddy_icon_settings(node, setting_name);
1034 			return;
1035 		}
1036 
1037 		if (icon_data == NULL || icon_len <= 0)
1038 		{
1039 			/* This really applies to the icon_len check.
1040 			 * icon_data should never be NULL if
1041 			 * read_icon_file() returns TRUE. */
1042 			purple_debug_error("buddyicon", "Empty buddy icon file: %s\n", path);
1043 			delete_buddy_icon_settings(node, setting_name);
1044 			g_free(path);
1045 			return;
1046 		}
1047 
1048 		new_filename = purple_util_get_image_filename(icon_data, icon_len);
1049 		if (new_filename == NULL)
1050 		{
1051 			purple_debug_error("buddyicon",
1052 				"New icon filename is NULL. This should never happen! "
1053 				"The old filename was: %s\n", path);
1054 			g_free(path);
1055 			delete_buddy_icon_settings(node, setting_name);
1056 
1057 			g_return_if_reached();
1058 		}
1059 
1060 		g_free(path);
1061 		path = g_build_filename(dirname, new_filename, NULL);
1062 		if ((file = g_fopen(path, "wb")) != NULL)
1063 		{
1064 			if (!fwrite(icon_data, icon_len, 1, file))
1065 			{
1066 				purple_debug_error("buddyicon", "Error writing %s: %s\n",
1067 				                   path, g_strerror(errno));
1068 			}
1069 			else
1070 				purple_debug_info("buddyicon", "Wrote migrated cache file: %s\n", path);
1071 
1072 			fclose(file);
1073 		}
1074 		else
1075 		{
1076 			purple_debug_error("buddyicon", "Unable to create file %s: %s\n",
1077 			                   path, g_strerror(errno));
1078 			g_free(new_filename);
1079 			g_free(path);
1080 
1081 			delete_buddy_icon_settings(node, setting_name);
1082 			return;
1083 		}
1084 		g_free(path);
1085 
1086 		purple_blist_node_set_string(node,
1087 		                             setting_name,
1088 		                             new_filename);
1089 		ref_filename(new_filename);
1090 
1091 		g_free(new_filename);
1092 
1093 		if (purple_strequal(setting_name, "buddy_icon"))
1094 		{
1095 			const char *hash;
1096 
1097 			hash = purple_blist_node_get_string(node, "avatar_hash");
1098 			if (hash != NULL)
1099 			{
1100 				purple_blist_node_set_string(node, "icon_checksum", hash);
1101 				purple_blist_node_remove_setting(node, "avatar_hash");
1102 			}
1103 		}
1104 	}
1105 	else
1106 	{
1107 		purple_debug_error("buddyicon", "Old icon file doesn't exist: %s\n", path);
1108 		delete_buddy_icon_settings(node, setting_name);
1109 		g_free(path);
1110 	}
1111 }
1112 
1113 void
_purple_buddy_icons_account_loaded_cb()1114 _purple_buddy_icons_account_loaded_cb()
1115 {
1116 	const char *dirname = purple_buddy_icons_get_cache_dir();
1117 	GList *cur;
1118 
1119 	for (cur = purple_accounts_get_all(); cur != NULL; cur = cur->next)
1120 	{
1121 		PurpleAccount *account = cur->data;
1122 		const char *account_icon_file = purple_account_get_string(account, "buddy_icon", NULL);
1123 
1124 		if (account_icon_file != NULL)
1125 		{
1126 			char *path = g_build_filename(dirname, account_icon_file, NULL);
1127 			if (!g_file_test(path, G_FILE_TEST_EXISTS))
1128 			{
1129 				purple_account_set_string(account, "buddy_icon", NULL);
1130 			} else {
1131 				ref_filename(account_icon_file);
1132 			}
1133 			g_free(path);
1134 		}
1135 	}
1136 }
1137 
1138 void
_purple_buddy_icons_blist_loaded_cb()1139 _purple_buddy_icons_blist_loaded_cb()
1140 {
1141 	PurpleBlistNode *node = purple_blist_get_root();
1142 	const char *dirname = purple_buddy_icons_get_cache_dir();
1143 
1144 	/* Doing this once here saves having to check it inside a loop. */
1145 	if (old_icons_dir != NULL)
1146 	{
1147 		if (!g_file_test(dirname, G_FILE_TEST_IS_DIR))
1148 		{
1149 			purple_debug_info("buddyicon", "Creating icon cache directory.\n");
1150 
1151 			if (g_mkdir(dirname, S_IRUSR | S_IWUSR | S_IXUSR) < 0)
1152 			{
1153 				purple_debug_error("buddyicon",
1154 				                   "Unable to create directory %s: %s\n",
1155 				                   dirname, g_strerror(errno));
1156 			}
1157 		}
1158 	}
1159 
1160 	while (node != NULL)
1161 	{
1162 		if (PURPLE_BLIST_NODE_IS_BUDDY(node))
1163 		{
1164 			const char *filename;
1165 
1166 			filename = purple_blist_node_get_string(node, "buddy_icon");
1167 			if (filename != NULL)
1168 			{
1169 				if (old_icons_dir != NULL)
1170 				{
1171 					migrate_buddy_icon(node,
1172 					                   "buddy_icon",
1173 					                   dirname, filename);
1174 				}
1175 				else
1176 				{
1177 					char *path = g_build_filename(dirname, filename, NULL);
1178 					if (!g_file_test(path, G_FILE_TEST_EXISTS))
1179 					{
1180 						purple_blist_node_remove_setting(node,
1181 						                                 "buddy_icon");
1182 						purple_blist_node_remove_setting(node,
1183 						                                 "icon_checksum");
1184 					}
1185 					else
1186 						ref_filename(filename);
1187 					g_free(path);
1188 				}
1189 			}
1190 		}
1191 		else if (PURPLE_BLIST_NODE_IS_CONTACT(node) ||
1192 		         PURPLE_BLIST_NODE_IS_CHAT(node) ||
1193 		         PURPLE_BLIST_NODE_IS_GROUP(node))
1194 		{
1195 			const char *filename;
1196 
1197 			filename = purple_blist_node_get_string(node, "custom_buddy_icon");
1198 			if (filename != NULL)
1199 			{
1200 				if (old_icons_dir != NULL)
1201 				{
1202 					migrate_buddy_icon(node,
1203 					                   "custom_buddy_icon",
1204 					                   dirname, filename);
1205 				}
1206 				else
1207 				{
1208 					char *path = g_build_filename(dirname, filename, NULL);
1209 					if (!g_file_test(path, G_FILE_TEST_EXISTS))
1210 					{
1211 						purple_blist_node_remove_setting(node,
1212 						                                 "custom_buddy_icon");
1213 					}
1214 					else
1215 						ref_filename(filename);
1216 					g_free(path);
1217 				}
1218 			}
1219 		}
1220 		node = purple_blist_node_next(node, TRUE);
1221 	}
1222 }
1223 
1224 void
purple_buddy_icons_set_caching(gboolean caching)1225 purple_buddy_icons_set_caching(gboolean caching)
1226 {
1227 	icon_caching = caching;
1228 }
1229 
1230 gboolean
purple_buddy_icons_is_caching(void)1231 purple_buddy_icons_is_caching(void)
1232 {
1233 	return icon_caching;
1234 }
1235 
1236 void
purple_buddy_icons_set_cache_dir(const char * dir)1237 purple_buddy_icons_set_cache_dir(const char *dir)
1238 {
1239 	g_return_if_fail(dir != NULL);
1240 
1241 	g_free(cache_dir);
1242 	cache_dir = g_strdup(dir);
1243 }
1244 
1245 const char *
purple_buddy_icons_get_cache_dir(void)1246 purple_buddy_icons_get_cache_dir(void)
1247 {
1248 	return cache_dir;
1249 }
1250 
1251 void *
purple_buddy_icons_get_handle()1252 purple_buddy_icons_get_handle()
1253 {
1254 	static int handle;
1255 
1256 	return &handle;
1257 }
1258 
1259 void
purple_buddy_icons_init()1260 purple_buddy_icons_init()
1261 {
1262 	account_cache = g_hash_table_new_full(
1263 		g_direct_hash, g_direct_equal,
1264 		NULL, (GFreeFunc)g_hash_table_destroy);
1265 
1266 	icon_data_cache = g_hash_table_new_full(g_str_hash, g_str_equal,
1267 	                                        g_free, NULL);
1268 	icon_file_cache = g_hash_table_new_full(g_str_hash, g_str_equal,
1269 	                                        g_free, NULL);
1270 	pointer_icon_cache = g_hash_table_new(g_direct_hash, g_direct_equal);
1271 
1272 	if (!cache_dir)
1273 		cache_dir = g_build_filename(purple_user_dir(), "icons", NULL);
1274 
1275 	purple_signal_connect(purple_imgstore_get_handle(), "image-deleting",
1276 	                      purple_buddy_icons_get_handle(),
1277 	                      G_CALLBACK(image_deleting_cb), NULL);
1278 }
1279 
1280 void
purple_buddy_icons_uninit()1281 purple_buddy_icons_uninit()
1282 {
1283 	purple_signals_disconnect_by_handle(purple_buddy_icons_get_handle());
1284 
1285 	g_hash_table_destroy(account_cache);
1286 	g_hash_table_destroy(icon_data_cache);
1287 	g_hash_table_destroy(icon_file_cache);
1288 	g_hash_table_destroy(pointer_icon_cache);
1289 	g_free(old_icons_dir);
1290 	g_free(cache_dir);
1291 
1292 	cache_dir = NULL;
1293 	old_icons_dir = NULL;
1294 }
1295 
purple_buddy_icon_get_scale_size(PurpleBuddyIconSpec * spec,int * width,int * height)1296 void purple_buddy_icon_get_scale_size(PurpleBuddyIconSpec *spec, int *width, int *height)
1297 {
1298 	int new_width, new_height;
1299 
1300 	new_width = *width;
1301 	new_height = *height;
1302 
1303 	if (*width < spec->min_width)
1304 		new_width = spec->min_width;
1305 	else if (*width > spec->max_width)
1306 		new_width = spec->max_width;
1307 
1308 	if (*height < spec->min_height)
1309 		new_height = spec->min_height;
1310 	else if (*height > spec->max_height)
1311 		new_height = spec->max_height;
1312 
1313 	/* preserve aspect ratio */
1314 	if ((double)*height * (double)new_width >
1315 		(double)*width * (double)new_height) {
1316 			new_width = 0.5 + (double)*width * (double)new_height / (double)*height;
1317 	} else {
1318 			new_height = 0.5 + (double)*height * (double)new_width / (double)*width;
1319 	}
1320 
1321 	*width = new_width;
1322 	*height = new_height;
1323 }
1324