Fix HTML <hX> header handling.
[claws.git] / src / html.c
1 /*
2  * Claws Mail -- a GTK+ based, lightweight, and fast e-mail client
3  * Copyright (C) 1999-2016 Hiroyuki Yamamoto and the Claws Mail team
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program 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
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program. If not, see <http://www.gnu.org/licenses/>.
17  */
18
19 #include <glib.h>
20 #include <stdio.h>
21 #include <string.h>
22 #include <ctype.h>
23
24 #include "html.h"
25 #include "codeconv.h"
26 #include "utils.h"
27 #include "entity.h"
28
29 #define SC_HTMLBUFSIZE  8192
30 #define HR_STR          "────────────────────────────────────────────────"
31 #define LI_STR          "• "
32
33 static SC_HTMLState sc_html_read_line   (SC_HTMLParser  *parser);
34 static void sc_html_append_char                 (SC_HTMLParser  *parser,
35                                          gchar           ch);
36 static void sc_html_append_str                  (SC_HTMLParser  *parser,
37                                          const gchar    *str,
38                                          gint            len);
39 static SC_HTMLState sc_html_parse_tag   (SC_HTMLParser  *parser);
40 static void sc_html_parse_special               (SC_HTMLParser  *parser);
41 static void sc_html_get_parenthesis             (SC_HTMLParser  *parser,
42                                          gchar          *buf,
43                                          gint            len);
44
45
46 SC_HTMLParser *sc_html_parser_new(FILE *fp, CodeConverter *conv)
47 {
48         SC_HTMLParser *parser;
49
50         cm_return_val_if_fail(fp != NULL, NULL);
51         cm_return_val_if_fail(conv != NULL, NULL);
52
53         parser = g_new0(SC_HTMLParser, 1);
54         parser->fp = fp;
55         parser->conv = conv;
56         parser->str = g_string_new(NULL);
57         parser->buf = g_string_new(NULL);
58         parser->bufp = parser->buf->str;
59         parser->state = SC_HTML_NORMAL;
60         parser->href = NULL;
61         parser->newline = TRUE;
62         parser->empty_line = TRUE;
63         parser->space = FALSE;
64         parser->pre = FALSE;
65         parser->indent = 0;
66
67         return parser;
68 }
69
70 void sc_html_parser_destroy(SC_HTMLParser *parser)
71 {
72         g_string_free(parser->str, TRUE);
73         g_string_free(parser->buf, TRUE);
74         g_free(parser->href);
75         g_free(parser);
76 }
77
78 gchar *sc_html_parse(SC_HTMLParser *parser)
79 {
80         parser->state = SC_HTML_NORMAL;
81         g_string_truncate(parser->str, 0);
82
83         if (*parser->bufp == '\0') {
84                 g_string_truncate(parser->buf, 0);
85                 parser->bufp = parser->buf->str;
86                 if (sc_html_read_line(parser) == SC_HTML_EOF)
87                         return NULL;
88         }
89
90         while (*parser->bufp != '\0') {
91                 switch (*parser->bufp) {
92                 case '<': {
93                         SC_HTMLState st;
94                         st = sc_html_parse_tag(parser);
95                         /* when we see an href, we need to flush the str
96                          * buffer.  Then collect all the chars until we
97                          * see the end anchor tag
98                          */
99                         if (SC_HTML_HREF_BEG == st || SC_HTML_HREF == st)
100                                 return parser->str->str;
101                         } 
102                         break;
103                 case '&':
104                         sc_html_parse_special(parser);
105                         break;
106                 case ' ':
107                 case '\t':
108                 case '\r':
109                 case '\n':
110                         if (parser->bufp[0] == '\r' && parser->bufp[1] == '\n')
111                                 parser->bufp++;
112
113                         if (!parser->pre) {
114                                 if (!parser->newline)
115                                         parser->space = TRUE;
116
117                                 parser->bufp++;
118                                 break;
119                         }
120                         /* fallthrough */
121                 default:
122                         sc_html_append_char(parser, *parser->bufp++);
123                 }
124         }
125
126         return parser->str->str;
127 }
128
129 static SC_HTMLState sc_html_read_line(SC_HTMLParser *parser)
130 {
131         gchar buf[SC_HTMLBUFSIZE];
132         gchar buf2[SC_HTMLBUFSIZE];
133         gint index;
134         gint n;
135
136         if (parser->fp == NULL)
137                 return SC_HTML_EOF;
138
139         n = fread(buf, 1, sizeof(buf) - 1, parser->fp);
140         if (n == 0) {
141                 parser->state = SC_HTML_EOF;
142                 return SC_HTML_EOF;
143         } else
144                 buf[n] = '\0';
145
146         if (conv_convert(parser->conv, buf2, sizeof(buf2), buf) < 0) {
147                 index = parser->bufp - parser->buf->str;
148
149                 conv_utf8todisp(buf2, sizeof(buf2), buf);
150                 g_string_append(parser->buf, buf2);
151
152                 parser->bufp = parser->buf->str + index;
153
154                 return SC_HTML_CONV_FAILED;
155         }
156
157         index = parser->bufp - parser->buf->str;
158
159         g_string_append(parser->buf, buf2);
160
161         parser->bufp = parser->buf->str + index;
162
163         return SC_HTML_NORMAL;
164 }
165
166 static void sc_html_append_char(SC_HTMLParser *parser, gchar ch)
167 {
168         GString *str = parser->str;
169
170         if (!parser->pre && parser->space) {
171                 g_string_append_c(str, ' ');
172                 parser->space = FALSE;
173         }
174
175         g_string_append_c(str, ch);
176
177         parser->empty_line = FALSE;
178         if (ch == '\n') {
179                 parser->newline = TRUE;
180                 if (str->len > 1 && str->str[str->len - 2] == '\n')
181                         parser->empty_line = TRUE;
182                 if (parser->indent > 0) {
183                         gint i, n = parser->indent;
184                         for (i = 0; i < n; i++)
185                                 g_string_append_c(str, '>');
186                         g_string_append_c(str, ' ');
187                 }
188         } else
189                 parser->newline = FALSE;
190 }
191
192 static void sc_html_append_str(SC_HTMLParser *parser, const gchar *str, gint len)
193 {
194         GString *string = parser->str;
195
196         if (!parser->pre && parser->space) {
197                 g_string_append_c(string, ' ');
198                 parser->space = FALSE;
199         }
200
201         if (len == 0) return;
202         if (len < 0)
203                 g_string_append(string, str);
204         else {
205                 gchar *s;
206                 Xstrndup_a(s, str, len, return);
207                 g_string_append(string, s);
208         }
209
210         parser->empty_line = FALSE;
211         if (string->len > 0 && string->str[string->len - 1] == '\n') {
212                 parser->newline = TRUE;
213                 if (string->len > 1 && string->str[string->len - 2] == '\n')
214                         parser->empty_line = TRUE;
215         } else
216                 parser->newline = FALSE;
217 }
218
219 static SC_HTMLTag *sc_html_get_tag(const gchar *str)
220 {
221         SC_HTMLTag *tag;
222         gchar *tmp;
223         guchar *tmpp;
224
225         cm_return_val_if_fail(str != NULL, NULL);
226
227         if (*str == '\0' || *str == '!') return NULL;
228
229         Xstrdup_a(tmp, str, return NULL);
230
231         tag = g_new0(SC_HTMLTag, 1);
232
233         for (tmpp = tmp; *tmpp != '\0' && !g_ascii_isspace(*tmpp); tmpp++)
234                 ;
235
236         if (*tmpp == '\0') {
237                 tag->name = g_utf8_strdown(tmp, -1);
238                 return tag;
239         } else {
240                 *tmpp++ = '\0';
241                 tag->name = g_utf8_strdown(tmp, -1);
242         }
243
244         while (*tmpp != '\0') {
245                 SC_HTMLAttr *attr;
246                 gchar *attr_name;
247                 gchar *attr_value;
248                 gchar *p;
249                 gchar quote;
250
251                 while (g_ascii_isspace(*tmpp)) tmpp++;
252                 attr_name = tmpp;
253
254                 while (*tmpp != '\0' && !g_ascii_isspace(*tmpp) &&
255                        *tmpp != '=')
256                         tmpp++;
257                 if (*tmpp != '\0' && *tmpp != '=') {
258                         *tmpp++ = '\0';
259                         while (g_ascii_isspace(*tmpp)) tmpp++;
260                 }
261
262                 if (*tmpp == '=') {
263                         *tmpp++ = '\0';
264                         while (g_ascii_isspace(*tmpp)) tmpp++;
265
266                         if (*tmpp == '"' || *tmpp == '\'') {
267                                 /* name="value" */
268                                 quote = *tmpp;
269                                 tmpp++;
270                                 attr_value = tmpp;
271                                 if ((p = strchr(attr_value, quote)) == NULL) {
272                                         if (debug_get_mode()) {
273                                                 g_warning("sc_html_get_tag(): syntax error in tag: '%s'",
274                                                                   str);
275                                         } else {
276                                                 gchar *cut = g_strndup(str, 100);
277                                                 g_warning("sc_html_get_tag(): syntax error in tag: '%s%s'",
278                                                                   cut, strlen(str)>100?"...":".");
279                                                 g_free(cut);
280                                         }
281                                         return tag;
282                                 }
283                                 tmpp = p;
284                                 *tmpp++ = '\0';
285                                 while (g_ascii_isspace(*tmpp)) tmpp++;
286                         } else {
287                                 /* name=value */
288                                 attr_value = tmpp;
289                                 while (*tmpp != '\0' && !g_ascii_isspace(*tmpp)) tmpp++;
290                                 if (*tmpp != '\0')
291                                         *tmpp++ = '\0';
292                         }
293                 } else
294                         attr_value = "";
295
296                 g_strchomp(attr_name);
297                 attr = g_new(SC_HTMLAttr, 1);
298                 attr->name = g_utf8_strdown(attr_name, -1);
299                 attr->value = g_strdup(attr_value);
300                 tag->attr = g_list_append(tag->attr, attr);
301         }
302
303         return tag;
304 }
305
306 static void sc_html_free_tag(SC_HTMLTag *tag)
307 {
308         if (!tag) return;
309
310         g_free(tag->name);
311         while (tag->attr != NULL) {
312                 SC_HTMLAttr *attr = (SC_HTMLAttr *)tag->attr->data;
313                 g_free(attr->name);
314                 g_free(attr->value);
315                 g_free(attr);
316                 tag->attr = g_list_remove(tag->attr, tag->attr->data);
317         }
318         g_free(tag);
319 }
320
321 static void decode_href(SC_HTMLParser *parser)
322 {
323         gchar *tmp;
324         SC_HTMLParser *tparser = g_new0(SC_HTMLParser, 1);
325
326         tparser->str = g_string_new(NULL);
327         tparser->buf = g_string_new(parser->href);
328         tparser->bufp = tparser->buf->str;
329
330         tmp = sc_html_parse(tparser);
331         
332         g_free(parser->href);
333         parser->href = g_strdup(tmp);
334
335         sc_html_parser_destroy(tparser);
336 }
337
338 static SC_HTMLState sc_html_parse_tag(SC_HTMLParser *parser)
339 {
340         gchar buf[SC_HTMLBUFSIZE];
341         SC_HTMLTag *tag;
342
343         sc_html_get_parenthesis(parser, buf, sizeof(buf));
344
345         tag = sc_html_get_tag(buf);
346
347         parser->state = SC_HTML_UNKNOWN;
348         if (!tag) return SC_HTML_UNKNOWN;
349
350         if (!strcmp(tag->name, "br") || !strcmp(tag->name, "br/")) {
351                 parser->space = FALSE;
352                 sc_html_append_char(parser, '\n');
353                 parser->state = SC_HTML_BR;
354         } else if (!strcmp(tag->name, "a")) {
355                 GList *cur;
356                 parser->href = NULL;
357                 for (cur = tag->attr; cur != NULL; cur = cur->next) {
358                         if (cur->data && !strcmp(((SC_HTMLAttr *)cur->data)->name, "href")) {
359                                 g_free(parser->href);
360                                 parser->href = g_strdup(((SC_HTMLAttr *)cur->data)->value);
361                                 decode_href(parser);
362                                 parser->state = SC_HTML_HREF_BEG;
363                                 break;
364                         }
365                 }
366                 if (parser->href == NULL)
367                         parser->href = g_strdup("");
368                 parser->state = SC_HTML_HREF_BEG;
369         } else if (!strcmp(tag->name, "/a")) {
370                 parser->state = SC_HTML_HREF;
371         } else if (!strcmp(tag->name, "p")) {
372                 parser->space = FALSE;
373                 if (!parser->empty_line) {
374                         parser->space = FALSE;
375                         if (!parser->newline) sc_html_append_char(parser, '\n');
376                         sc_html_append_char(parser, '\n');
377                 }
378                 parser->state = SC_HTML_PAR;
379         } else if (!strcmp(tag->name, "pre")) {
380                 parser->pre = TRUE;
381                 parser->state = SC_HTML_PRE;
382         } else if (!strcmp(tag->name, "/pre")) {
383                 parser->pre = FALSE;
384                 parser->state = SC_HTML_NORMAL;
385         } else if (!strcmp(tag->name, "hr")) {
386                 if (!parser->newline) {
387                         parser->space = FALSE;
388                         sc_html_append_char(parser, '\n');
389                 }
390                 sc_html_append_str(parser, HR_STR, -1);
391                 sc_html_append_char(parser, '\n');
392                 parser->state = SC_HTML_HR;
393         } else if (!strcmp(tag->name, "div")    ||
394                    !strcmp(tag->name, "ul")     ||
395                    !strcmp(tag->name, "li")     ||
396                    !strcmp(tag->name, "table")  ||
397                    !strcmp(tag->name, "dd")     ||
398                    !strcmp(tag->name, "tr")) {
399                 if (!parser->newline) {
400                         parser->space = FALSE;
401                         sc_html_append_char(parser, '\n');
402                 }
403                 if (!strcmp(tag->name, "li")) {
404                         sc_html_append_str(parser, LI_STR, -1);
405                 }
406                 parser->state = SC_HTML_NORMAL;
407         } else if (tag->name[0] == 'h' && g_ascii_isdigit(tag->name[1])) {
408                 if (!parser->newline) {
409                         parser->space = FALSE;
410                         sc_html_append_char(parser, '\n');
411                 }
412                 sc_html_append_char(parser, '\n');
413         } else if (!strcmp(tag->name, "blockquote")) {
414                 parser->state = SC_HTML_NORMAL;
415                 parser->indent++;
416         } else if (!strcmp(tag->name, "/blockquote")) {
417                 parser->state = SC_HTML_NORMAL;
418                 parser->indent--;
419         } else if (!strcmp(tag->name, "/table") ||
420                    (tag->name[0] == '/' &&
421                     tag->name[1] == 'h' &&
422                     g_ascii_isdigit(tag->name[2]))) {
423                 if (!parser->empty_line) {
424                         parser->space = FALSE;
425                         if (!parser->newline) sc_html_append_char(parser, '\n');
426                         sc_html_append_char(parser, '\n');
427                 }
428                 parser->state = SC_HTML_NORMAL;
429         } else if (!strcmp(tag->name, "/div")   ||
430                    !strcmp(tag->name, "/ul")    ||
431                    !strcmp(tag->name, "/li")) {
432                 if (!parser->newline) {
433                         parser->space = FALSE;
434                         sc_html_append_char(parser, '\n');
435                 }
436                 parser->state = SC_HTML_NORMAL;
437                         }
438
439         sc_html_free_tag(tag);
440
441         return parser->state;
442 }
443
444 static void sc_html_parse_special(SC_HTMLParser *parser)
445 {
446         gchar *entity;
447
448         parser->state = SC_HTML_UNKNOWN;
449         cm_return_if_fail(*parser->bufp == '&');
450
451         entity = entity_decode(parser->bufp);
452         if (entity != NULL) {
453                 sc_html_append_str(parser, entity, -1);
454                 g_free(entity);
455                 while (*parser->bufp++ != ';');
456         } else {
457                 /* output literal `&' */
458                 sc_html_append_char(parser, *parser->bufp++);
459         }
460         parser->state = SC_HTML_NORMAL;
461 }
462
463 static gchar *sc_html_find_tag(SC_HTMLParser *parser, const gchar *tag)
464 {
465         gchar *cur = parser->bufp;
466         gint len = strlen(tag);
467
468         if (cur == NULL)
469                 return NULL;
470
471         while ((cur = strstr(cur, "<")) != NULL) {
472                 if (!g_ascii_strncasecmp(cur, tag, len))
473                         return cur;
474                 cur += 2;
475         }
476         return NULL;
477 }
478
479 static void sc_html_get_parenthesis(SC_HTMLParser *parser, gchar *buf, gint len)
480 {
481         gchar *p;
482
483         buf[0] = '\0';
484         cm_return_if_fail(*parser->bufp == '<');
485
486         /* ignore comment / CSS / script stuff */
487         if (!strncmp(parser->bufp, "<!--", 4)) {
488                 parser->bufp += 4;
489                 while ((p = strstr(parser->bufp, "-->")) == NULL)
490                         if (sc_html_read_line(parser) == SC_HTML_EOF) return;
491                 parser->bufp = p + 3;
492                 return;
493         }
494         if (!g_ascii_strncasecmp(parser->bufp, "<style", 6)) {
495                 parser->bufp += 6;
496                 while ((p = sc_html_find_tag(parser, "</style>")) == NULL)
497                         if (sc_html_read_line(parser) == SC_HTML_EOF) return;
498                 parser->bufp = p + 8;
499                 return;
500         }
501         if (!g_ascii_strncasecmp(parser->bufp, "<script", 7)) {
502                 parser->bufp += 7;
503                 while ((p = sc_html_find_tag(parser, "</script>")) == NULL)
504                         if (sc_html_read_line(parser) == SC_HTML_EOF) return;
505                 parser->bufp = p + 9;
506                 return;
507         }
508
509         parser->bufp++;
510         while ((p = strchr(parser->bufp, '>')) == NULL)
511                 if (sc_html_read_line(parser) == SC_HTML_EOF) return;
512
513         strncpy2(buf, parser->bufp, MIN(p - parser->bufp + 1, len));
514         g_strstrip(buf);
515         parser->bufp = p + 1;
516 }