9432181e75d93746d53364cd486e1de0a692c27d
[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
20 typedef struct __BLOG {
21         HashList *BLOG;
22         long p;
23         int gotonext;
24         StrBuf *Charset;
25         StrBuf *Buf;
26 } BLOG;
27
28
29 /*
30  * Generate a permalink for a post
31  * (Call with NULL arguments to make this function wcprintf() the permalink
32  * instead of writing it to the template)
33  */
34 void tmplput_blog_permalink(StrBuf *Target, WCTemplputParams *TP) {
35         StrBuf *buf = NewStrBufPlain(HKEY("/readfwd?go="));
36         StrBufUrlescAppend(buf, WC->CurRoom.name, 0);
37         StrBufAppendPrintf(buf, "?p=%d", WC->bptlid);
38         StrBufAppendTemplate(Target, TP, buf, 0);
39         FreeStrBuf(&buf);
40 }
41
42
43 /*
44  * Render a single blog post and (optionally) its comments
45  */
46 void blogpost_render(struct blogpost *bp, int with_comments)
47 {
48         const StrBuf *Mime;
49         int i;
50
51         WC->bptlid = bp->top_level_id;  /* This is used in templates; do not remove it */
52
53         /* Always show the top level post, unless we somehow ended up with an empty list */
54         if (bp->num_msgs > 0) {
55                 read_message(WC->WBuf, HKEY("view_blog_post"), bp->msgs[0], NULL, &Mime);
56         }
57
58         if (with_comments) {
59                 /* Show any existing comments, then offer the comment box */
60                 wc_printf("<a class=\"blog_show_comments_link\" name=\"comments\"></a>\n");
61                 wc_printf(_("%d comments"), bp->num_msgs - 1);
62                 wc_printf(" | <a class=\"blog_permalink_link\" href=\"");
63                 tmplput_blog_permalink(NULL, NULL);
64                 wc_printf("\">%s</a>", _("permalink"));
65                 wc_printf("</div>\n");
66                 for (i=1; i<bp->num_msgs; ++i) {
67                         read_message(WC->WBuf, HKEY("view_blog_comment"), bp->msgs[i], NULL, &Mime);
68                 }
69                 do_template("view_blog_comment_box");
70         }
71
72         else {
73                 /* Show only the number of comments */
74                 wc_printf("<a class=\"blog_show_comments_link\" href=\"readfwd?p=%d?go=", bp->top_level_id);
75                 urlescputs(ChrPtr(WC->CurRoom.name));
76                 wc_printf("#comments\">");
77                 wc_printf(_("%d comments"), bp->num_msgs - 1);
78                 wc_printf(" %d %s", bp->unread_oments, _("new"));
79                 wc_printf("</a> | <a class=\"blog_permalink_link\" href=\"");
80                 tmplput_blog_permalink(NULL, NULL);
81                 wc_printf("\">%s</a>", _("permalink"));
82                 wc_printf("<hr>\n</div>\n");
83         }
84 }
85
86
87 /*
88  * Destructor for "struct blogpost"
89  */
90 void blogpost_destroy(struct blogpost *bp) {
91         if (bp->alloc_msgs > 0) {
92                 free(bp->msgs);
93         }
94         free(bp);
95 }
96
97
98 /*
99  * Entry point for message read operations.
100  */
101 int blogview_GetParamsGetServerCall(SharedMessageStatus *Stat, 
102                                    void **ViewSpecific, 
103                                    long oper, 
104                                    char *cmd, 
105                                     long len,
106                                     char *filter,
107                                     long flen)
108 {
109         BLOG *BL = (BLOG*) malloc(sizeof(BLOG)); 
110         BL->BLOG = NewHash(1, lFlathash);
111         
112         /* are we looking for a specific post? */
113         BL->p = lbstr("p");
114         BL->gotonext = havebstr("gotonext");
115         BL->Charset = NewStrBuf();
116         BL->Buf = NewStrBuf();
117         *ViewSpecific = BL;
118
119         Stat->startmsg = (-1);                                  /* not used here */
120         Stat->sortit = 1;                                       /* not used here */
121         Stat->num_displayed = DEFAULT_MAXMSGS;                  /* not used here */
122         if (Stat->maxmsgs == 0) Stat->maxmsgs = DEFAULT_MAXMSGS;
123         
124         /* perform a "read all" call to fetch the message list -- we'll cut it down later */
125         rlid[2].cmd(cmd, len);
126         if (BL->gotonext)
127                 Stat->load_seen = 1;
128         return 200;
129 }
130
131
132 /*
133  * This function is called for every message in the list.
134  */
135 int blogview_LoadMsgFromServer(SharedMessageStatus *Stat, 
136                               void **ViewSpecific, 
137                               message_summary* Msg, 
138                               int is_new, 
139                               int i)
140 {
141         BLOG *BL = (BLOG*) *ViewSpecific;
142         struct blogpost *bp = NULL;
143
144         ReadOneMessageSummary(Msg, BL->Charset, BL->Buf);
145
146         /* Stop processing if the viewer is only interested in a single post and
147          * that message ID is neither the id nor the refs.
148          */
149         if ((BL->p != 0) &&
150             (BL->p != Msg->reply_inreplyto_hash) &&
151             (BL->p != Msg->reply_references_hash)) {
152                 return 200;
153         }
154
155         /*
156          * Add our little bundle of blogworthy wonderfulness to the hash table
157          */
158         if (Msg->reply_references_hash == 0) {
159                 bp = malloc(sizeof(struct blogpost));
160                 if (!bp) return(200);
161                 memset(bp, 0, sizeof (struct blogpost));
162                 bp->top_level_id = Msg->reply_inreplyto_hash;
163                 Put(BL->BLOG,
164                     (const char *)&Msg->reply_inreplyto_hash,
165                     sizeof(Msg->reply_inreplyto_hash),
166                     bp,
167                     (DeleteHashDataFunc)blogpost_destroy);
168         }
169         else {
170                 GetHash(BL->BLOG,
171                         (const char *)&Msg->reply_references_hash,
172                         sizeof(Msg->reply_references_hash),
173                         (void *)&bp);
174         }
175
176         /*
177          * Now we have a 'struct blogpost' to which we can add a message.  It's either the
178          * blog post itself or a comment attached to it; either way, the code is the same from
179          * this point onward.
180          */
181         if (bp != NULL) {
182                 if (bp->alloc_msgs == 0) {
183                         bp->alloc_msgs = 1000;
184                         bp->msgs = malloc(bp->alloc_msgs * sizeof(long));
185                         memset(bp->msgs, 0, (bp->alloc_msgs * sizeof(long)) );
186                 }
187                 if (bp->num_msgs >= bp->alloc_msgs) {
188                         bp->alloc_msgs *= 2;
189                         bp->msgs = realloc(bp->msgs, (bp->alloc_msgs * sizeof(long)));
190                         memset(&bp->msgs[bp->num_msgs], 0,
191                                 ((bp->alloc_msgs - bp->num_msgs) * sizeof(long)) );
192                 }
193                 bp->msgs[bp->num_msgs++] = Msg->msgnum;
194                 if ((Msg->Flags & MSGFLAG_READ) != 0) {
195                         bp->unread_oments++;
196                 }
197         }
198         else {
199                 syslog(LOG_DEBUG, "** comment %ld is unparented", Msg->msgnum);
200         }
201
202         return 200;
203 }
204
205
206 /*
207  * Sort a list of 'struct blogpost' pointers by newest-to-oldest msgnum.
208  * With big thanks to whoever wrote http://www.c.happycodings.com/Sorting_Searching/code14.html
209  */
210 static int blogview_sortfunc(const void *a, const void *b) { 
211         struct blogpost * const *one = a;
212         struct blogpost * const *two = b;
213
214         if ( (*one)->msgs[0] > (*two)->msgs[0] ) return(-1);
215         if ( (*one)->msgs[0] < (*two)->msgs[0] ) return(+1);
216         return(0);
217 }
218
219
220 /*
221  * All blogpost entries are now in the hash list.
222  * Sort them, select the desired range, and render what we want to see.
223  */
224 int blogview_render(SharedMessageStatus *Stat, void **ViewSpecific, long oper)
225 {
226         BLOG *BL = (BLOG*) *ViewSpecific;
227         HashPos *it;
228         const char *Key;
229         void *Data;
230         long len;
231         int i;
232         struct blogpost **blogposts = NULL;
233         int num_blogposts = 0;
234         int num_blogposts_alloc = 0;
235         int with_comments = 0;
236         int firstp = 0;
237         int maxp = 0;
238
239         /* Comments are shown if we are only viewing a single blog post */
240         with_comments = (BL->p != 0);
241
242         firstp = ibstr("firstp");   /* start reading at... */
243         maxp   = ibstr("maxp");     /* max posts to show... */
244         if (maxp < 1) maxp = 5;     /* default; move somewhere else? */
245
246         it = GetNewHashPos(BL->BLOG, 0);
247
248         if ((BL->gotonext) && (BL->p == 0)) {
249                 /* did we come here via gotonext? lets find out whether
250                  * this blog has just one blogpost with new comments just display 
251                  * this one.
252                  */
253                 struct blogpost *unread_bp = NULL;
254                 int unread_count = 0;
255                 while (GetNextHashPos(BL->BLOG, it, &len, &Key, &Data)) {
256                         struct blogpost *one_bp = (struct blogpost *) Data;
257                         if (one_bp->unread_oments > 0) {
258                                 unread_bp = one_bp;
259                                 unread_count++;
260                         }
261                 }
262                 if (unread_count == 1) {
263                         blogpost_render(unread_bp, 1);
264
265                         DeleteHashPos(&it);
266                         return 0;
267                 }
268
269                 RewindHashPos(BL->BLOG, it, 0);
270         }
271
272         /* Iterate through the hash list and copy the data pointers into an array */
273         while (GetNextHashPos(BL->BLOG, it, &len, &Key, &Data)) {
274                 if (num_blogposts >= num_blogposts_alloc) {
275                         if (num_blogposts_alloc == 0) {
276                                 num_blogposts_alloc = 100;
277                         }
278                         else {
279                                 num_blogposts_alloc *= 2;
280                         }
281                         blogposts = realloc(blogposts, (num_blogposts_alloc * sizeof (struct blogpost *)));
282                 }
283                 blogposts[num_blogposts++] = (struct blogpost *) Data;
284         }
285         DeleteHashPos(&it);
286
287         /* Now we have our array.  It is ONLY an array of pointers.  The objects to
288          * which they point are still owned by the hash list.
289          */
290         if (num_blogposts > 0) {
291                 int start_here = 0;
292                 /* Sort newest-to-oldest */
293                 qsort(blogposts, num_blogposts, sizeof(void *), blogview_sortfunc);
294
295                 /* allow the user to select a starting point in the list */
296                 for (i=0; i<num_blogposts; ++i) {
297                         if (blogposts[i]->top_level_id == firstp) {
298                                 start_here = i;
299                         }
300                 }
301
302                 /* FIXME -- allow the user (or a default setting) to select a maximum number of posts to display */
303
304                 /* Now go through the list and render what we've got */
305                 for (i=start_here; i<num_blogposts; ++i) {
306                         if ((i > 0) && (i == start_here)) {
307                                 int j = i - maxp;
308                                 if (j < 0) j = 0;
309                                 wc_printf("<div class=\"newer_blog_posts\"><a href=\"readfwd?go=");
310                                 urlescputs(ChrPtr(WC->CurRoom.name));
311                                 wc_printf("?firstp=%d?maxp=%d\">", blogposts[j]->top_level_id, maxp);
312                                 wc_printf("%s →</a></div>\n", _("Newer posts"));
313                         }
314                         if (i < (start_here + maxp)) {
315                                 blogpost_render(blogposts[i], with_comments);
316                         }
317                         else if (i == (start_here + maxp)) {
318                                 wc_printf("<div class=\"older_blog_posts\"><a href=\"readfwd?go=");
319                                 urlescputs(ChrPtr(WC->CurRoom.name));
320                                 wc_printf("?firstp=%d?maxp=%d\">", blogposts[i]->top_level_id, maxp);
321                                 wc_printf("← %s</a></div>\n", _("Older posts"));
322                         }
323                 }
324
325                 /* Done.  We are only freeing the array of pointers; the data itself
326                  * will be freed along with the hash list.
327                  */
328                 free(blogposts);
329         }
330
331         return(0);
332 }
333
334
335 int blogview_Cleanup(void **ViewSpecific)
336 {
337         BLOG *BL = (BLOG*) *ViewSpecific;
338
339         FreeStrBuf(&BL->Buf);
340         FreeStrBuf(&BL->Charset);
341         DeleteHash(&BL->BLOG);
342         free(BL);
343         wDumpContent(1);
344         return 0;
345 }
346
347
348 void 
349 InitModule_BLOGVIEWRENDERERS
350 (void)
351 {
352         RegisterReadLoopHandlerset(
353                 VIEW_BLOG,
354                 blogview_GetParamsGetServerCall,
355                 NULL,
356                 NULL,
357                 NULL, 
358                 blogview_LoadMsgFromServer,
359                 blogview_render,
360                 blogview_Cleanup
361         );
362         RegisterNamespace("BLOG:PERMALINK", 0, 1, tmplput_blog_permalink, NULL, CTX_NONE);
363 }