2.88
format.c
1/* snac - A simple, minimalistic ActivityPub instance */
2/* copyright (c) 2022 - 2026 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#include "xs_match.h"
11#include "xs_unicode.h"
12
13#include "snac.h"
14
15/* emoticons, people laughing and such */
16const char * const smileys[] = {
17 ":-)", "🙂",
18 ":-D", "😀",
19 "X-D", "😆",
20 ";-)", "😉",
21 "B-)", "😎",
22 ">:-(", "😡",
23 ":-(", "😞",
24 ":-*", "😘",
25 ":-/", "😕",
26 "8-o", "😲",
27 "%-)", "🤪",
28 ":_(", "😢",
29 ":-|", "😐",
30 "<3", "❤️",
31 ":facepalm:", "🤦",
32 ":shrug:", "🤷",
33 ":shrug2:", "¯\\_(ツ)_/¯",
34 ":eyeroll:", "🙄",
35 ":beer:", "🍺",
36 ":beers:", "🍻",
37 ":munch:", "😱",
38 ":thumb:", "👍",
39 NULL, NULL
40};
41
42
43xs_dict *emojis(void)
44/* returns a dict with the emojis */
45{
46 xs *fn = xs_fmt("%s/emojis.json", srv_basedir);
47 FILE *f;
48
49 if (mtime(fn) == 0) {
50 /* file does not exist; create it with the defaults */
51 xs *d = xs_dict_new();
52 const char * const *emo = smileys;
53
54 while (*emo) {
55 d = xs_dict_append(d, emo[0], emo[1]);
56 emo += 2;
57 }
58
59 if ((f = fopen(fn, "w")) != NULL) {
60 xs_json_dump(d, 4, f);
61 fclose(f);
62 }
63 else
64 srv_log(xs_fmt("Error creating '%s'", fn));
65 }
66
67 xs_dict *d = NULL;
68
69 if ((f = fopen(fn, "r")) != NULL) {
70 d = xs_json_load(f);
71 fclose(f);
72
73 if (d == NULL)
74 srv_log(xs_fmt("JSON parse error in '%s'", fn));
75 }
76 else
77 srv_log(xs_fmt("Error opening '%s'", fn));
78
79 return d;
80}
81
82
83xs_dict *emojis_rm_categories() {
84 xs *emjs = emojis();
85 char *res = xs_dict_new();
86 const char *k, *v;
87 xs_dict_foreach(emjs, k, v) {
88 if (xs_type(v) == XSTYPE_DICT) {
89 const char *v2;
90 xs_dict_foreach(v, k, v2)
91 res = xs_dict_append(res, k, v2);
92 }
93 else
94 res = xs_dict_append(res, k, v);
95 }
96 return res;
97}
98
99
100/* Non-whitespace without trailing comma, period or closing paren */
101#define NOSPACE "([^[:space:],.)]+|[,.)]+[^[:space:],.)])+"
102
103static xs_str *format_line(const char *line, xs_list **attach)
104/* formats a line */
105{
106 xs_str *s = xs_str_new(NULL);
107 char *p;
108 const char *v;
109
110 /* split by markup */
111 xs *sm = xs_regex_split(line,
112 "("
113 "`[^`]+`" "|"
114 "~~[^~]+~~" "|"
115 "\\*\\*?\\*?[^\\*]+\\*?\\*?\\*" "|"
116 "__[^_]+__" "|" //anzu
117 "!\\[[^]]+\\]\\([^\\)]+\\)\\)?" "|"
118 "\\[[^]]+\\]\\([^\\)]+\\)\\)?" "|"
119 "[a-z]+:/" "/" NOSPACE "|"
120 "(mailto|xmpp):[^@[:space:]]+@" NOSPACE
121 ")");
122 int n = 0;
123
124 p = sm;
125 while (xs_list_iter(&p, &v)) {
126 if ((n & 0x1)) {
127 /* markup */
128 if (xs_startswith(v, "`")) {
129 xs *s1 = xs_strip_chars_i(xs_dup(v), "`");
130 xs *e1 = encode_html(s1);
131 xs *s2 = xs_fmt("<code>%s</code>", e1);
132 s = xs_str_cat(s, s2);
133 }
134 else
135 if (xs_startswith(v, "***")) {
136 xs *s1 = xs_strip_chars_i(xs_dup(v), "*");
137 xs *s2 = xs_fmt("<b><i>%s</i></b>", s1);
138 s = xs_str_cat(s, s2);
139 }
140 else
141 if (xs_startswith(v, "**")) {
142 xs *s1 = xs_strip_chars_i(xs_dup(v), "*");
143 xs *s2 = xs_fmt("<b>%s</b>", s1);
144 s = xs_str_cat(s, s2);
145 }
146 else
147 if (xs_startswith(v, "*")) {
148 xs *s1 = xs_strip_chars_i(xs_dup(v), "*");
149 xs *s2 = xs_fmt("<i>%s</i>", s1);
150 s = xs_str_cat(s, s2);
151 }
152 //anzu - begin
153 else
154 if (xs_startswith(v, "__")) {
155 xs *s1 = xs_strip_chars_i(xs_dup(v), "_");
156 xs *s2 = xs_fmt("<u>%s</u>", s1);
157 s = xs_str_cat(s, s2);
158 }
159 //anzu - end
160 else
161 if (xs_startswith(v, "~~")) {
162 xs *s1 = xs_strip_chars_i(xs_dup(v), "~");
163 xs *e1 = encode_html(s1);
164 xs *s2 = xs_fmt("<s>%s</s>", e1);
165 s = xs_str_cat(s, s2);
166 }
167 else
168 if (*v == '[') {
169 /* markdown-like links [label](url) */
170 xs *w = xs_replace_i(xs_replace(v, "#", "#"), "@", "@");
171 xs *l = xs_split_n(w, "](", 1);
172
173 if (xs_list_len(l) == 2) {
174 xs *name = xs_dup(xs_list_get(l, 0));
175 xs *url = xs_dup(xs_list_get(l, 1));
176
177 name = xs_crop_i(name, 1, 0);
178 url = xs_crop_i(url, 0, -1);
179
180 xs *link = xs_fmt("<a href=\"%s\">%s</a>", url, name);
181
182 s = xs_str_cat(s, link);
183 }
184 else
185 s = xs_str_cat(s, v);
186 }
187 else
188 if (*v == '!') {
189 /* markdown-like images  */
190 xs *w = xs_replace_i(xs_replace(v, "#", "#"), "@", "@");
191 xs *l = xs_split_n(w, "](", 1);
192
193 if (xs_list_len(l) == 2) {
194 xs *alt_text = xs_dup(xs_list_get(l, 0));
195 xs *img_url = xs_dup(xs_list_get(l, 1));
196
197 alt_text = xs_crop_i(alt_text, 2, 0);
198 img_url = xs_crop_i(img_url, 0, -1);
199
200 const char *mime = xs_mime_by_ext(img_url);
201
202 if (attach != NULL && xs_startswith(mime, "image/")) {
203 const xs_dict *ad;
204 int add = 1;
205
206 xs_list_foreach(*attach, ad) {
207 if (strcmp(xs_dict_get_def(ad, "url", ""), img_url) == 0) {
208 add = 0;
209 break;
210 }
211 }
212
213 if (add) {
214 xs *d = xs_dict_new();
215
216 d = xs_dict_append(d, "mediaType", mime);
217 d = xs_dict_append(d, "url", img_url);
218 d = xs_dict_append(d, "name", alt_text);
219 d = xs_dict_append(d, "type", "Image");
220
221 *attach = xs_list_append(*attach, d);
222 }
223 }
224 else {
225 xs *link = xs_fmt("<a href=\"%s\">%s</a>", img_url, alt_text);
226
227 s = xs_str_cat(s, link);
228 }
229 }
230 else
231 s = xs_str_cat(s, v);
232 }
233 else
234 if (xs_str_in(v, ":/" "/") != -1) {
235 /* direct URLs in the post body */
236 xs *u = xs_replace_i(xs_replace(v, "#", "#"), "@", "@");
237
238 xs *v2 = xs_strip_chars_i(xs_dup(u), ".,)");
239
240 const char *mime = xs_mime_by_ext(v2);
241
242 if (attach != NULL && xs_startswith(mime, "image/")) {
243 /* if it's a link to an image, insert it as an attachment */
244 const xs_dict *ad;
245 int add = 1;
246
247 xs_list_foreach(*attach, ad) {
248 if (strcmp(xs_dict_get_def(ad, "url", ""), v2) == 0) {
249 add = 0;
250 break;
251 }
252 }
253
254 if (add) {
255 xs *d = xs_dict_new();
256
257 d = xs_dict_append(d, "mediaType", mime);
258 d = xs_dict_append(d, "url", v2);
259 d = xs_dict_append(d, "name", "");
260 d = xs_dict_append(d, "type", "Image");
261
262 *attach = xs_list_append(*attach, d);
263 }
264 }
265 else {
266 xs *s1 = xs_fmt("<a href=\"%s\" target=\"_blank\">%s</a>", v2, u);
267 s = xs_str_cat(s, s1);
268 }
269 }
270 else
271 if (xs_match(v, "mailto*|xmpp*")) {
272 xs *u = xs_replace_i(xs_replace(v, "#", "#"), "@", "@");
273
274 xs *v2 = xs_strip_chars_i(xs_dup(u), ".,)");
275
276 xs *s1 = xs_fmt("<a href=\"%s\" target=\"_blank\">%s</a>", v2, u);
277 s = xs_str_cat(s, s1);
278 }
279 else
280 s = xs_str_cat(s, v);
281 }
282 else
283 /* surrounded text, copy directly */
284 s = xs_str_cat(s, v);
285
286 n++;
287 }
288
289 return s;
290}
291
292
293xs_str *not_really_markdown(const char *content, xs_list **attach, xs_list **tag)
294/* formats a content using some Markdown rules */
295{
296 xs_str *s = xs_str_new(NULL);
297 int in_pre = 0;
298 int in_blq = 0;
299 int in_ul = 0;
300 xs *list;
301 char *p;
302 const char *v;
303
304 /* work by lines */
305 list = xs_split(content, "\n");
306
307 p = list;
308 while (xs_list_iter(&p, &v)) {
309 xs *ss = NULL;
310
311 if (strcmp(v, "```") == 0) {
312 if (!in_pre)
313 s = xs_str_cat(s, "<pre>");
314 else
315 s = xs_str_cat(s, "</pre>");
316
317 in_pre = !in_pre;
318 continue;
319 }
320
321 if (in_pre) {
322 // Encode all HTML characters when we're in pre element until we are out.
323 ss = encode_html(v);
324
325 s = xs_str_cat(s, ss);
326 s = xs_str_cat(s, "<br>");
327 continue;
328 }
329
330 else
331 ss = xs_strip_i(format_line(v, attach));
332
333 if (xs_startswith(ss, "---")) {
334 /* delete the --- */
335 ss = xs_strip_i(xs_crop_i(ss, 3, 0));
336 s = xs_str_cat(s, "<hr>");
337
338 s = xs_str_cat(s, ss);
339
340 continue;
341 }
342
343 //anzu - begin
344 // h1 reserved for snac?
345 if (xs_startswith(ss, "# ")) {
346 ss = xs_strip_i(xs_crop_i(ss, 2, 0));
347 s = xs_str_cat(s, "<h2>");
348 s = xs_str_cat(s, ss);
349 s = xs_str_cat(s, "</h2>");
350 continue;
351 }
352 if (xs_startswith(ss, "## ")) {
353 ss = xs_strip_i(xs_crop_i(ss, 3, 0));
354 s = xs_str_cat(s, "<h2>");
355 s = xs_str_cat(s, ss);
356 s = xs_str_cat(s, "</h2>");
357 continue;
358 }
359 if (xs_startswith(ss, "### ")) {
360 ss = xs_strip_i(xs_crop_i(ss, 4, 0));
361 s = xs_str_cat(s, "<h3>");
362 s = xs_str_cat(s, ss);
363 s = xs_str_cat(s, "</h3>");
364 continue;
365 }
366 //anzu - end
367
368 if (xs_startswith(ss, ">")) {
369 /* delete the > and subsequent spaces */
370 ss = xs_strip_i(xs_crop_i(ss, 1, 0));
371
372 if (!in_blq) {
373 s = xs_str_cat(s, "<blockquote>");
374 in_blq = 1;
375 }
376
377 s = xs_str_cat(s, ss);
378 s = xs_str_cat(s, "<br>");
379
380 continue;
381 }
382
383 if (xs_startswith(ss, "* ") || xs_startswith(ss, "- ")) {
384 /* unsorted list */
385 ss = xs_strip_i(xs_crop_i(ss, 1, 0));
386
387 if (!in_ul) {
388 s = xs_str_cat(s, "<ul>");
389 in_ul = 1;
390 }
391
392 s = xs_str_cat(s, "<li>", ss);
393
394 continue;
395 }
396
397 if (in_blq) {
398 s = xs_str_cat(s, "</blockquote>");
399 in_blq = 0;
400 }
401
402 if (in_ul) {
403 s = xs_str_cat(s, "</ul>");
404 in_ul = 0;
405 }
406
407 s = xs_str_cat(s, ss);
408 s = xs_str_cat(s, "<br>");
409 }
410
411 if (in_blq)
412 s = xs_str_cat(s, "</blockquote>");
413 if (in_pre)
414 s = xs_str_cat(s, "</pre>");
415
416 /* some beauty fixes */
417 s = xs_replace_i(s, "<br><br><blockquote>", "<br><blockquote>");
418 s = xs_replace_i(s, "</blockquote><br>", "</blockquote>");
419 s = xs_replace_i(s, "</pre><br>", "</pre>");
420 s = xs_replace_i(s, "</ul><br>", "</ul>");
421 s = xs_replace_i(s, "</h2><br>", "</h2>"); //anzu ???
422 s = xs_replace_i(s, "</h3><br>", "</h3>"); //anzu ???
423
424 {
425 /* traditional emoticons */
426 xs *d = emojis_rm_categories();
427 int c = 0;
428 const char *k, *v;
429
430 while (xs_dict_next(d, &k, &v, &c)) {
431 const char *t = xs_mime_by_ext(v);
432
433 /* is it an URL to an image? */
434 if (xs_startswith(v, "https:/" "/") &&
435 (xs_startswith(t, "image/") || strcmp(t, "application/octet-stream") == 0)) {
436 if (tag && xs_str_in(s, k) != -1) {
437 /* add the emoji to the tag list */
438 xs *e = xs_dict_new();
439 xs *i = xs_dict_new();
440 xs *u = xs_str_utctime(0, ISO_DATE_SPEC);
441
442 e = xs_dict_append(e, "id", v);
443 e = xs_dict_append(e, "type", "Emoji");
444 e = xs_dict_append(e, "name", k);
445 e = xs_dict_append(e, "updated", u);
446
447 i = xs_dict_append(i, "type", "Image");
448 i = xs_dict_append(i, "mediaType", t);
449 i = xs_dict_append(i, "url", v);
450 e = xs_dict_append(e, "icon", i);
451
452 *tag = xs_list_append(*tag, e);
453 }
454 }
455 else
456 s = xs_replace_i(s, k, v);
457 }
458 }
459
460 return s;
461}
462
463
464const char * const valid_tags[] = {
465 "a", "p", "br", "br/", "blockquote", "ul", "ol", "li", "cite", "small",
466 "span", "i", "b", "u", "s", "pre", "code", "em", "strong", "hr", "img", "del", "bdi",
467 "h2","h3", //anzu
468 NULL
469};
470
471xs_str *sanitize(const char *content)
472/* cleans dangerous HTML output */
473{
474 xs_str *s = xs_str_new(NULL);
475 xs *sl;
476 int n = 0;
477 char *p;
478 const char *v;
479
480 if (!content)
481 return NULL;
482
483 sl = xs_regex_split(content, "</?[^>]+>");
484
485 p = sl;
486
487 n = 0;
488 while (xs_list_iter(&p, &v)) {
489 if (n & 0x1) {
490 xs *s1 = xs_strip_i(xs_crop_i(xs_dup(v), v[1] == '/' ? 2 : 1, -1));
491 xs *l1 = xs_split_n(s1, " ", 1);
492 xs *tag = xs_utf8_to_lower(xs_list_get(l1, 0));
493 xs *s2 = NULL;
494 int i;
495
496 /* check if it's one of the valid tags */
497 for (i = 0; valid_tags[i]; i++) {
498 if (strcmp(tag, valid_tags[i]) == 0)
499 break;
500 }
501
502 if (valid_tags[i]) {
503 /* accepted tag: rebuild it with only the accepted elements */
504 xs *el = xs_regex_select(v, "(src|href|rel|class|target)=(\"[^\"]*\"|'[^']*')");
505 xs *s3 = xs_join(el, " ");
506
507 s2 = xs_fmt("<%s%s%s%s>",
508 v[1] == '/' ? "/" : "", tag, xs_list_len(el) ? " " : "", s3);
509
510 s = xs_str_cat(s, s2);
511 } else {
512 /* treat end of divs as paragraph breaks */
513 if (strcmp(v, "</div>"))
514 s = xs_str_cat(s, "<p>");
515 }
516 }
517 else {
518 /* non-tag */
519 s = xs_str_cat(s, v);
520 }
521
522 n++;
523 }
524
525 return s;
526}
527
528
529xs_str *encode_html(const char *str)
530/* escapes html characters */
531{
532 xs_str *encoded = xs_html_encode((char *)str);
533
534 /* Restore only <br>. Probably safe. Let's hope nothing goes wrong with this. */
535 encoded = xs_replace_i(encoded, "<br>", "<br>");
536
537 return encoded;
538}