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