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