Template the last bits of the blog view.
[citadel.git] / 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)
115 {
116         wcsession *WCC = WC;
117         WCTemplputParams SubTP;
118         const StrBuf *Mime;
119         int i;
120
121         memset(&SubTP, 0, sizeof(WCTemplputParams));    
122         StackContext(NULL, &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);
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);
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         blogpost oneBP;
301
302         memset(&SubTP, 0, sizeof(WCTemplputParams));    
303         memset(&oneBP, 0, sizeof(blogpost));
304
305         /* Comments are shown if we are only viewing a single blog post */
306         with_comments = (BL->p != 0);
307
308         firstp = ibstr("firstp");   /* start reading at... */
309         maxp   = ibstr("maxp");     /* max posts to show... */
310         if (maxp < 1) maxp = 5;     /* default; move somewhere else? */
311         putlbstr("maxp", maxp);
312
313         it = GetNewHashPos(BL->BLOGPOSTS, 0);
314
315         if ((BL->gotonext) && (BL->p == 0)) {
316                 /* did we come here via gotonext? lets find out whether
317                  * this blog has just one blogpost with new comments just display 
318                  * this one.
319                  */
320                 blogpost *unread_bp = NULL;
321                 int unread_count = 0;
322                 while (GetNextHashPos(BL->BLOGPOSTS, it, &len, &Key, &Data)) {
323                         blogpost *one_bp = (blogpost *) Data;
324                         if (one_bp->unread_oments > 0) {
325                                 unread_bp = one_bp;
326                                 unread_count++;
327                         }
328                 }
329                 if (unread_count == 1) {
330                         blogpost_render(unread_bp, 1);
331
332                         DeleteHashPos(&it);
333                         return 0;
334                 }
335
336                 RewindHashPos(BL->BLOGPOSTS, it, 0);
337         }
338
339         /* Iterate through the hash list and copy the data pointers into an array */
340         while (GetNextHashPos(BL->BLOGPOSTS, it, &len, &Key, &Data)) {
341                 if (num_blogposts >= num_blogposts_alloc) {
342                         if (num_blogposts_alloc == 0) {
343                                 num_blogposts_alloc = 100;
344                         }
345                         else {
346                                 num_blogposts_alloc *= 2;
347                         }
348                         blogposts = realloc(blogposts, (num_blogposts_alloc * sizeof (blogpost *)));
349                 }
350                 blogposts[num_blogposts++] = (blogpost *) Data;
351         }
352         DeleteHashPos(&it);
353
354         /* Now we have our array.  It is ONLY an array of pointers.  The objects to
355          * which they point are still owned by the hash list.
356          */
357         if (num_blogposts > 0) {
358                 int start_here = 0;
359                 /* Sort newest-to-oldest */
360                 qsort(blogposts, num_blogposts, sizeof(void *), blogview_sortfunc);
361
362                 /* allow the user to select a starting point in the list */
363                 for (i=0; i<num_blogposts; ++i) {
364                         if (blogposts[i]->top_level_id == firstp) {
365                                 start_here = i;
366                         }
367                 }
368
369                 /* FIXME -- allow the user (or a default setting) to select a maximum number of posts to display */
370
371                 /* Now go through the list and render what we've got */
372                 for (i=start_here; i<num_blogposts; ++i) {
373                         if ((i > 0) && (i == start_here)) {
374                                 int j = i - maxp;
375                                 if (j < 0) j = 0;
376
377                                 StackContext(NULL, &SubTP, &blogposts[j], CTX_BLOGPOST, 0, NULL);
378                                 DoTemplate(HKEY("view_blog_post_start"), WCC->WBuf, &SubTP);
379                                 UnStackContext(&SubTP);
380                         }
381                         if (i < (start_here + maxp)) {
382                                 blogpost_render(blogposts[i], with_comments);
383                         }
384                         else if (i == (start_here + maxp)) {
385                                 StackContext(NULL, &SubTP, &blogposts[i], CTX_BLOGPOST, 0, NULL);
386                                 DoTemplate(HKEY("view_blog_post_stop"), WCC->WBuf, &SubTP);
387                                 UnStackContext(&SubTP);
388                         }
389                 }
390
391                 /* Done.  We are only freeing the array of pointers; the data itself
392                  * will be freed along with the hash list.
393                  */
394                 free(blogposts);
395         }
396
397         return(0);
398 }
399
400
401 int blogview_Cleanup(void **ViewSpecific)
402 {
403         BLOG *BL = (BLOG*) *ViewSpecific;
404
405         FreeStrBuf(&BL->Buf);
406         FreeStrBuf(&BL->Charset);
407         DeleteHash(&BL->BLOGPOSTS);
408         free(BL);
409         wDumpContent(1);
410         return 0;
411 }
412
413
414 void 
415 InitModule_BLOGVIEWRENDERERS
416 (void)
417 {
418         RegisterCTX(CTX_BLOGPOST);
419
420         RegisterReadLoopHandlerset(
421                 VIEW_BLOG,
422                 blogview_GetParamsGetServerCall,
423                 NULL,
424                 NULL,
425                 NULL, 
426                 blogview_LoadMsgFromServer,
427                 blogview_render,
428                 blogview_Cleanup
429         );
430
431         RegisterNamespace("BLOG:TOPLEVEL:MSGID", 0, 0, tmplput_blog_toplevel_id, NULL, CTX_BLOGPOST);
432         RegisterNamespace("BLOG:COMMENTS:COUNT", 0, 0, tmplput_blog_comment_count, NULL, CTX_BLOGPOST);
433         RegisterNamespace("BLOG:COMMENTS:UNREAD:COUNT", 0, 0, tmplput_blog_comment_unread_count, NULL, CTX_BLOGPOST);
434 }