]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-theme-adium.c
Correctly escape message body so html tags are not interpreted by webkit.
[empathy.git] / libempathy-gtk / empathy-theme-adium.c
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3  * Copyright (C) 2008 Collabora Ltd.
4  *
5  * This library is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU Lesser General Public
7  * License as published by the Free Software Foundation; either
8  * version 2.1 of the License, or (at your option) any later version.
9  *
10  * This library is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * Lesser General Public License for more details.
14  *
15  * You should have received a copy of the GNU Lesser General Public
16  * License along with this library; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
18  *
19  * Authors: Xavier Claessens <xclaesse@gmail.com>
20  */
21
22 #include "config.h"
23
24 #include <string.h>
25 #include <glib/gi18n.h>
26
27 #include <webkit/webkitnetworkrequest.h>
28
29 #include <libempathy/empathy-time.h>
30 #include <libempathy/empathy-utils.h>
31
32 #include "empathy-theme-adium.h"
33 #include "empathy-smiley-manager.h"
34 #include "empathy-conf.h"
35 #include "empathy-ui-utils.h"
36
37 #define DEBUG_FLAG EMPATHY_DEBUG_CHAT
38 #include <libempathy/empathy-debug.h>
39
40 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyThemeAdium)
41
42 typedef struct {
43         EmpathySmileyManager *smiley_manager;
44         EmpathyContact       *last_contact;
45         gboolean              page_loaded;
46         GList                *message_queue;
47         gchar                *path;
48         gchar                *default_avatar_filename;
49         gchar                *in_content_html;
50         gsize                 in_content_len;
51         gchar                *in_nextcontent_html;
52         gsize                 in_nextcontent_len;
53         gchar                *out_content_html;
54         gsize                 out_content_len;
55         gchar                *out_nextcontent_html;
56         gsize                 out_nextcontent_len;
57 } EmpathyThemeAdiumPriv;
58
59 static void theme_adium_iface_init (EmpathyChatViewIface *iface);
60
61 enum {
62         PROP_0,
63         PROP_PATH,
64 };
65
66 G_DEFINE_TYPE_WITH_CODE (EmpathyThemeAdium, empathy_theme_adium,
67                          WEBKIT_TYPE_WEB_VIEW,
68                          G_IMPLEMENT_INTERFACE (EMPATHY_TYPE_CHAT_VIEW,
69                                                 theme_adium_iface_init));
70
71 static void
72 theme_adium_load (EmpathyThemeAdium *theme)
73 {
74         EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
75         gchar                 *basedir;
76         gchar                 *file;
77         gchar                 *template_html;
78         gsize                  template_len;
79         GString               *string;
80         gchar                **strv;
81         gchar                 *content;
82         gchar                 *css_path;
83
84         basedir = g_build_filename (priv->path, "Contents", "Resources", NULL);
85
86         /* Load html files */
87         file = g_build_filename (basedir, "Template.html", NULL);
88         g_file_get_contents (file, &template_html, &template_len, NULL);
89         g_free (file);
90
91         file = g_build_filename (basedir, "Incoming", "Content.html", NULL);
92         g_file_get_contents (file, &priv->in_content_html, &priv->in_content_len, NULL);
93         g_free (file);
94
95         file = g_build_filename (basedir, "Incoming", "NextContent.html", NULL);
96         g_file_get_contents (file, &priv->in_nextcontent_html, &priv->in_nextcontent_len, NULL);
97         g_free (file);
98
99         file = g_build_filename (basedir, "Outgoing", "Content.html", NULL);
100         g_file_get_contents (file, &priv->out_content_html, &priv->out_content_len, NULL);
101         g_free (file);
102
103         file = g_build_filename (basedir, "Outgoing", "NextContent.html", NULL);
104         g_file_get_contents (file, &priv->out_nextcontent_html, &priv->out_nextcontent_len, NULL);
105         g_free (file);
106
107         css_path = g_build_filename (basedir, "main.css", NULL);
108
109         /* Replace %@ with the needed information in the template html */
110         strv = g_strsplit (template_html, "%@", 5);
111         string = g_string_sized_new (template_len);
112         g_string_append (string, strv[0]);
113         g_string_append (string, basedir);
114         g_string_append (string, strv[1]);
115         g_string_append (string, css_path);
116         g_string_append (string, strv[2]);
117         g_string_append (string, ""); /* We don't want header */
118         g_string_append (string, strv[3]);
119         g_string_append (string, ""); /* We have no footer */
120         g_string_append (string, strv[4]);
121         content = g_string_free (string, FALSE);
122
123         /* Load the template */
124         webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (theme),
125                                           content, basedir);
126
127         g_free (basedir);
128         g_free (content);
129         g_free (template_html);
130         g_free (css_path);
131         g_strfreev (strv);
132 }
133
134 static WebKitNavigationResponse
135 theme_adium_navigation_requested_cb (WebKitWebView        *view,
136                                      WebKitWebFrame       *frame,
137                                      WebKitNetworkRequest *request,
138                                      gpointer              user_data)
139 {
140         const gchar *uri;
141
142         uri = webkit_network_request_get_uri (request);
143         empathy_url_show (GTK_WIDGET (view), uri);
144
145         return WEBKIT_NAVIGATION_RESPONSE_IGNORE;
146 }
147
148 static gchar *
149 theme_adium_escape_script (const gchar *text)
150 {
151         const gchar *cur = text;
152         GString     *string;
153
154         string = g_string_sized_new (strlen (text));
155         while (!G_STR_EMPTY (cur)) {
156                 switch (*cur) {
157                 case '\\':
158                         /* \ becomes \\ */
159                         g_string_append (string, "\\\\");       
160                         break;
161                 case '\"':
162                         /* " becomes \" */
163                         g_string_append (string, "\\\"");
164                         break;
165                 case '\n':
166                         /* Remove end of lines */
167                         break;
168                 default:
169                         g_string_append_c (string, *cur);
170                 }
171                 cur++;
172         }
173
174         return g_string_free (string, FALSE);
175 }
176
177 static gchar *
178 theme_adium_parse_body (EmpathyThemeAdium *theme,
179                         const gchar       *text)
180 {
181         EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
182         gboolean               use_smileys = FALSE;
183         GSList                *smileys, *l;
184         GString               *string;
185         gint                   i;
186         GRegex                *uri_regex;
187         GMatchInfo            *match_info;
188         gboolean               match;
189         gchar                 *ret = NULL;
190         gint                   prev;
191
192         ret = g_markup_escape_text (text, -1);
193
194         empathy_conf_get_bool (empathy_conf_get (),
195                                EMPATHY_PREFS_CHAT_SHOW_SMILEYS,
196                                &use_smileys);
197
198         if (use_smileys) {
199                 /* Replace smileys by a <img/> tag */
200                 string = g_string_sized_new (strlen (ret));
201                 smileys = empathy_smiley_manager_parse (priv->smiley_manager, ret);
202                 for (l = smileys; l; l = l->next) {
203                         EmpathySmiley *smiley;
204
205                         smiley = l->data;
206                         if (smiley->path) {
207                                 g_string_append_printf (string,
208                                                         "<abbr title='%s'><img src=\"%s\"/ alt=\"%s\"/></abbr>",
209                                                         smiley->str, smiley->path, smiley->str);
210                         } else {
211                                 g_string_append (string, smiley->str);
212                         }
213                         empathy_smiley_free (smiley);
214                 }
215                 g_slist_free (smileys);
216
217                 g_free (ret);
218                 ret = g_string_free (string, FALSE);
219         }
220
221         /* Add <a href></a> arround links */
222         uri_regex = empathy_uri_regex_dup_singleton ();
223         match = g_regex_match (uri_regex, cur, 0, &match_info);
224         if (match) {
225                 gint last = 0;
226                 gint s = 0, e = 0;
227
228                 string = g_string_sized_new (strlen (ret));
229                 do {
230                         g_match_info_fetch_pos (match_info, 0, &s, &e);
231
232                         if (s > last) {
233                                 /* Append the text between last link (or the
234                                  * start of the message) and this link */
235                                 g_string_append_len (string, ret + last, s - last);
236                         }
237
238                         /* Append the link inside <a href=""></a> tag */
239                         g_string_append (string, "<a href=\"");
240                         g_string_append_len (string, ret + s, e - s);
241                         g_string_append (string, "\">");
242                         g_string_append_len (string, ret + s, e - s);
243                         g_string_append (string, "</a>");
244
245                         last = e;
246                 } while (g_match_info_next (match_info, NULL));
247
248                 if (e < strlen (ret)) {
249                         /* Append the text after the last link */
250                         g_string_append_len (string, ret + e, strlen (ret) - e);
251                 }
252
253                 g_free (ret);
254                 ret = g_string_free (string, FALSE);
255         }
256         g_match_info_free (match_info);
257         g_regex_unref (uri_regex);
258
259         /* Replace \n by <br/> */
260         string = NULL;
261         prev = 0;
262         for (i = 0; ret[i] != '\0'; i++) {
263                 if (ret[i] == '\n') {
264                         if (!string ) {
265                                 string = g_string_sized_new (strlen (ret));
266                         }
267                         g_string_append_len (string, ret + prev, i - prev);
268                         g_string_append (string, "<br/>");
269                         prev = i + 1;
270                 }
271         }
272         if (string) {
273                 g_string_append (string, ret + prev);
274                 g_free (ret);
275                 ret = g_string_free (string, FALSE);
276         }
277
278         return ret;
279 }
280
281 static void
282 theme_adium_scroll_down (EmpathyChatView *view)
283 {
284         /* Not implemented */
285 }
286
287 #define FOLLOW(cur, str) (!strncmp (cur, str, strlen (str)))
288 static void
289 theme_adium_append_message (EmpathyChatView *view,
290                             EmpathyMessage  *msg)
291 {
292         EmpathyThemeAdium     *theme = EMPATHY_THEME_ADIUM (view);
293         EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
294         EmpathyContact        *sender;
295         gchar                 *dup_body = NULL;
296         const gchar           *body;
297         const gchar           *name;
298         EmpathyAvatar         *avatar;
299         const gchar           *avatar_filename = NULL;
300         time_t                 timestamp;
301         gsize                  len;
302         GString               *string;
303         gchar                 *cur = NULL;
304         gchar                 *prev;
305         gchar                 *script;
306         gchar                 *escape;
307         const gchar           *func;
308
309         if (!priv->page_loaded) {
310                 priv->message_queue = g_list_prepend (priv->message_queue,
311                                                       g_object_ref (msg));
312                 return;
313         }
314
315         /* Get information */
316         sender = empathy_message_get_sender (msg);
317         timestamp = empathy_message_get_timestamp (msg);
318         body = empathy_message_get_body (msg);
319         dup_body = theme_adium_parse_body (theme, body);
320         if (dup_body) {
321                 body = dup_body;
322         }
323         name = empathy_contact_get_name (sender);
324         avatar = empathy_contact_get_avatar (sender);
325         if (avatar) {
326                 avatar_filename = avatar->filename;
327         }
328         if (!avatar_filename) {
329                 if (!priv->default_avatar_filename) {
330                         priv->default_avatar_filename =
331                                 empathy_filename_from_icon_name ("stock_person",
332                                                                  GTK_ICON_SIZE_DIALOG);
333                 }
334                 avatar_filename = priv->default_avatar_filename;
335         }
336
337         /* Get the right html/func to add the message */
338         if (priv->last_contact &&
339             empathy_contact_equal (priv->last_contact, sender)) {
340                 func = "appendNextMessage";
341                 if (empathy_contact_is_user (sender)) {
342                         cur = priv->out_nextcontent_html;
343                         len = priv->out_nextcontent_len;
344                 }
345                 if (!cur) {
346                         cur = priv->in_nextcontent_html;
347                         len = priv->in_nextcontent_len;
348                 }
349         }
350         if (!cur) {
351                 func = "appendMessage";
352                 if (empathy_contact_is_user (sender)) {
353                         cur = priv->out_content_html;
354                         len = priv->out_content_len;
355                 }
356                 if (!cur) {
357                         cur = priv->in_content_html;
358                         len = priv->in_content_len;
359                 }
360         }
361
362         /* Make some search-and-replace in the html code */
363         prev = cur;
364         string = g_string_sized_new (len + strlen (body));
365         while ((cur = strchr (cur, '%'))) {
366                 const gchar *replace = NULL;
367                 gchar       *dup_replace = NULL;
368                 gchar       *fin = NULL;
369
370                 if (FOLLOW (cur, "%message%")) {
371                         replace = body;
372                 } else if (FOLLOW (cur, "%userIconPath%")) {
373                         replace = avatar_filename;
374                 } else if (FOLLOW (cur, "%sender%")) {
375                         replace = name;
376                 } else if (FOLLOW (cur, "%time")) {
377                         gchar *format = NULL;
378                         gchar *start;
379                         gchar *end;
380
381                         /* Extract the time format it provided. */
382                         if (*(start = cur + strlen("%time")) == '{') {
383                                 start++;
384                                 end = strstr (start, "}%");
385                                 if (!end) /* Invalid string */
386                                         continue;
387                                 format = g_strndup (start, end - start);
388                                 fin = end + 1;
389                         } 
390
391                         dup_replace = empathy_time_to_string_local (timestamp,
392                                 format ? format : EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
393                         replace = dup_replace;
394                         g_free (format);
395                 } else {
396                         cur++;
397                         continue;
398                 }
399
400                 /* Here we have a replacement to make */
401                 g_string_append_len (string, prev, cur - prev);
402                 g_string_append (string, replace);
403                 g_free (dup_replace);
404
405                 /* And update the pointers */
406                 if (fin) {
407                         prev = cur = fin + 1;
408                 } else {
409                         prev = cur = strchr (cur + 1, '%') + 1;
410                 }
411         }
412         g_string_append (string, prev);
413
414         /* Execute a js to add the message */
415         cur = g_string_free (string, FALSE);
416         escape = theme_adium_escape_script (cur);
417         script = g_strdup_printf("%s(\"%s\")", func, escape);
418         webkit_web_view_execute_script (WEBKIT_WEB_VIEW (view), script);
419
420         /* Keep the sender of the last displayed message */
421         if (priv->last_contact) {
422                 g_object_unref (priv->last_contact);
423         }
424         priv->last_contact = g_object_ref (sender);
425
426         g_free (dup_body);
427         g_free (cur);
428         g_free (script);
429 }
430 #undef FOLLOW
431
432 static void
433 theme_adium_append_event (EmpathyChatView *view,
434                           const gchar     *str)
435 {
436         /* Not implemented */
437 }
438
439 static void
440 theme_adium_scroll (EmpathyChatView *view,
441                     gboolean         allow_scrolling)
442 {
443         /* Not implemented */
444 }
445
446 static gboolean
447 theme_adium_get_has_selection (EmpathyChatView *view)
448 {
449         /* Not implemented */
450         return FALSE;
451 }
452
453 static void
454 theme_adium_clear (EmpathyChatView *view)
455 {
456         /* Not implemented */
457 }
458
459 static gboolean
460 theme_adium_find_previous (EmpathyChatView *view,
461                            const gchar     *search_criteria,
462                            gboolean         new_search)
463 {
464         /* Not implemented */
465         return FALSE;
466 }
467
468 static gboolean
469 theme_adium_find_next (EmpathyChatView *view,
470                        const gchar     *search_criteria,
471                        gboolean         new_search)
472 {
473         /* Not implemented */
474         return FALSE;
475 }
476
477 static void
478 theme_adium_find_abilities (EmpathyChatView *view,
479                             const gchar    *search_criteria,
480                             gboolean       *can_do_previous,
481                             gboolean       *can_do_next)
482 {
483         /* Not implemented */
484 }
485
486 static void
487 theme_adium_highlight (EmpathyChatView *view,
488                        const gchar     *text)
489 {
490         /* Not implemented */
491 }
492
493 static void
494 theme_adium_copy_clipboard (EmpathyChatView *view)
495 {
496         /* Not implemented */
497 }
498
499 static void
500 theme_adium_iface_init (EmpathyChatViewIface *iface)
501 {
502         iface->append_message = theme_adium_append_message;
503         iface->append_event = theme_adium_append_event;
504         iface->scroll = theme_adium_scroll;
505         iface->scroll_down = theme_adium_scroll_down;
506         iface->get_has_selection = theme_adium_get_has_selection;
507         iface->clear = theme_adium_clear;
508         iface->find_previous = theme_adium_find_previous;
509         iface->find_next = theme_adium_find_next;
510         iface->find_abilities = theme_adium_find_abilities;
511         iface->highlight = theme_adium_highlight;
512         iface->copy_clipboard = theme_adium_copy_clipboard;
513 }
514
515 static void
516 theme_adium_load_finished_cb (WebKitWebView  *view,
517                               WebKitWebFrame *frame,
518                               gpointer        user_data)
519 {
520         EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
521         EmpathyChatView       *chat_view = EMPATHY_CHAT_VIEW (view);
522
523         DEBUG ("Page loaded");
524         priv->page_loaded = TRUE;
525
526         /* Display queued messages */
527         priv->message_queue = g_list_reverse (priv->message_queue);
528         while (priv->message_queue) {
529                 EmpathyMessage *message = priv->message_queue->data;
530
531                 theme_adium_append_message (chat_view, message);
532                 priv->message_queue = g_list_remove (priv->message_queue, message);
533                 g_object_unref (message);
534         }
535 }
536
537 static void
538 theme_adium_finalize (GObject *object)
539 {
540         EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
541
542         g_free (priv->in_content_html);
543         g_free (priv->in_nextcontent_html);
544         g_free (priv->out_content_html);
545         g_free (priv->out_nextcontent_html);
546         g_free (priv->default_avatar_filename);
547         g_free (priv->path);
548         g_object_unref (priv->smiley_manager);
549
550         if (priv->last_contact) {
551                 g_object_unref (priv->last_contact);
552         }
553
554         G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object);
555 }
556
557 static void
558 theme_adium_constructed (GObject *object)
559 {
560         theme_adium_load (EMPATHY_THEME_ADIUM (object));
561 }
562
563 static void
564 theme_adium_get_property (GObject    *object,
565                           guint       param_id,
566                           GValue     *value,
567                           GParamSpec *pspec)
568 {
569         EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
570
571         switch (param_id) {
572         case PROP_PATH:
573                 g_value_set_string (value, priv->path);
574                 break;
575         default:
576                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
577                 break;
578         };
579 }
580
581 static void
582 theme_adium_set_property (GObject      *object,
583                           guint         param_id,
584                           const GValue *value,
585                           GParamSpec   *pspec)
586 {
587         EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
588
589         switch (param_id) {
590         case PROP_PATH:
591                 g_free (priv->path);
592                 priv->path = g_value_dup_string (value);
593                 break;
594         default:
595                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
596                 break;
597         };
598 }
599
600 static void
601 empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass)
602 {
603         GObjectClass *object_class = G_OBJECT_CLASS (klass);
604         
605         object_class->finalize = theme_adium_finalize;
606         object_class->constructed = theme_adium_constructed;
607         object_class->get_property = theme_adium_get_property;
608         object_class->set_property = theme_adium_set_property;
609
610         g_object_class_install_property (object_class,
611                                          PROP_PATH,
612                                          g_param_spec_string ("path",
613                                                               "The theme path",
614                                                               "Path to the adium theme",
615                                                               g_get_home_dir (),
616                                                               G_PARAM_CONSTRUCT_ONLY |
617                                                               G_PARAM_READWRITE));
618
619
620         g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv));
621 }
622
623 static void
624 empathy_theme_adium_init (EmpathyThemeAdium *theme)
625 {
626         EmpathyThemeAdiumPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (theme,
627                 EMPATHY_TYPE_THEME_ADIUM, EmpathyThemeAdiumPriv);
628
629         theme->priv = priv;     
630
631         priv->smiley_manager = empathy_smiley_manager_new ();
632
633         g_signal_connect (theme, "load-finished",
634                           G_CALLBACK (theme_adium_load_finished_cb),
635                           NULL);
636         g_signal_connect (theme, "navigation-requested",
637                           G_CALLBACK (theme_adium_navigation_requested_cb),
638                           NULL);
639 }
640
641 EmpathyThemeAdium *
642 empathy_theme_adium_new (const gchar *path)
643 {
644         g_return_val_if_fail (empathy_theme_adium_is_valid (path), NULL);
645
646         return g_object_new (EMPATHY_TYPE_THEME_ADIUM,
647                              "path", path,
648                              NULL);
649 }
650
651 gboolean
652 empathy_theme_adium_is_valid (const gchar *path)
653 {
654         gboolean ret;
655         gchar   *file;
656
657         /* We ship a default Template.html as fallback if there is any problem
658          * with the one inside the theme. The only other required file is
659          * Content.html for incoming messages (outgoing fallback to use
660          * incoming). */
661         file = g_build_filename (path, "Contents", "Resources", "Incoming",
662                                  "Content.html", NULL);
663         ret = g_file_test (file, G_FILE_TEST_EXISTS);
664         g_free (file);
665
666         return ret;
667 }
668