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