indent -kr -i8 -brf -bbb -fnc -l132 -nce on all of webcit-classic
[citadel.git] / webcit / blogview_renderer.c
1
2 /* 
3  * Blog view renderer module for WebCit
4  *
5  * Copyright (c) 1996-2012 by the citadel.org team
6  *
7  * This program is open source software.  You can redistribute it and/or
8  * modify it under the terms of the GNU General Public License, version 3.
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
16 #include "webcit.h"
17 #include "webserver.h"
18 #include "dav.h"
19
20 CtxType CTX_BLOGPOST = CTX_NONE;
21
22 typedef struct __BLOG {
23         HashList *BLOGPOSTS;
24         long p;
25         int gotonext;
26         long firstp;
27         long maxp;
28         StrBuf *Charset;
29         StrBuf *Buf;
30         const StrBuf *FilterTag;
31 } BLOG;
32
33 /* 
34  * Array type for a blog post.  The first message is the post; the rest are comments
35  */
36 typedef struct _blogpost {
37         int top_level_id;
38         long *msgs;             /* Array of msgnums for messages we are displaying */
39         int num_msgs;           /* Number of msgnums stored in 'msgs' */
40         int alloc_msgs;         /* Currently allocated size of array */
41         int unread_oments;
42 } blogpost;
43
44
45 /*
46  * XML sitemap generator -- go through the message list for a Blog room
47  */
48 void sitemap_do_blog(void) {
49         wcsession *WCC = WC;
50         blogpost oneBP;
51         int num_msgs = 0;
52         int i;
53         SharedMessageStatus Stat;
54         message_summary *Msg = NULL;
55         StrBuf *Buf = NewStrBuf();
56         StrBuf *FoundCharset = NewStrBuf();
57         WCTemplputParams SubTP;
58
59         memset(&Stat, 0, sizeof Stat);
60         memset(&oneBP, 0, sizeof(blogpost));
61         memset(&SubTP, 0, sizeof(WCTemplputParams));
62         StackContext(NULL, &SubTP, &oneBP, CTX_BLOGPOST, 0, NULL);
63
64         Stat.maxload = INT_MAX;
65         Stat.lowest_found = (-1);
66         Stat.highest_found = (-1);
67         num_msgs = load_msg_ptrs("MSGS ALL", NULL, NULL, &Stat, NULL, NULL, NULL, NULL, 0);
68         if (num_msgs < 1)
69                 return;
70
71         for (i = 0; i < num_msgs; ++i) {
72                 Msg = GetMessagePtrAt(i, WCC->summ);
73                 if (Msg != NULL) {
74                         ReadOneMessageSummary(Msg, FoundCharset, Buf);
75                         /* Show only top level posts, not comments */
76                         if ((Msg->reply_inreplyto_hash != 0) && (Msg->reply_references_hash == 0)) {
77                                 oneBP.top_level_id = Msg->reply_inreplyto_hash;
78                                 DoTemplate(HKEY("view_blog_sitemap"), WCC->WBuf, &SubTP);
79                         }
80                 }
81         }
82         UnStackContext(&SubTP);
83         FreeStrBuf(&Buf);
84         FreeStrBuf(&FoundCharset);
85 }
86
87
88
89 /*
90  * Generate a permalink for a post
91  * (Call with NULL arguments to make this function wcprintf() the permalink
92  * instead of writing it to the template)
93  */
94 void tmplput_blog_toplevel_id(StrBuf * Target, WCTemplputParams * TP) {
95         blogpost *bp = (blogpost *) CTX(CTX_BLOGPOST);
96         char buf[SIZ];
97         snprintf(buf, SIZ, "%d", bp->top_level_id);
98         StrBufAppendTemplateStr(Target, TP, buf, 0);
99 }
100
101 void tmplput_blog_comment_count(StrBuf * Target, WCTemplputParams * TP) {
102         blogpost *bp = (blogpost *) CTX(CTX_BLOGPOST);
103         char buf[SIZ];
104         snprintf(buf, SIZ, "%d", bp->num_msgs - 1);
105         StrBufAppendTemplateStr(Target, TP, buf, 0);
106 }
107 void tmplput_blog_comment_unread_count(StrBuf * Target, WCTemplputParams * TP) {
108         blogpost *bp = (blogpost *) CTX(CTX_BLOGPOST);
109         char buf[SIZ];
110         snprintf(buf, SIZ, "%d", bp->unread_oments);
111         StrBufAppendTemplateStr(Target, TP, buf, 0);
112 }
113
114
115
116 /*
117  * Render a single blog post and (optionally) its comments
118  */
119 void blogpost_render(blogpost * bp, int with_comments, WCTemplputParams * TP) {
120         wcsession *WCC = WC;
121         WCTemplputParams SubTP;
122         const StrBuf *Mime;
123         int i;
124
125         memset(&SubTP, 0, sizeof(WCTemplputParams));
126         StackContext(TP, &SubTP, bp, CTX_BLOGPOST, 0, NULL);
127
128         /* Always show the top level post, unless we somehow ended up with an empty list */
129         if (bp->num_msgs > 0) {
130                 read_message(WC->WBuf, HKEY("view_blog_post"), bp->msgs[0], NULL, &Mime, TP);
131         }
132
133         if (with_comments) {
134                 /* Show any existing comments, then offer the comment box */
135                 DoTemplate(HKEY("view_blog_show_commentlink"), WCC->WBuf, &SubTP);
136
137                 for (i = 1; i < bp->num_msgs; ++i) {
138                         read_message(WC->WBuf, HKEY("view_blog_comment"), bp->msgs[i], NULL, &Mime, &SubTP);
139                 }
140                 DoTemplate(HKEY("view_blog_comment_box"), WCC->WBuf, &SubTP);
141         }
142
143         else {
144                 /* Show only the number of comments */
145                 DoTemplate(HKEY("view_blog_show_no_comments"), WCC->WBuf, &SubTP);
146         }
147         UnStackContext(&SubTP);
148 }
149
150
151 /*
152  * Destructor for "blogpost"
153  */
154 void blogpost_destroy(blogpost * bp) {
155         if (bp->alloc_msgs > 0) {
156                 free(bp->msgs);
157         }
158         free(bp);
159 }
160
161
162 /*
163  * Entry point for message read operations.
164  */
165 int blogview_GetParamsGetServerCall(SharedMessageStatus * Stat,
166                                     void **ViewSpecific, long oper, char *cmd, long len, char *filter, long flen) {
167         BLOG *BL = (BLOG *) malloc(sizeof(BLOG));
168         BL->BLOGPOSTS = NewHash(1, lFlathash);
169
170         /* are we looking for a specific post? */
171         BL->p = lbstr("p");
172         BL->gotonext = havebstr("gotonext");
173         BL->Charset = NewStrBuf();
174         BL->Buf = NewStrBuf();
175         BL->FilterTag = sbstr("FilterTag");
176         BL->firstp = lbstr("firstp");   /* start reading at... */
177         BL->maxp = lbstr("maxp");       /* max posts to show... */
178         if (BL->maxp < 1)
179                 BL->maxp = 5;   /* default; move somewhere else? */
180         putlbstr("maxp", BL->maxp);
181         *ViewSpecific = BL;
182
183         Stat->startmsg = (-1);  /* not used here */
184         Stat->sortit = 1;       /* not used here */
185         Stat->num_displayed = DEFAULT_MAXMSGS;  /* not used here */
186         if (Stat->maxmsgs == 0)
187                 Stat->maxmsgs = DEFAULT_MAXMSGS;
188
189         /* perform a "read all" call to fetch the message list -- we'll cut it down later */
190         snprintf(cmd, len, "MSGS ALL||2|8\n");
191
192         if (BL->gotonext)
193                 Stat->load_seen = 1;
194         return 200;
195 }
196
197
198 int blogview_IdentifyBlogposts(StrBuf * Line,
199                                const char **pos, message_summary * Msg, StrBuf * ConversionBuffer, void **ViewSpecific) {
200         BLOG *BL = (BLOG *) * ViewSpecific;
201         blogpost *bp = NULL;
202
203         /* Stop processing if the viewer is only interested in a single post and
204          * that message ID is neither the id nor the refs.
205          */
206         if ((BL->p != 0) && (BL->p != Msg->reply_inreplyto_hash) && (BL->p != Msg->reply_references_hash)) {
207                 return 0;
208         }
209
210         if ((Msg->reply_references_hash == 0) &&
211             (BL->FilterTag != NULL) && (strstr(ChrPtr(Msg->EnvTo), ChrPtr(BL->FilterTag)) == NULL)) {
212                 /* filtering for tags, blogpost doesn't fit. */
213                 return 0;
214         }
215
216         /*
217          * build up a hashtable of the blogposts.
218          */
219         if (Msg->reply_references_hash == 0) {
220                 bp = malloc(sizeof(blogpost));
221
222                 if (bp == NULL)
223                         return 0;
224
225                 memset(bp, 0, sizeof(blogpost));
226
227                 bp->top_level_id = Msg->reply_inreplyto_hash;
228                 bp->alloc_msgs = 1000;
229                 bp->msgs = malloc(bp->alloc_msgs * sizeof(long));
230                 memset(bp->msgs, 0, (bp->alloc_msgs * sizeof(long)));
231
232                 /* the first one is the blogpost itself, all subequent are comments. */
233                 bp->msgs[0] = Msg->msgnum;
234                 bp->num_msgs = 1;
235
236                 Put(BL->BLOGPOSTS, LKEY(Msg->reply_inreplyto_hash), bp, (DeleteHashDataFunc) blogpost_destroy);
237         }
238         /*
239          * Comments will be handled on the next iteration.
240          */
241
242         return 1;
243 }
244
245
246 /*
247  * This function is called for every message in the list.
248  */
249 int blogview_LoadMsgFromServer(SharedMessageStatus * Stat, void **ViewSpecific, message_summary * Msg, int is_new, int i) {
250         blogpost *bp = NULL;
251         BLOG *BL = (BLOG *) * ViewSpecific;
252
253         if (Msg->reply_references_hash != 0) {
254                 /* 
255                  * this is a comment. try to assign it to a blogpost.
256                  */
257                 GetHash(BL->BLOGPOSTS, LKEY(Msg->reply_references_hash), (void *) &bp);
258
259                 /*
260                  * Now we have a 'blogpost' to which we can add the comment.  It's either the
261                  * blog post itself or a comment attached to it; either way, the code is the same from
262                  * this point onward.
263                  */
264                 if (bp != NULL) {
265                         if (bp->num_msgs >= bp->alloc_msgs) {
266                                 bp->alloc_msgs *= 2;
267                                 bp->msgs = realloc(bp->msgs, (bp->alloc_msgs * sizeof(long)));
268                                 memset(&bp->msgs[bp->num_msgs], 0, ((bp->alloc_msgs - bp->num_msgs) * sizeof(long)));
269                         }
270                         bp->msgs[bp->num_msgs++] = Msg->msgnum;
271                         if ((Msg->Flags & MSGFLAG_READ) != 0) {
272                                 bp->unread_oments++;
273                         }
274                 }
275                 else {
276                         /*
277                          * Ok, this comment probably belongs to one of the blogposts
278                          * we ruled out by filters. don't load it.
279                          */
280                         return 200;
281                 }
282         }
283         return 200;
284 }
285
286
287 /*
288  * Sort a list of 'struct blogpost' pointers by newest-to-oldest msgnum.
289  * With big thanks to whoever wrote http://www.c.happycodings.com/Sorting_Searching/code14.html
290  */
291 static int blogview_sortfunc(const void *a, const void *b) {
292         blogpost const *one = GetSearchPayload(a);
293         blogpost const *two = GetSearchPayload(b);
294
295         if (one->msgs[0] > two->msgs[0])
296                 return (-1);
297         if (one->msgs[0] < two->msgs[0])
298                 return (+1);
299         return (0);
300 }
301
302
303 /*
304  * All blogpost entries are now in the hash list.
305  * Sort them, select the desired range, and render what we want to see.
306  */
307 int blogview_render(SharedMessageStatus * Stat, void **ViewSpecific, long oper) {
308         wcsession *WCC = WC;
309         BLOG *BL = (BLOG *) * ViewSpecific;
310         HashPos *it;
311         const char *Key;
312         blogpost *thisBlogpost;
313         void *Data;
314         long len;
315         int num_blogposts = 0;
316         int with_comments = 0;
317         WCTemplputParams SubTP;
318         WCTemplputParams StopSubTP;
319         blogpost oneBP;
320         long firstPOffset = 0;
321         int count = 0;
322         int totalCount = 0;
323         StrBuf *PrevNext = NULL;
324
325         num_blogposts = GetCount(BL->BLOGPOSTS);
326         if (num_blogposts == 0) {
327                 /* Nothing to do... */
328                 return 0;
329         }
330         memset(&SubTP, 0, sizeof(WCTemplputParams));
331         memset(&StopSubTP, 0, sizeof(WCTemplputParams));
332         memset(&oneBP, 0, sizeof(blogpost));
333
334         /* Comments are shown if we are only viewing a single blog post */
335         with_comments = (BL->p != 0);
336
337         it = GetNewHashPos(BL->BLOGPOSTS, 0);
338
339         if ((BL->gotonext) && (BL->p == 0)) {
340                 /* did we come here via gotonext? lets find out whether
341                  * this blog has just one blogpost with new comments just display 
342                  * this one.
343                  */
344                 blogpost *unread_bp = NULL;
345                 int unread_count = 0;
346                 while (GetNextHashPos(BL->BLOGPOSTS, it, &len, &Key, &Data)) {
347                         blogpost *one_bp = (blogpost *) Data;
348                         if (one_bp->unread_oments > 0) {
349                                 unread_bp = one_bp;
350                                 unread_count++;
351                         }
352                 }
353                 if (unread_count == 1) {
354                         blogpost_render(unread_bp, 1, NULL);    /// TODO other than null?
355
356                         DeleteHashPos(&it);
357                         return 0;
358                 }
359         }
360
361         /* Now we have our array.  It is ONLY an array of pointers.  The objects to
362          * which they point are still owned by the hash list.
363          */
364
365         /* get it into the right sort of order - blogposts may have been edited */
366         SortByPayload(BL->BLOGPOSTS, blogview_sortfunc);
367
368         /* Find the start post to display: */
369         if (BL->firstp != 0) {
370                 /* Translate the firstpost id into an offset */
371                 RewindHashPos(BL->BLOGPOSTS, it, 0);
372                 while (GetNextHashPos(BL->BLOGPOSTS, it, &len, &Key, &Data)) {
373                         thisBlogpost = (blogpost *) Data;
374                         if (thisBlogpost->top_level_id == BL->firstp) {
375                                 firstPOffset = count;
376                                 break;
377                         }
378                         count++;
379                 }
380         }
381
382         if ((num_blogposts > BL->maxp) || (firstPOffset != 0)) {
383                 PrevNext = NewStrBuf();
384                 if (firstPOffset > 0) {
385                         const char *k;
386                         long len;
387                         long posPrev = 0;
388                         /* we now need to go up to maxp items back */
389                         if (firstPOffset > BL->maxp) {
390                                 posPrev = firstPOffset - BL->maxp;
391                         }
392                         GetHashAt(BL->BLOGPOSTS, posPrev, &len, &k, &Data);
393                         thisBlogpost = (blogpost *) Data;
394                         StackContext(NULL, &SubTP, thisBlogpost, CTX_BLOGPOST, 0, NULL);
395                         DoTemplate(HKEY("view_blog_newer_posts"), PrevNext, &SubTP);
396                 }
397                 if (firstPOffset + BL->maxp <= num_blogposts) {
398                         const char *k;
399                         long len;
400                         long posNext = firstPOffset + BL->maxp;
401                         GetHashAt(BL->BLOGPOSTS, posNext, &len, &k, &Data);
402                         thisBlogpost = (blogpost *) Data;
403                         StackContext(NULL, &SubTP, thisBlogpost, CTX_BLOGPOST, 0, NULL);
404                         DoTemplate(HKEY("view_blog_older_posts"), PrevNext, &SubTP);
405                 }
406         }
407
408         StrBufAppendBuf(WCC->WBuf, PrevNext, 0);
409         count = totalCount = 0;
410         RewindHashPos(BL->BLOGPOSTS, it, 0);
411
412         /* FIXME -- allow the user (or a default setting) to select a maximum number of posts to display */
413         while (GetNextHashPos(BL->BLOGPOSTS, it, &len, &Key, &Data)) {
414                 thisBlogpost = (blogpost *) Data;
415
416                 /* allow the user to select a starting point in the list */
417                 if (totalCount < firstPOffset) {
418                         /* skip all till we found the first valid: */
419                         totalCount++;
420                         continue;
421                 }
422                 if (count >= BL->maxp) {
423                         /* enough is enough. */
424                         break;
425                 }
426                 StackContext(NULL, &SubTP, thisBlogpost, CTX_BLOGPOST, 0, NULL);
427                 blogpost_render(thisBlogpost, with_comments, &SubTP);
428                 UnStackContext(&SubTP);
429                 count++;
430                 totalCount++;
431         }
432         StrBufAppendBuf(WCC->WBuf, PrevNext, 0);
433         FreeStrBuf(&PrevNext);
434         DeleteHashPos(&it);
435
436         return (0);
437 }
438
439
440 int blogview_Cleanup(void **ViewSpecific) {
441         BLOG *BL = (BLOG *) * ViewSpecific;
442
443         FreeStrBuf(&BL->Buf);
444         FreeStrBuf(&BL->Charset);
445         DeleteHash(&BL->BLOGPOSTS);
446         free(BL);
447         wDumpContent(1);
448         return 0;
449 }
450
451
452 void InitModule_BLOGVIEWRENDERERS(void) {
453         const char *browseListFields[] = {
454                 "msgn",
455                 "nvto",
456                 "wefw",
457                 NULL
458         };
459         RegisterCTX(CTX_BLOGPOST);
460
461         RegisterReadLoopHandlerset(VIEW_BLOG,
462                                    blogview_GetParamsGetServerCall,
463                                    NULL,
464                                    NULL,
465                                    blogview_IdentifyBlogposts,
466                                    blogview_LoadMsgFromServer, blogview_render, blogview_Cleanup, browseListFields);
467
468         RegisterNamespace("BLOG:TOPLEVEL:MSGID", 0, 0, tmplput_blog_toplevel_id, NULL, CTX_BLOGPOST);
469         RegisterNamespace("BLOG:COMMENTS:COUNT", 0, 0, tmplput_blog_comment_count, NULL, CTX_BLOGPOST);
470         RegisterNamespace("BLOG:COMMENTS:UNREAD:COUNT", 0, 0, tmplput_blog_comment_unread_count, NULL, CTX_BLOGPOST);
471 }