Libravatar: refactor and fix leak on corner case
[claws.git] / src / plugins / libravatar / libravatar.c
1 /*
2  * Claws Mail -- a GTK+ based, lightweight, and fast e-mail client
3  * Copyright (C) 1999-2014 Hiroyuki Yamamoto and the Claws Mail Team
4  * Copyright (C) 2014 Ricardo Mones
5  *
6  * This program is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 #ifdef HAVE_CONFIG_H
21 #  include "config.h"
22 #include "claws-features.h"
23 #endif
24
25 #include <glib.h>
26 #include <glib/gi18n.h>
27
28 #include <curl/curl.h>
29
30 #include "version.h"
31 #include "libravatar.h"
32 #include "libravatar_prefs.h"
33 #include "libravatar_cache.h"
34 #include "libravatar_missing.h"
35 #include "libravatar_federation.h"
36 #include "prefs_common.h"
37 #include "procheader.h"
38 #include "procmsg.h"
39 #include "utils.h"
40 #include "md5.h"
41
42 /* indexes of keys are default_mode - 10 if applicable */
43 static const char *def_mode[] = {
44         "404",  /* not used, only useful in web pages */
45         "mm",
46         "identicon",
47         "monsterid",
48         "wavatar",
49         "retro"
50 };
51
52 static guint update_hook_id;
53 static guint render_hook_id;
54 static gchar *cache_dir = NULL; /* dir-separator terminated */
55
56 static gboolean libravatar_header_update_hook(gpointer source, gpointer data)
57 {
58         AvatarCaptureData *acd = (AvatarCaptureData *)source;
59
60         debug_print("libravatar avatar_header_update invoked\n");
61
62         if (!strcmp(acd->header, "From:")) {
63                 gchar *a, *lower;
64
65                 a = g_strdup(acd->content);
66                 extract_address(a);
67
68                 /* string to lower */
69                 for (lower = a; *lower; lower++)
70                         *lower = g_ascii_tolower(*lower);
71
72                 debug_print("libravatar added '%s'\n", a);
73                 procmsg_msginfo_add_avatar(acd->msginfo, AVATAR_LIBRAVATAR, a);
74                 g_free(a);
75         }
76
77         return FALSE; /* keep getting */
78 }
79
80 static gchar *federated_base_url_from_address(const gchar *address)
81 {
82 #if (defined USE_GNUTLS && GLIB_CHECK_VERSION(2,22,0))
83         gchar *base_url = NULL;
84
85         if (!libravatarprefs.allow_federated) {
86                 debug_print("federated domains disabled by configuration\n");
87                 goto default_url;
88         }
89
90         base_url = federated_url_for_address(address);
91         if (base_url != NULL) {
92                 return base_url;
93         }
94
95 default_url:
96 #endif
97         return g_strdup(libravatarprefs.base_url);
98 }
99
100 static GtkWidget *image_widget_from_filename(const gchar *filename)
101 {
102         GtkWidget *image = NULL;
103         GdkPixbuf *picture = NULL;
104         GError *error = NULL;
105         gint w, h;
106
107         gdk_pixbuf_get_file_info(filename, &w, &h);
108
109         if (w != AVATAR_SIZE || h != AVATAR_SIZE)
110                 /* server can provide a different size from the requested in URL */
111                 picture = gdk_pixbuf_new_from_file_at_scale(
112                                 filename, AVATAR_SIZE, AVATAR_SIZE, TRUE, &error);
113         else    /* exact size */
114                 picture = gdk_pixbuf_new_from_file(filename, &error);
115
116         if (error != NULL) {
117                 g_warning("Failed to load image '%s': %s\n", filename, error->message);
118                 g_error_free(error);
119         } else {
120                 if (picture) {
121                         image = gtk_image_new_from_pixbuf(picture);
122                         g_object_unref(picture);
123                 } else
124                         g_warning("Failed to load image '%s': no error returned!\n", filename);
125         }
126
127         return image;
128 }
129
130 static gchar *cache_name_for_md5(const gchar *md5)
131 {
132         if (libravatarprefs.default_mode >= DEF_MODE_MM
133                         && libravatarprefs.default_mode <= DEF_MODE_RETRO) {
134                 /* cache dir for generated avatars */
135                 return g_strconcat(cache_dir, def_mode[libravatarprefs.default_mode - 10],
136                                    G_DIR_SEPARATOR_S, md5, NULL);
137         }
138         /* default cache dir */
139         return g_strconcat(cache_dir, md5, NULL);
140 }
141
142 static size_t write_image_data_cb(void *ptr, size_t size, size_t nmemb, void *stream)
143 {
144         size_t written = fwrite(ptr, size, nmemb, (FILE *)stream);
145         debug_print("received %zu bytes from avatar server\n", written);
146
147         return written;
148 }
149
150 static GtkWidget *image_widget_from_url(const gchar *url, const gchar *md5)
151 {
152         GtkWidget *image = NULL;
153         gchar *filename;
154         FILE *file;
155         CURL *curl;
156
157         curl = curl_easy_init();
158         if (curl == NULL) {
159                 g_warning("could not initialize curl to get image from url\n");
160                 return NULL;
161         }
162         curl_easy_setopt(curl, CURLOPT_URL, url);
163         curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_image_data_cb);
164         /* make sure timeout is less than general IO timeout */
165         curl_easy_setopt(curl, CURLOPT_TIMEOUT,
166                         (libravatarprefs.timeout == 0
167                                 || libravatarprefs.timeout
168                                         > prefs_common_get_prefs()->io_timeout_secs)
169                         ? prefs_common_get_prefs()->io_timeout_secs
170                         : libravatarprefs.timeout);
171         curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1);
172
173         filename = cache_name_for_md5(md5);
174         file = fopen(filename, "wb");
175         if (file != NULL) {
176                 long filesize;
177
178                 if (libravatarprefs.allow_redirects) {
179                         long maxredirs = (libravatarprefs.default_mode == DEF_MODE_URL)? 3L
180                                 : ((libravatarprefs.default_mode == DEF_MODE_MM)? 2L: 1L);
181
182                         curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
183                         curl_easy_setopt(curl, CURLOPT_MAXREDIRS, maxredirs);
184                 }
185                 curl_easy_setopt(curl, CURLOPT_FILE, file);
186                 debug_print("retrieving URL to file: %s -> %s\n", url, filename);
187                 curl_easy_perform(curl);
188                 filesize = ftell(file);
189                 fclose(file);
190
191                 if (filesize < MIN_PNG_SIZE)
192                         debug_print("not enough data for an avatar image: %ld bytes\n", filesize);
193                 else
194                         image = image_widget_from_filename(filename);
195
196                 if (!libravatarprefs.cache_icons || filesize == 0) {
197                         if (g_unlink(filename) < 0)
198                                 g_warning("failed to delete cache file %s\n", filename);
199                 }
200
201                 if (filesize == 0)
202                         missing_add_md5(libravatarmisses, md5);
203         } else {
204                 g_warning("could not open '%s' for writing\n", filename);
205         }
206         curl_easy_cleanup(curl);
207         g_free(filename);
208
209         return image;
210 }
211
212 static gboolean is_recent_enough(const gchar *filename)
213 {
214         GStatBuf s;
215         time_t t;
216
217         if (libravatarprefs.cache_icons) {
218                 t = time(NULL);
219                 if (t != (time_t)-1 && !g_stat(filename, &s)) {
220                         if (t - s.st_ctime <= libravatarprefs.cache_interval * 3600)
221                                 return TRUE;
222                 }
223         }
224
225         return FALSE; /* re-download */
226 }
227
228 static GtkWidget *image_widget_from_cached_md5(const gchar *md5)
229 {
230         GtkWidget *image = NULL;
231         gchar *filename;
232
233         filename = cache_name_for_md5(md5);
234         if (is_file_exist(filename) && is_recent_enough(filename)) {
235                 debug_print("found cached image for %s\n", md5);
236                 image = image_widget_from_filename(filename);
237         }
238         g_free(filename);
239
240         return image;
241 }
242
243 static gchar *libravatar_url_for_md5(const gchar *base, const gchar *md5)
244 {
245         if (libravatarprefs.default_mode >= DEF_MODE_404) {
246                 return g_strdup_printf("%s/%s?s=%u&d=%s",
247                                 base, md5, AVATAR_SIZE,
248                                 def_mode[libravatarprefs.default_mode - 10]);
249         } else if (libravatarprefs.default_mode == DEF_MODE_URL) {
250                 return g_strdup_printf("%s/%s?s=%u&d=%s",
251                                 base, md5, AVATAR_SIZE,
252                                 libravatarprefs.default_mode_url);
253         } else if (libravatarprefs.default_mode == DEF_MODE_NONE) {
254                 return g_strdup_printf("%s/%s?s=%u",
255                                 base, md5, AVATAR_SIZE);
256         }
257
258         g_warning("invalid libravatar default mode: %d\n", libravatarprefs.default_mode);
259         return NULL;
260 }
261
262 static gboolean libravatar_image_render_hook(gpointer source, gpointer data)
263 {
264         AvatarRender *ar = (AvatarRender *)source;
265         GtkWidget *image = NULL;
266         gchar *a = NULL, *url = NULL;
267         gchar md5sum[33];
268
269         debug_print("libravatar avatar_image_render invoked\n");
270
271         a = procmsg_msginfo_get_avatar(ar->full_msginfo, AVATAR_LIBRAVATAR);
272         if (a != NULL) {
273                 gchar *base;
274
275                 md5_hex_digest(md5sum, a);
276                 /* try missing cache */
277                 if (is_missing_md5(libravatarmisses, md5sum)) {
278                         return FALSE;
279                 }
280                 /* try disk cache */
281                 image = image_widget_from_cached_md5(md5sum);
282                 if (image != NULL) {
283                         if (ar->image) /* previous plugin set one */
284                                 gtk_widget_destroy(ar->image);
285                         ar->image = image;
286                         ar->type  = AVATAR_LIBRAVATAR;
287                         return FALSE;
288                 }
289                 /* not cached copy: try network */
290                 if (prefs_common_get_prefs()->work_offline) {
291                         debug_print("working off-line: libravatar network retrieval skipped\n");
292                         return FALSE;
293                 }
294                 base = federated_base_url_from_address(a);
295                 url = libravatar_url_for_md5(base, md5sum);
296                 if (url != NULL) {
297                         image = image_widget_from_url(url, md5sum);
298                         g_free(url);
299                         if (image != NULL) {
300                                 if (ar->image) /* previous plugin set one */
301                                         gtk_widget_destroy(ar->image);
302                                 ar->image = image;
303                                 ar->type  = AVATAR_LIBRAVATAR;
304                         }
305                 }
306                 g_free(base);
307
308                 return TRUE;
309         }
310
311         return FALSE; /* keep rendering */
312 }
313
314 static gint cache_dir_init()
315 {
316         cache_dir = libravatar_cache_init(def_mode, DEF_MODE_MM - 10, DEF_MODE_RETRO - 10);
317         cm_return_val_if_fail (cache_dir != NULL, -1);
318
319         return 0;
320 }
321
322 static gint missing_cache_init()
323 {
324         gchar *cache_file = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S,
325                                         LIBRAVATAR_CACHE_DIR, G_DIR_SEPARATOR_S,
326                                         LIBRAVATAR_MISSING_FILE, NULL);
327
328         libravatarmisses = missing_load_from_file(cache_file);
329         g_free(cache_file);
330
331         if (libravatarmisses == NULL)
332                 return -1;
333
334         return 0;
335 }
336
337 static void missing_cache_done()
338 {
339         gchar *cache_file;
340
341         if (libravatarmisses != NULL) {
342                 cache_file = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S,
343                                         LIBRAVATAR_CACHE_DIR, G_DIR_SEPARATOR_S,
344                                         LIBRAVATAR_MISSING_FILE, NULL);
345                 missing_save_to_file(libravatarmisses, cache_file);
346                 g_free(cache_file);
347                 g_hash_table_destroy(libravatarmisses);
348         }
349 }
350
351 /**
352  * Initialize plugin.
353  *
354  * @param error  For storing the returned error message.
355  *
356  * @return 0 if initialization succeeds, -1 on failure.
357  */
358 gint plugin_init(gchar **error)
359 {
360         if (!check_plugin_version(MAKE_NUMERIC_VERSION(3,9,3,29),
361                                   VERSION_NUMERIC, _("Libravatar"), error))
362                 return -1;
363         /* get info from headers */
364         update_hook_id = hooks_register_hook(AVATAR_HEADER_UPDATE_HOOKLIST,
365                                              libravatar_header_update_hook,
366                                              NULL);
367         if (update_hook_id == -1) {
368                 *error = g_strdup(_("Failed to register avatar header update hook"));
369                 return -1;
370         }
371         /* get image for displaying */
372         render_hook_id = hooks_register_hook(AVATAR_IMAGE_RENDER_HOOKLIST,
373                                              libravatar_image_render_hook,
374                                              NULL);
375         if (render_hook_id == -1) {
376                 *error = g_strdup(_("Failed to register avatar image render hook"));
377                 return -1;
378         }
379         /* cache dir */
380         if (cache_dir_init() == -1) {
381                 *error = g_strdup(_("Failed to create avatar image cache directory"));
382                 return -1;
383         }
384         /* preferences page */
385         libravatar_prefs_init();
386         /* curl library */
387         curl_global_init(CURL_GLOBAL_DEFAULT);
388         /* missing cache */
389         if (missing_cache_init() == -1) {
390                 *error = g_strdup(_("Failed to load missing items cache"));
391                 return -1;
392         }
393         debug_print("Libravatar plugin loaded\n");
394
395         return 0;
396 }
397
398 /**
399  * Destructor for the plugin.
400  * Unregister the callback function and frees matcher.
401  *
402  * @return Always TRUE.
403  */
404 gboolean plugin_done(void)
405 {
406         if (render_hook_id != -1) {
407                 hooks_unregister_hook(AVATAR_IMAGE_RENDER_HOOKLIST,
408                                       render_hook_id);
409                 render_hook_id = -1;
410         }
411         if (update_hook_id != -1) {
412                 hooks_unregister_hook(AVATAR_HEADER_UPDATE_HOOKLIST,
413                                       update_hook_id);
414                 update_hook_id = -1;
415         }
416         libravatar_prefs_done();
417         missing_cache_done();
418         if (cache_dir != NULL)
419                 g_free(cache_dir);
420         debug_print("Libravatar plugin unloaded\n");
421
422         return TRUE;
423 }
424
425 /**
426  * Get the name of the plugin.
427  *
428  * @return The plugin's name, maybe translated.
429  */
430 const gchar *plugin_name(void)
431 {
432         return _("Libravatar");
433 }
434
435 /**
436  * Get the description of the plugin.
437  *
438  * @return The plugin's description, maybe translated.
439  */
440 const gchar *plugin_desc(void)
441 {
442         return _("Display libravatar profiles' images for mail messages. More\n"
443                  "info about libravatar at http://www.libravatar.org/. If you have\n"
444                  "a gravatar.com profile but not a libravatar one, those will also\n"
445                  "be retrieved (when redirections are allowed in plugin config).\n"
446                  "Plugin config page is available from main window at:\n"
447                  "/Configuration/Preferences/Plugins/Libravatar.\n\n"
448                  "This plugin uses libcurl to retrieve images, so if you're behind a\n"
449                  "proxy please refer to curl(1) manpage for details on 'http_proxy'\n"
450                  "configuration. More details about this and others on README file.\n\n"
451                  "Feedback to <ricardo@mones.org> is welcome.\n");
452 }
453
454 /**
455  * Get the kind of plugin.
456  *
457  * @return The "GTK2" constant.
458  */
459 const gchar *plugin_type(void)
460 {
461         return "GTK2";
462 }
463
464 /**
465  * Get the license acronym the plugin is released under.
466  *
467  * @return The "GPL3+" constant.
468  */
469 const gchar *plugin_licence(void)
470 {
471         return "GPL3+";
472 }
473
474 /**
475  * Get the version of the plugin.
476  *
477  * @return The current version string.
478  */
479 const gchar *plugin_version(void)
480 {
481         return VERSION;
482 }
483
484 /**
485  * Get the features implemented by the plugin.
486  *
487  * @return A constant PluginFeature structure with the features.
488  */
489 struct PluginFeature *plugin_provides(void)
490 {
491         static struct PluginFeature features[] =
492                 { {PLUGIN_OTHER, N_("Libravatar")},
493                   {PLUGIN_NOTHING, NULL}};
494
495         return features;
496 }
497