2.53 format.c
  1/* snac - A simple, minimalistic ActivityPub instance */
  2/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
  3
  4#include "xs.h"
  5#include "xs_regex.h"
  6#include "xs_mime.h"
  7#include "xs_html.h"
  8#include "xs_json.h"
  9#include "xs_time.h"
 10
 11#include "snac.h"
 12
 13/* emoticons, people laughing and such */
 14const char *smileys[] = {
 15    ":-)",        "🙂",
 16    ":-D",        "😀",
 17    "X-D",        "😆",
 18    ";-)",        "😉",
 19    "B-)",        "😎",
 20    ">:-(",       "😡",
 21    ":-(",        "😞",
 22    ":-*",        "😘",
 23    ":-/",        "😕",
 24    "8-o",        "😲",
 25    "%-)",        "🤪",
 26    ":_(",        "😢",
 27    ":-|",        "😐",
 28    "<3",         "&#10084;&#65039;",
 29    ":facepalm:", "&#129318;",
 30    ":shrug:",    "&#129335;",
 31    ":shrug2:",   "&#175;\\_(&#12484;)_/&#175;",
 32    ":eyeroll:",  "&#128580;",
 33    ":beer:",     "&#127866;",
 34    ":beers:",    "&#127867;",
 35    ":munch:",    "&#128561;",
 36    ":thumb:",    "&#128077;",
 37    NULL,         NULL
 38};
 39
 40
 41xs_dict *emojis(void)
 42/* returns a dict with the emojis */
 43{
 44    xs *fn = xs_fmt("%s/emojis.json", srv_basedir);
 45    FILE *f;
 46
 47    if (mtime(fn) == 0) {
 48        /* file does not exist; create it with the defaults */
 49        xs *d = xs_dict_new();
 50        const char **emo = smileys;
 51
 52        while (*emo) {
 53            d = xs_dict_append(d, emo[0], emo[1]);
 54            emo += 2;
 55        }
 56
 57        if ((f = fopen(fn, "w")) != NULL) {
 58            xs_json_dump(d, 4, f);
 59            fclose(f);
 60        }
 61        else
 62            srv_log(xs_fmt("Error creating '%s'", fn));
 63    }
 64
 65    xs_dict *d = NULL;
 66
 67    if ((f = fopen(fn, "r")) != NULL) {
 68        d = xs_json_load(f);
 69        fclose(f);
 70
 71        if (d == NULL)
 72            srv_log(xs_fmt("JSON parse error in '%s'", fn));
 73    }
 74    else
 75        srv_log(xs_fmt("Error opening '%s'", fn));
 76
 77    return d;
 78}
 79
 80
 81static xs_str *format_line(const char *line, xs_list **attach)
 82/* formats a line */
 83{
 84    xs_str *s = xs_str_new(NULL);
 85    char *p, *v;
 86
 87    /* split by markup */
 88    xs *sm = xs_regex_split(line,
 89        "(`[^`]+`|\\*\\*?[^\\*]+\\*?\\*|https?:/" "/[^[:space:]]+)");
 90    int n = 0;
 91
 92    p = sm;
 93    while (xs_list_iter(&p, &v)) {
 94        if ((n & 0x1)) {
 95            /* markup */
 96            if (xs_startswith(v, "`")) {
 97                xs *s1 = xs_crop_i(xs_dup(v), 1, -1);
 98                xs *e1 = encode_html(s1);
 99                xs *s2 = xs_fmt("<code>%s</code>", e1);
100                s = xs_str_cat(s, s2);
101            }
102            else
103            if (xs_startswith(v, "**")) {
104                xs *s1 = xs_crop_i(xs_dup(v), 2, -2);
105                xs *s2 = xs_fmt("<b>%s</b>", s1);
106                s = xs_str_cat(s, s2);
107            }
108            else
109            if (xs_startswith(v, "*")) {
110                xs *s1 = xs_crop_i(xs_dup(v), 1, -1);
111                xs *s2 = xs_fmt("<i>%s</i>", s1);
112                s = xs_str_cat(s, s2);
113            }
114            else
115            if (xs_startswith(v, "http")) {
116                xs *u  = xs_replace(v, "#", "&#35;");
117                xs *v2 = xs_strip_chars_i(xs_dup(u), ".,)");
118
119                const char *mime = xs_mime_by_ext(v2);
120
121                if (attach != NULL && xs_startswith(mime, "image/")) {
122                    /* if it's a link to an image, insert it as an attachment */
123                    xs *d = xs_dict_new();
124
125                    d = xs_dict_append(d, "mediaType", mime);
126                    d = xs_dict_append(d, "url",       v2);
127                    d = xs_dict_append(d, "name",      "");
128                    d = xs_dict_append(d, "type",      "Image");
129
130                    *attach = xs_list_append(*attach, d);
131                }
132                else {
133                    xs *s1 = xs_fmt("<a href=\"%s\" target=\"_blank\">%s</a>", v2, u);
134                    s = xs_str_cat(s, s1);
135                }
136            }
137            else
138                s = xs_str_cat(s, v);
139        }
140        else
141            /* surrounded text, copy directly */
142            s = xs_str_cat(s, v);
143
144        n++;
145    }
146
147    return s;
148}
149
150
151xs_str *not_really_markdown(const char *content, xs_list **attach, xs_list **tag)
152/* formats a content using some Markdown rules */
153{
154    xs_str *s  = xs_str_new(NULL);
155    int in_pre = 0;
156    int in_blq = 0;
157    xs *list;
158    char *p, *v;
159
160    /* work by lines */
161    list = xs_split(content, "\n");
162
163    p = list;
164    while (xs_list_iter(&p, &v)) {
165        xs *ss = NULL;
166
167        if (strcmp(v, "```") == 0) {
168            if (!in_pre)
169                s = xs_str_cat(s, "<pre>");
170            else
171                s = xs_str_cat(s, "</pre>");
172
173            in_pre = !in_pre;
174            continue;
175        }
176
177        if (in_pre) {
178            // Encode all HTML characters when we're in pre element until we are out.
179            ss = encode_html(v);
180
181            s = xs_str_cat(s, ss);
182            s = xs_str_cat(s, "<br>");
183            continue;
184        }
185
186        else
187            ss = xs_strip_i(format_line(v, attach));
188
189        if (xs_startswith(ss, "---")) {
190            /* delete the --- */
191            ss = xs_strip_i(xs_crop_i(ss, 3, 0));
192            s = xs_str_cat(s, "<hr>");
193
194            s = xs_str_cat(s, ss);
195
196            continue;
197        }
198
199        if (xs_startswith(ss, ">")) {
200            /* delete the > and subsequent spaces */
201            ss = xs_strip_i(xs_crop_i(ss, 1, 0));
202
203            if (!in_blq) {
204                s = xs_str_cat(s, "<blockquote>");
205                in_blq = 1;
206            }
207
208            s = xs_str_cat(s, ss);
209            s = xs_str_cat(s, "<br>");
210
211            continue;
212        }
213
214        if (in_blq) {
215            s = xs_str_cat(s, "</blockquote>");
216            in_blq = 0;
217        }
218
219        s = xs_str_cat(s, ss);
220        s = xs_str_cat(s, "<br>");
221    }
222
223    if (in_blq)
224        s = xs_str_cat(s, "</blockquote>");
225    if (in_pre)
226        s = xs_str_cat(s, "</pre>");
227
228    /* some beauty fixes */
229    s = xs_replace_i(s, "<br><br><blockquote>", "<br><blockquote>");
230    s = xs_replace_i(s, "</blockquote><br>", "</blockquote>");
231    s = xs_replace_i(s, "</pre><br>", "</pre>");
232
233    {
234        /* traditional emoticons */
235        xs *d = emojis();
236        int c = 0;
237        char *k, *v;
238
239        while (xs_dict_next(d, &k, &v, &c)) {
240            const char *t = NULL;
241
242            /* is it an URL to an image? */
243            if (xs_startswith(v, "https:/" "/") && xs_startswith((t = xs_mime_by_ext(v)), "image/")) {
244                if (tag && xs_str_in(s, k) != -1) {
245                    /* add the emoji to the tag list */
246                    xs *e = xs_dict_new();
247                    xs *i = xs_dict_new();
248                    xs *u = xs_str_utctime(0, ISO_DATE_SPEC);
249
250                    e = xs_dict_append(e, "id", v);
251                    e = xs_dict_append(e, "type", "Emoji");
252                    e = xs_dict_append(e, "name", k);
253                    e = xs_dict_append(e, "updated", u);
254
255                    i = xs_dict_append(i, "type", "Image");
256                    i = xs_dict_append(i, "mediaType", t);
257                    i = xs_dict_append(i, "url", v);
258                    e = xs_dict_append(e, "icon", i);
259
260                    *tag = xs_list_append(*tag, e);
261                }
262            }
263            else
264                s = xs_replace_i(s, k, v);
265        }
266    }
267
268    return s;
269}
270
271
272const char *valid_tags[] = {
273    "a", "p", "br", "br/", "blockquote", "ul", "ol", "li", "cite", "small",
274    "span", "i", "b", "u", "s", "pre", "code", "em", "strong", "hr", "img", "del", "bdi", NULL
275};
276
277xs_str *sanitize(const char *content)
278/* cleans dangerous HTML output */
279{
280    xs_str *s = xs_str_new(NULL);
281    xs *sl;
282    int n = 0;
283    char *p, *v;
284
285    sl = xs_regex_split(content, "</?[^>]+>");
286
287    p = sl;
288
289    n = 0;
290    while (xs_list_iter(&p, &v)) {
291        if (n & 0x1) {
292            xs *s1  = xs_strip_i(xs_crop_i(xs_dup(v), v[1] == '/' ? 2 : 1, -1));
293            xs *l1  = xs_split_n(s1, " ", 1);
294            xs *tag = xs_tolower_i(xs_dup(xs_list_get(l1, 0)));
295            xs *s2  = NULL;
296            int i;
297
298            /* check if it's one of the valid tags */
299            for (i = 0; valid_tags[i]; i++) {
300                if (strcmp(tag, valid_tags[i]) == 0)
301                    break;
302            }
303
304            if (valid_tags[i]) {
305                /* accepted tag: rebuild it with only the accepted elements */
306                xs *el = xs_regex_select(v, "(src|href|rel|class|target)=\"[^\"]*\"");
307                xs *s3 = xs_join(el, " ");
308
309                s2 = xs_fmt("<%s%s%s%s>",
310                    v[1] == '/' ? "/" : "", tag, xs_list_len(el) ? " " : "", s3);
311
312                s = xs_str_cat(s, s2);
313            } else {
314                /* treat end of divs as paragraph breaks */
315                if (strcmp(v, "</div>"))
316                    s = xs_str_cat(s, "<p>");
317            }
318        }
319        else {
320            /* non-tag */
321            s = xs_str_cat(s, v);
322        }
323
324        n++;
325    }
326
327    return s;
328}
329
330
331xs_str *encode_html(const char *str)
332/* escapes html characters */
333{
334    xs_str *encoded = xs_html_encode((char *)str);
335
336    /* Restore only <br>. Probably safe. Let's hope nothing goes wrong with this. */
337    encoded = xs_replace_i(encoded, "&lt;br&gt;", "<br>");
338
339    return encoded;
340}