fb88eddcd968221f8fde786d6b254984ae6c354c
[citadel.git] / webcit-ng / forum_view.c
1 /*
2  * Forum view (threaded/flat)
3  *
4  * Copyright (c) 1996-2018 by the citadel.org team
5  *
6  * This program is open source software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License version 3.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  */
14
15 #include "webcit.h"
16
17 struct mthread {
18         long msgnum;
19         time_t datetime;
20         int threadhash;
21         int refhashes[10];
22         char from[64];
23         int parent;
24 };
25
26
27 // Commands we need to send to Citadel Server before we begin rendering forum view.
28 // These are common to flat and threaded views.
29 //
30 void setup_for_forum_view(struct ctdlsession *c)
31 {
32         char buf[1024];
33         ctdl_printf(c, "MSGP text/html|text/plain");            // Declare the MIME types we know how to render
34         ctdl_readline(c, buf, sizeof(buf));                     // Ignore the response
35         ctdl_printf(c, "MSGP dont_decode");                     // Tell the server we will decode base64/etc client-side
36         ctdl_readline(c, buf, sizeof(buf));                     // Ignore the response
37 }
38
39
40 #if 0
41 // Renderer for one message in the threaded view
42 // (This will probably work for the flat view too.)
43 //
44 void forum_render_one_message(struct ctdlsession *c, StrBuf *sj, long msgnum)
45 {
46         StrBuf *raw_msg = NULL;
47         StrBuf *sanitized_msg = NULL;
48         char buf[1024];
49         char content_transfer_encoding[1024] = { 0 };
50         char content_type[1024] = { 0 };
51         char author[128] = { 0 };
52         char datetime[128] = { 0 } ;
53
54         ctdl_printf(c, "MSG4 %ld", msgnum);
55         ctdl_readline(c, buf, sizeof(buf));
56         if (buf[0] != '1') {
57                 StrBufAppendPrintf(sj, "<div>ERROR CONDITION FIXME WRITE A BOX</div>");
58                 return;
59         }
60
61         while ( (ctdl_readline(c, buf, sizeof(buf)) >= 0) && (strcmp(buf, "text")) && (strcmp(buf, "000")) ) {
62                 // citadel header parsing here
63                 if (!strncasecmp(buf, "from=", 5)) {
64                         safestrncpy(author, &buf[5], sizeof author);
65                 }
66                 if (!strncasecmp(buf, "time=", 5)) {
67                         time_t tt;
68                         struct tm tm;
69                         tt = atol(&buf[5]);
70                         localtime_r(&tt, &tm);
71                         strftime(datetime, sizeof datetime, "%c", &tm);
72                 }
73         }
74
75         if (!strcmp(buf, "text")) {
76                 while ( (ctdl_readline(c, buf, sizeof(buf)) >= 0) && (strcmp(buf, "")) && (strcmp(buf, "000")) ) {
77                         // rfc822 header parsing here
78                         if (!strncasecmp(buf, "Content-transfer-encoding:", 26)) {
79                                 strcpy(content_transfer_encoding, &buf[26]);
80                                 striplt(content_transfer_encoding);
81                         }
82                         if (!strncasecmp(buf, "Content-type:", 13)) {
83                                 strcpy(content_type, &buf[13]);
84                                 striplt(content_type);
85                         }
86                 }
87                 raw_msg = ctdl_readtextmsg(c);
88         }
89         else {
90                 raw_msg = NULL;
91         }
92
93         // begin output
94
95         StrBufAppendPrintf(sj, "<div>");                                                // begin message wrapper
96         StrBufAppendPrintf(sj, "<div style=\"float:left;padding-right:2px\">");         // begin avatar FIXME move the style to a stylesheet
97         StrBufAppendPrintf(sj, "<i class=\"fa fa-user-circle fa-2x\"></i> ");           // FIXME temporary avatar
98         StrBufAppendPrintf(sj, "</div>");                                               // end avatar
99         StrBufAppendPrintf(sj, "<div>");                                                // begin content
100         StrBufAppendPrintf(sj, "<div>");                                                // begin header
101         StrBufAppendPrintf(sj, "<span class=\"ctdl-username\"><a href=\"#\">%s</a></span> ", author);   // FIXME link to user profile or whatever
102         StrBufAppendPrintf(sj, "<span class=\"ctdl-msgdate\">%s</span> ", datetime);
103         StrBufAppendPrintf(sj, "</div>");                                               // end header
104         StrBufAppendPrintf(sj, "<div>");                                                // begin body
105
106         if (raw_msg) {
107
108                 // These are the encodings we know how to handle.  Decode in-place.
109
110                 if (!strcasecmp(content_transfer_encoding, "base64")) {
111                         StrBufDecodeBase64(raw_msg);
112                 }
113                 if (!strcasecmp(content_transfer_encoding, "quoted-printable")) {
114                         StrBufDecodeQP(raw_msg);
115                 }
116
117                 // At this point, raw_msg contains the decoded message.
118                 // Now run through the renderers we have available.
119
120                 if (!strncasecmp(content_type, "text/html", 9)) {
121                         sanitized_msg = html2html("UTF-8", 0, c->room, msgnum, raw_msg);
122                 }
123                 else if (!strncasecmp(content_type, "text/plain", 10)) {
124                         sanitized_msg = text2html("UTF-8", 0, c->room, msgnum, raw_msg);
125                 }
126                 else if (!strncasecmp(content_type, "text/x-citadel-variformat", 25)) {
127                         sanitized_msg = variformat2html(raw_msg);
128                 }
129                 else {
130                         sanitized_msg = NewStrBufPlain(HKEY("<i>No renderer for this content type</i><br>"));
131                 }
132                 FreeStrBuf(&raw_msg);
133
134                 // If sanitized_msg is not NULL, we have rendered the message and can output it.
135
136                 if (sanitized_msg) {
137                         StrBufAppendBuf(sj, sanitized_msg, 0);
138                         FreeStrBuf(&sanitized_msg);
139                 }
140         }
141
142         StrBufAppendPrintf(sj, "</div>");                                               // end body
143         StrBufAppendPrintf(sj, "</div>");                                               // end content
144         StrBufAppendPrintf(sj, "</div>");                                               // end wrapper
145 }
146
147
148 // This code implements the thread display code.  The thread sorting algorithm is working nicely but we're trying
149 // not to do rendering in the C server of webcit.  Maybe move it into the server as "MSGS threaded" or something like that?
150
151 // Threaded view (recursive section)
152 //
153 void thread_o_print(struct ctdlsession *c, StrBuf *sj, struct mthread *m, int num_msgs, int where_parent_is, int nesting_level)
154 {
155         int i = 0;
156         int j = 0;
157         int num_printed = 0;
158
159         for (i=0; i<num_msgs; ++i) {
160                 if (m[i].parent == where_parent_is) {
161
162                         if (++num_printed == 1) {
163                                 StrBufAppendPrintf(sj, "<ul style=\"list-style-type: none;\">");
164                         }
165
166                         StrBufAppendPrintf(sj, "<li class=\"post\" id=\"post-%ld\">",  m[i].msgnum);
167                         forum_render_one_message(c, sj, m[i].msgnum);
168                         StrBufAppendPrintf(sj, "</li>\r\n");
169                         if (i != 0) {
170                                 thread_o_print(c, sj, m, num_msgs, i, nesting_level+1);
171                         }
172                 }
173         }
174
175         if (num_printed > 0) {
176                 StrBufAppendPrintf(sj, "</ul>");
177         }
178 }
179
180
181 // Threaded view (entry point)
182 //
183 void threaded_view(struct http_transaction *h, struct ctdlsession *c, char *which)
184 {
185         int num_msgs = 0;
186         int num_alloc = 0;
187         struct mthread *m;
188         char buf[1024];
189         char refs[1024];
190         int i, j, k;
191
192         ctdl_printf(c, "MSGS ALL|||9");                 // 9 == headers + thread references
193         ctdl_readline(c, buf, sizeof(buf));
194         if (buf[0] != '1') {
195                 do_404(h);
196                 return;
197         }
198
199         StrBuf *sj = NewStrBuf();
200         StrBufAppendPrintf(sj, "<html><body>\r\n");
201
202         while (ctdl_readline(c, buf, sizeof buf), strcmp(buf,"000")) {
203
204                 ++num_msgs;
205                 if (num_msgs > num_alloc) {
206                         if (num_alloc == 0) {
207                                 num_alloc = 100;
208                                 m = malloc(num_alloc * sizeof(struct mthread));
209                         }
210                         else {
211                                 num_alloc *= 2;
212                                 m = realloc(m, (num_alloc * sizeof(struct mthread)));
213                         }
214                 }
215
216                 memset(&m[num_msgs-1], 0, sizeof(struct mthread));
217                 m[num_msgs-1].msgnum = extract_long(buf, 0);
218                 m[num_msgs-1].datetime = extract_long(buf, 1);
219                 extract_token(m[num_msgs-1].from, buf, 2, '|', sizeof m[num_msgs-1].from);
220                 m[num_msgs-1].threadhash = extract_int(buf, 6);
221                 extract_token(refs, buf, 7, '|', sizeof refs);
222
223                 char *t;
224                 char *r = refs;
225                 i = 0;
226                 while ((t = strtok_r(r, ",", &r))) {
227                         if (i == 0) {
228                                 m[num_msgs-1].refhashes[0] = atoi(t);           // always keep the first one
229                         }
230                         else {
231                                 memcpy(&m[num_msgs-1].refhashes[1], &m[num_msgs-1].refhashes[2], sizeof(int)*8 );       // shift the rest
232                                 m[num_msgs-1].refhashes[9] = atoi(t);
233                         }
234                         ++i;
235                 }
236
237         }
238
239         // Sort by thread.  I did read jwz's sorting algorithm and it looks pretty good, but jwz is a self-righteous asshole so we do it our way.
240         for (i=0; i<num_msgs; ++i) {
241                 for (j=9; (j>=0)&&(m[i].parent==0); --j) {
242                         for (k=0; (k<num_msgs)&&(m[i].parent==0); ++k) {
243                                 if (m[i].refhashes[j] == m[k].threadhash) {
244                                         m[i].parent = k;
245                                 }
246                         }
247                 }
248         }
249
250         // Now render it
251         setup_for_forum_view(c);
252         thread_o_print(c, sj, m, num_msgs, 0, 0);               // Render threads recursively and recursively
253
254         // Garbage collection is for people who aren't smart enough to manage their own memory.
255         if (num_msgs > 0) {
256                 free(m);
257         }
258
259         StrBufAppendPrintf(sj, "</body></html>\r\n");
260
261         add_response_header(h, strdup("Content-type"), strdup("text/html; charset=utf-8"));
262         h->response_code = 200;
263         h->response_string = strdup("OK");
264         h->response_body_length = StrLength(sj);
265         h->response_body = SmashStrBuf(&sj);
266         return;
267 }
268
269
270 // flat view (entry point)
271 //
272 void flat_view(struct http_transaction *h, struct ctdlsession *c, char *which)
273 {
274         StrBuf *sj = NewStrBuf();
275         StrBufAppendPrintf(sj, "<html><body>\r\n");
276
277         setup_for_forum_view(c);
278         long *msglist = get_msglist(c, "ALL");
279         if (msglist) {
280                 int i;
281                 for (i=0; (msglist[i] > 0); ++i) {
282                         forum_render_one_message(c, sj, msglist[i]);
283                 }
284                 free(msglist);
285         }
286
287         StrBufAppendPrintf(sj, "</body></html>\r\n");
288
289         add_response_header(h, strdup("Content-type"), strdup("text/html; charset=utf-8"));
290         h->response_code = 200;
291         h->response_string = strdup("OK");
292         h->response_body_length = StrLength(sj);
293         h->response_body = SmashStrBuf(&sj);
294         return;
295 }
296
297
298 // render one message (entire transaction)      FIXME EXTERMINATE
299 //
300 void html_render_one_message(struct http_transaction *h, struct ctdlsession *c, long msgnum)
301 {
302         StrBuf *sj = NewStrBuf();
303         StrBufAppendPrintf(sj, "<html><body>\r\n");
304         setup_for_forum_view(c);                // FIXME way too inefficient to do this for every message !!!!!!!!!!!!!
305         forum_render_one_message(c, sj, msgnum);
306         StrBufAppendPrintf(sj, "</body></html>\r\n");
307         add_response_header(h, strdup("Content-type"), strdup("text/html; charset=utf-8"));
308         h->response_code = 200;
309         h->response_string = strdup("OK");
310         h->response_body_length = StrLength(sj);
311         h->response_body = SmashStrBuf(&sj);
312         return;
313 }
314
315 #endif
316
317 // Fetch a single message and return it in JSON format for client-side rendering
318 //
319 void json_render_one_message(struct http_transaction *h, struct ctdlsession *c, long msgnum)
320 {
321         StrBuf *raw_msg = NULL;
322         StrBuf *sanitized_msg = NULL;
323         char buf[1024];
324         char content_transfer_encoding[1024] = { 0 };
325         char content_type[1024] = { 0 };
326         char author[128] = { 0 };
327         char datetime[128] = { 0 } ;
328
329         setup_for_forum_view(c);
330
331         ctdl_printf(c, "MSG4 %ld", msgnum);
332         ctdl_readline(c, buf, sizeof(buf));
333         if (buf[0] != '1') {
334                 do_404(h);
335                 return;
336         }
337
338         JsonValue *j = NewJsonObject(HKEY("message"));
339
340         while ( (ctdl_readline(c, buf, sizeof(buf)) >= 0) && (strcmp(buf, "text")) && (strcmp(buf, "000")) ) {
341                 // citadel header parsing here
342                 if (!strncasecmp(buf, "from=", 5)) {
343                         JsonObjectAppend(j, NewJsonPlainString( HKEY("from"), &buf[5], -1));
344                 }
345                 if (!strncasecmp(buf, "rfca=", 5)) {
346                         JsonObjectAppend(j, NewJsonPlainString( HKEY("from"), &buf[5], -1));
347                 }
348                 if (!strncasecmp(buf, "time=", 5)) {
349                         time_t tt;
350                         struct tm tm;
351                         tt = atol(&buf[5]);
352                         localtime_r(&tt, &tm);
353                         strftime(datetime, sizeof datetime, "%c", &tm);
354                         JsonObjectAppend(j, NewJsonPlainString( HKEY("time"), datetime, -1));
355                 }
356         }
357
358         if (!strcmp(buf, "text")) {
359                 while ( (ctdl_readline(c, buf, sizeof(buf)) >= 0) && (strcmp(buf, "")) && (strcmp(buf, "000")) ) {
360                         // rfc822 header parsing here
361                         if (!strncasecmp(buf, "Content-transfer-encoding:", 26)) {
362                                 strcpy(content_transfer_encoding, &buf[26]);
363                                 striplt(content_transfer_encoding);
364                         }
365                         if (!strncasecmp(buf, "Content-type:", 13)) {
366                                 strcpy(content_type, &buf[13]);
367                                 striplt(content_type);
368                         }
369                 }
370                 raw_msg = ctdl_readtextmsg(c);
371         }
372         else {
373                 raw_msg = NULL;
374         }
375
376         if (raw_msg) {
377
378                 // These are the encodings we know how to handle.  Decode in-place.
379
380                 if (!strcasecmp(content_transfer_encoding, "base64")) {
381                         StrBufDecodeBase64(raw_msg);
382                 }
383                 if (!strcasecmp(content_transfer_encoding, "quoted-printable")) {
384                         StrBufDecodeQP(raw_msg);
385                 }
386
387                 // At this point, raw_msg contains the decoded message.
388                 // Now run through the renderers we have available.
389
390                 if (!strncasecmp(content_type, "text/html", 9)) {
391                         sanitized_msg = html2html("UTF-8", 0, c->room, msgnum, raw_msg);
392                 }
393                 else if (!strncasecmp(content_type, "text/plain", 10)) {
394                         sanitized_msg = text2html("UTF-8", 0, c->room, msgnum, raw_msg);
395                 }
396                 else if (!strncasecmp(content_type, "text/x-citadel-variformat", 25)) {
397                         sanitized_msg = variformat2html(raw_msg);
398                 }
399                 else {
400                         sanitized_msg = NewStrBufPlain(HKEY("<i>No renderer for this content type</i><br>"));
401                 }
402                 FreeStrBuf(&raw_msg);
403
404                 // If sanitized_msg is not NULL, we have rendered the message and can output it.
405
406                 if (sanitized_msg) {
407                         JsonObjectAppend(j, NewJsonString(HKEY("text"), sanitized_msg, NEWJSONSTRING_SMASHBUF));
408                 }
409         }
410
411         StrBuf *sj = NewStrBuf();
412         SerializeJson(sj, j, 1);                        // '1' == free the source object
413
414         add_response_header(h, strdup("Content-type"), strdup("application/json"));
415         h->response_code = 200;
416         h->response_string = strdup("OK");
417         h->response_body_length = StrLength(sj);
418         h->response_body = SmashStrBuf(&sj);
419 }