2.54
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", "❤️",
29 ":facepalm:", "🤦",
30 ":shrug:", "🤷",
31 ":shrug2:", "¯\\_(ツ)_/¯",
32 ":eyeroll:", "🙄",
33 ":beer:", "🍺",
34 ":beers:", "🍻",
35 ":munch:", "😱",
36 ":thumb:", "👍",
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;
86 const char *v;
87
88 /* split by markup */
89 xs *sm = xs_regex_split(line,
90 "("
91 "`[^`]+`" "|"
92 "\\*\\*?[^\\*]+\\*?\\*" "|"
93 "\\[[^]]+\\]\\([^\\)]+\\)" "|"
94 "https?:/" "/[^[:space:]]+"
95 ")");
96 int n = 0;
97
98 p = sm;
99 while (xs_list_iter(&p, &v)) {
100 if ((n & 0x1)) {
101 /* markup */
102 if (xs_startswith(v, "`")) {
103 xs *s1 = xs_crop_i(xs_dup(v), 1, -1);
104 xs *e1 = encode_html(s1);
105 xs *s2 = xs_fmt("<code>%s</code>", e1);
106 s = xs_str_cat(s, s2);
107 }
108 else
109 if (xs_startswith(v, "**")) {
110 xs *s1 = xs_crop_i(xs_dup(v), 2, -2);
111 xs *s2 = xs_fmt("<b>%s</b>", s1);
112 s = xs_str_cat(s, s2);
113 }
114 else
115 if (xs_startswith(v, "*")) {
116 xs *s1 = xs_crop_i(xs_dup(v), 1, -1);
117 xs *s2 = xs_fmt("<i>%s</i>", s1);
118 s = xs_str_cat(s, s2);
119 }
120 else
121 if (xs_startswith(v, "http")) {
122 xs *u = xs_replace(v, "#", "#");
123 xs *v2 = xs_strip_chars_i(xs_dup(u), ".,)");
124
125 const char *mime = xs_mime_by_ext(v2);
126
127 if (attach != NULL && xs_startswith(mime, "image/")) {
128 /* if it's a link to an image, insert it as an attachment */
129 xs *d = xs_dict_new();
130
131 d = xs_dict_append(d, "mediaType", mime);
132 d = xs_dict_append(d, "url", v2);
133 d = xs_dict_append(d, "name", "");
134 d = xs_dict_append(d, "type", "Image");
135
136 *attach = xs_list_append(*attach, d);
137 }
138 else {
139 xs *s1 = xs_fmt("<a href=\"%s\" target=\"_blank\">%s</a>", v2, u);
140 s = xs_str_cat(s, s1);
141 }
142 }
143 else
144 if (*v == '[') {
145 /* markdown-like links [label](url) */
146 xs *w = xs_strip_chars_i(xs_dup(v), "[)");
147 xs *l = xs_split_n(w, "](", 1);
148
149 if (xs_list_len(l) == 2) {
150 xs *link = xs_fmt("<a href=\"%s\">%s</a>",
151 xs_list_get(l, 1), xs_list_get(l, 0));
152
153 s = xs_str_cat(s, link);
154 }
155 else
156 s = xs_str_cat(s, v);
157 }
158 else
159 s = xs_str_cat(s, v);
160 }
161 else
162 /* surrounded text, copy directly */
163 s = xs_str_cat(s, v);
164
165 n++;
166 }
167
168 return s;
169}
170
171
172xs_str *not_really_markdown(const char *content, xs_list **attach, xs_list **tag)
173/* formats a content using some Markdown rules */
174{
175 xs_str *s = xs_str_new(NULL);
176 int in_pre = 0;
177 int in_blq = 0;
178 xs *list;
179 char *p;
180 const char *v;
181
182 /* work by lines */
183 list = xs_split(content, "\n");
184
185 p = list;
186 while (xs_list_iter(&p, &v)) {
187 xs *ss = NULL;
188
189 if (strcmp(v, "```") == 0) {
190 if (!in_pre)
191 s = xs_str_cat(s, "<pre>");
192 else
193 s = xs_str_cat(s, "</pre>");
194
195 in_pre = !in_pre;
196 continue;
197 }
198
199 if (in_pre) {
200 // Encode all HTML characters when we're in pre element until we are out.
201 ss = encode_html(v);
202
203 s = xs_str_cat(s, ss);
204 s = xs_str_cat(s, "<br>");
205 continue;
206 }
207
208 else
209 ss = xs_strip_i(format_line(v, attach));
210
211 if (xs_startswith(ss, "---")) {
212 /* delete the --- */
213 ss = xs_strip_i(xs_crop_i(ss, 3, 0));
214 s = xs_str_cat(s, "<hr>");
215
216 s = xs_str_cat(s, ss);
217
218 continue;
219 }
220
221 if (xs_startswith(ss, ">")) {
222 /* delete the > and subsequent spaces */
223 ss = xs_strip_i(xs_crop_i(ss, 1, 0));
224
225 if (!in_blq) {
226 s = xs_str_cat(s, "<blockquote>");
227 in_blq = 1;
228 }
229
230 s = xs_str_cat(s, ss);
231 s = xs_str_cat(s, "<br>");
232
233 continue;
234 }
235
236 if (in_blq) {
237 s = xs_str_cat(s, "</blockquote>");
238 in_blq = 0;
239 }
240
241 s = xs_str_cat(s, ss);
242 s = xs_str_cat(s, "<br>");
243 }
244
245 if (in_blq)
246 s = xs_str_cat(s, "</blockquote>");
247 if (in_pre)
248 s = xs_str_cat(s, "</pre>");
249
250 /* some beauty fixes */
251 s = xs_replace_i(s, "<br><br><blockquote>", "<br><blockquote>");
252 s = xs_replace_i(s, "</blockquote><br>", "</blockquote>");
253 s = xs_replace_i(s, "</pre><br>", "</pre>");
254
255 {
256 /* traditional emoticons */
257 xs *d = emojis();
258 int c = 0;
259 const char *k, *v;
260
261 while (xs_dict_next(d, &k, &v, &c)) {
262 const char *t = NULL;
263
264 /* is it an URL to an image? */
265 if (xs_startswith(v, "https:/" "/") && xs_startswith((t = xs_mime_by_ext(v)), "image/")) {
266 if (tag && xs_str_in(s, k) != -1) {
267 /* add the emoji to the tag list */
268 xs *e = xs_dict_new();
269 xs *i = xs_dict_new();
270 xs *u = xs_str_utctime(0, ISO_DATE_SPEC);
271
272 e = xs_dict_append(e, "id", v);
273 e = xs_dict_append(e, "type", "Emoji");
274 e = xs_dict_append(e, "name", k);
275 e = xs_dict_append(e, "updated", u);
276
277 i = xs_dict_append(i, "type", "Image");
278 i = xs_dict_append(i, "mediaType", t);
279 i = xs_dict_append(i, "url", v);
280 e = xs_dict_append(e, "icon", i);
281
282 *tag = xs_list_append(*tag, e);
283 }
284 }
285 else
286 s = xs_replace_i(s, k, v);
287 }
288 }
289
290 return s;
291}
292
293
294const char *valid_tags[] = {
295 "a", "p", "br", "br/", "blockquote", "ul", "ol", "li", "cite", "small",
296 "span", "i", "b", "u", "s", "pre", "code", "em", "strong", "hr", "img", "del", "bdi", NULL
297};
298
299xs_str *sanitize(const char *content)
300/* cleans dangerous HTML output */
301{
302 xs_str *s = xs_str_new(NULL);
303 xs *sl;
304 int n = 0;
305 char *p;
306 const char *v;
307
308 sl = xs_regex_split(content, "</?[^>]+>");
309
310 p = sl;
311
312 n = 0;
313 while (xs_list_iter(&p, &v)) {
314 if (n & 0x1) {
315 xs *s1 = xs_strip_i(xs_crop_i(xs_dup(v), v[1] == '/' ? 2 : 1, -1));
316 xs *l1 = xs_split_n(s1, " ", 1);
317 xs *tag = xs_tolower_i(xs_dup(xs_list_get(l1, 0)));
318 xs *s2 = NULL;
319 int i;
320
321 /* check if it's one of the valid tags */
322 for (i = 0; valid_tags[i]; i++) {
323 if (strcmp(tag, valid_tags[i]) == 0)
324 break;
325 }
326
327 if (valid_tags[i]) {
328 /* accepted tag: rebuild it with only the accepted elements */
329 xs *el = xs_regex_select(v, "(src|href|rel|class|target)=\"[^\"]*\"");
330 xs *s3 = xs_join(el, " ");
331
332 s2 = xs_fmt("<%s%s%s%s>",
333 v[1] == '/' ? "/" : "", tag, xs_list_len(el) ? " " : "", s3);
334
335 s = xs_str_cat(s, s2);
336 } else {
337 /* treat end of divs as paragraph breaks */
338 if (strcmp(v, "</div>"))
339 s = xs_str_cat(s, "<p>");
340 }
341 }
342 else {
343 /* non-tag */
344 s = xs_str_cat(s, v);
345 }
346
347 n++;
348 }
349
350 return s;
351}
352
353
354xs_str *encode_html(const char *str)
355/* escapes html characters */
356{
357 xs_str *encoded = xs_html_encode((char *)str);
358
359 /* Restore only <br>. Probably safe. Let's hope nothing goes wrong with this. */
360 encoded = xs_replace_i(encoded, "<br>", "<br>");
361
362 return encoded;
363}