Add to git.
[monolith.git] / discussion / ml_discussion.c
1 /* Monolith discussion widget.
2  * - by Richard W.M. Jones <rich@annexia.org>
3  *
4  * This library is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Library General Public
6  * License as published by the Free Software Foundation; either
7  * version 2 of the License, or (at your option) any later version.
8  *
9  * This library 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 GNU
12  * Library General Public License for more details.
13  *
14  * You should have received a copy of the GNU Library General Public
15  * License along with this library; if not, write to the Free
16  * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17  *
18  * $Id: ml_discussion.c,v 1.13 2003/02/22 15:34:29 rich Exp $
19  */
20
21 #include "config.h"
22
23 #include <stdio.h>
24 #include <stdlib.h>
25
26 #ifdef HAVE_ASSERT_H
27 #include <assert.h>
28 #endif
29
30 #ifdef HAVE_STRING_H
31 #include <string.h>
32 #endif
33
34 #include <pool.h>
35 #include <tree.h>
36 #include <hash.h>
37 #include <pstring.h>
38
39 #include <pthr_dbi.h>
40 #include <pthr_iolib.h>
41
42 #include <monolith.h>
43 #include <ml_window.h>
44 #include <ml_widget.h>
45 #include <ml_smarttext.h>
46 #include <ml_button.h>
47 #include <ml_form_layout.h>
48 #include <ml_text_label.h>
49 #include <ml_form.h>
50 #include <ml_form_input.h>
51 #include <ml_form_textarea.h>
52 #include <ml_form_select.h>
53 #include <ml_form_text.h>
54 #include <ml_form_submit.h>
55 #include <ml_dialog.h>
56
57 #include "ml_discussion.h"
58
59 #define MD_DEBUG 0
60
61 #define ML_DISCUSSION_POPUP_WIN "ml_discussion_popup"
62
63 static void repaint (void *, ml_session, const char *, io_handle);
64
65 struct ml_widget_operations discussion_ops =
66   {
67     repaint: repaint
68   };
69
70 struct ml_discussion
71 {
72   struct ml_widget_operations *ops;
73   pool pool;                    /* Pool for allocations. */
74   ml_session session;           /* Current session. */
75   const char *conninfo;         /* Database connection. */
76   int resid;                    /* Resource ID. */
77   const char *name;             /* Newsgroup (resource) name. */
78   int allow_anon;               /* Allow anonymous postings. */
79   char default_view;            /* Default view (either 1-pane or 2-pane). */
80   int first_item;               /* First item to display. */
81   int nr_items;                 /* Number of items to display on each page. */
82
83   /* In article display mode, these are the buttons that appear across the
84    * top of the widget.
85    */
86   ml_button prev, next;         /* Previous/next buttons. */
87   ml_button post;               /* Post button. */
88   ml_button mark_all_read, mark_all_unread; /* Mark all as read / unread. */
89
90   /* To avoid creating the same buttons over and over again during the
91    * repaint, cache buttons here. This is a hash of struct button_entry
92    * -> ml_button.
93    */
94   hash button_cache;
95
96   /* These are used during posting. */
97   ml_form_text subject;         /* Subject line. */
98   ml_form_textarea body;        /* Body of the posting. */
99   ml_form_select body_type;     /* Type (plain, smart, HTML). */
100 };
101
102 struct button_entry
103 {
104   int name;
105 #define BUTTON_NAME_REPLY_IN_PUBLIC   1
106 #define BUTTON_NAME_REPLY_IN_PRIVATE  2
107 #define BUTTON_NAME_SAVE              3
108 #define BUTTON_NAME_CANCEL            4
109 #define BUTTON_NAME_SUPERSEDE         5
110   int artid;
111 };
112
113 static inline void
114 expire_old_articles (pool pool, db_handle dbh, int resid, int expiry_days)
115 {
116   st_handle sth;
117   int rows, min_id;
118
119   sth = st_prepare_cached
120     (dbh,
121      "delete from ml_discussion_article "
122      "where resid = ? "
123      "and current_timestamp - posted_date >= interval ?",
124      DBI_INT, DBI_STRING);
125   rows = st_execute (sth,
126                      resid,
127                      psprintf (pool, "%d days", expiry_days));
128
129   if (rows > 0)         /* Any rows actually deleted? */
130     {
131       sth = st_prepare_cached
132         (dbh,
133          "select coalesce (min (id), 0) from ml_discussion_article "
134          "where resid = ?",
135          DBI_INT);
136       st_execute (sth, resid);
137
138       st_bind (sth, 0, min_id, DBI_INT);
139
140       sth = st_prepare_cached
141         (dbh,
142          "delete from ml_discussion_read "
143          "where resid = ? "
144          "and high <= ?",
145          DBI_INT, DBI_INT);
146       st_execute (sth, resid, min_id);
147     }
148 }
149
150 static void post_form (ml_session session, void *vw);
151 static void mark_all_read (ml_session, void *vw);
152 static void mark_all_unread (ml_session, void *vw);
153
154 ml_discussion
155 new_ml_discussion (pool pool, ml_session session, const char *conninfo, const char *res_name)
156 {
157   ml_discussion w = pmalloc (pool, sizeof *w);
158   db_handle dbh;
159   st_handle sth;
160   char *expiry_days_str;
161
162 #if MD_DEBUG
163   fprintf (stderr, "new_ml_discussion: creating new widget for %s\n",
164            res_name);
165 #endif
166
167   w->ops = &discussion_ops;
168   w->pool = pool;
169   w->session = session;
170   w->conninfo = conninfo;
171   w->name = res_name;
172
173   /* Get the resource ID and a few other details. */
174   dbh = get_db_handle (conninfo, DBI_THROW_ERRORS);
175
176   sth = st_prepare_cached
177     (dbh,
178      "select r.resid, g.allow_anon, g.default_view, g.expiry_days "
179      "from ml_resources r, ml_discussion_group g "
180      "where r.name = ? "
181      "and r.resid = g.resid",
182      DBI_STRING);
183   st_execute (sth, res_name);
184
185   st_bind (sth, 0, w->resid, DBI_INT);
186   st_bind (sth, 1, w->allow_anon, DBI_BOOL);
187   st_bind (sth, 2, w->default_view, DBI_CHAR);
188   st_bind (sth, 3, expiry_days_str, DBI_STRING);
189
190   if (!st_fetch (sth)) return 0; /* Not found. */
191
192   /* Rather than running cron jobs, just expire old articles here
193    * if necessary.
194    */
195   if (expiry_days_str)
196     {
197       int expiry_days;
198
199       if (sscanf (expiry_days_str, "%d", &expiry_days) == 1 &&
200           expiry_days > 0)
201         expire_old_articles (pool, dbh, w->resid, expiry_days);
202     }
203
204   /* Be polite, put back the database handle. */
205   db_commit (dbh);
206   put_db_handle (dbh);
207
208   w->first_item = 0;
209   w->nr_items = 20;
210
211   /* Create the buttons. Previous and next buttons start disabled, but
212    * are enabled in the repaint function, if appropriate.
213    */
214   w->prev = new_ml_button (pool, "&lt;&lt;");
215   w->next = new_ml_button (pool, "&gt;&gt;");
216
217   w->post = new_ml_button (pool, "New message");
218   ml_button_set_popup (w->post, ML_DISCUSSION_POPUP_WIN);
219   ml_button_set_popup_size (w->post, 640, 480);
220   ml_button_set_callback (w->post, post_form, session, w);
221
222   w->mark_all_read = new_ml_button (pool, "Mark all read");
223   ml_button_set_callback (w->mark_all_read, mark_all_read, session, w);
224
225   w->mark_all_unread = new_ml_button (pool, "Mark all unread");
226   ml_button_set_callback (w->mark_all_unread, mark_all_unread, session, w);
227
228   /* Cache of buttons. */
229   w->button_cache = new_hash (pool, struct button_entry, ml_button);
230
231   return w;
232 }
233
234 static ml_button get_button (ml_discussion w, ml_session session,
235                              int name, int artid);
236 static void reply_in_public_form (ml_session session, void *vargs);
237 static void reply_in_private_form (ml_session session, void *vargs);
238 static void save_form (ml_session session, void *vargs);
239 static void cancel_form (ml_session session, void *vargs);
240 static void supersede_form (ml_session session, void *vargs);
241 static void prev_button (ml_session session, void *vw);
242 static void next_button (ml_session session, void *vw);
243 static void mark_read (ml_session, db_handle dbh, int resid, int userid, int artid);
244
245 /* when we have flattened the representation of the tree into a vector,
246  * each vector element will have the following type.
247  */
248 struct d_art
249 {
250   int depth;                    /* 0 = top-level. */
251   int artid;                    /* Article ID. */
252 };
253
254 static inline void
255 repaint_1pane (ml_discussion w, ml_session session,
256                const char *windowid, io_handle io,
257                db_handle dbh, int userid, vector artlist, vector artids)
258 {
259   st_handle sth;
260   hash articles;
261   struct article {
262     const char *subject, *username, *body, *posted_date;
263     char body_type;
264     int userid;
265     int read;
266   };
267   int i;
268
269   assert (vector_size (artids) == vector_size (artlist));
270
271   /* Maps article ID -> article contents. */
272   articles = new_hash (w->pool, int, struct article);
273
274   /* In 1-pane mode we are going to display the full article contents. */
275   if (vector_size (artids) > 0)
276     {
277       int artid;
278       struct article art;
279
280       art.read = 0;
281
282       sth = st_prepare_cached
283         (dbh,
284          "select a.id, a.subject, u.userid, u.username, a.body, a.body_type, "
285          "a.posted_date "
286          "from ml_discussion_article a "
287          "left outer join ml_users u on a.author = u.userid "
288          "where a.resid = ? and a.id in (@)",
289          DBI_INT, DBI_VECTOR_INT);
290       st_execute (sth, w->resid, artids);
291
292       st_bind (sth, 0, artid, DBI_INT);
293       st_bind (sth, 1, art.subject, DBI_STRING);
294       st_bind (sth, 2, art.userid, DBI_INT);
295       st_bind (sth, 3, art.username, DBI_STRING);
296       st_bind (sth, 4, art.body, DBI_STRING);
297       st_bind (sth, 5, art.body_type, DBI_CHAR);
298       st_bind (sth, 6, art.posted_date, DBI_STRING);
299
300       while (st_fetch (sth))
301         hash_insert (articles, artid, art);
302     }
303
304   /* Pull out the read/unread status of each article (but not for
305    * anonymous users - they see all articles as unread).
306    */
307   if (userid)
308     {
309       int low, high;            /* Each range is [low, high-1]. */
310
311       sth = st_prepare_cached
312         (dbh,
313          "select low, high from ml_discussion_read "
314          "where resid = ? and userid = ?",
315          DBI_INT, DBI_INT);
316       st_execute (sth, w->resid, userid);
317
318       st_bind (sth, 0, low, DBI_INT);
319       st_bind (sth, 1, high, DBI_INT);
320
321       while (st_fetch (sth))
322         {
323           struct article *artp;
324           int artid;
325
326           /* Mark each article in the range [low, high-1] as read. */
327           for (artid = low; artid < high; ++artid)
328             {
329               if (hash_get_ptr (articles, artid, artp))
330                 artp->read = 1;
331             }
332         }
333     }
334
335   /* Display the articles. */
336   for (i = 0; i < vector_size (artlist); ++i)
337     {
338       int artid;
339       struct d_art d_art;
340       struct article art;
341       ml_button b;
342       const char *h_class;
343
344       vector_get (artlist, i, d_art);
345       artid = d_art.artid;
346       assert (artid > 0);
347       if (!hash_get (articles, artid, art)) abort ();
348
349       io_fprintf (io,
350                   "<table class=\"ml_discussion_posting\" "
351                   "style=\"margin-left: %dem\">\n",
352                   d_art.depth * 2);
353
354       /* Unread postings appear in bold. */
355       h_class = art.read ? "read" : "unread";
356
357       /* The article subject, username, date. */
358       io_fprintf (io, "<tr><th class=\"ml_discussion_posting_%s\">",
359                   h_class);
360       ml_plaintext_print (io, art.subject);
361       io_fputs ("<br>\nby ", io);
362       ml_plaintext_print (io, (art.userid ? art.username : "anonymous"));
363       /* XXX Date formatting. */
364       io_fprintf (io, " on %s</th></tr>\n", art.posted_date);
365
366       /* The article body. */
367       io_fputs ("<tr><td>", io);
368       ml_anytext_print (io, art.body, art.body_type);
369       io_fputs ("</td></tr>", io);
370
371       /* Buttons. */
372       io_fprintf (io, "<tr><td>");
373       if (w->allow_anon || userid)
374         {
375           b = get_button (w, session, BUTTON_NAME_REPLY_IN_PUBLIC, artid);
376           ml_widget_repaint (b, session, windowid, io);
377           io_fputs ("&nbsp;", io);
378           if (userid && art.userid)
379             {
380               b = get_button (w, session, BUTTON_NAME_REPLY_IN_PRIVATE, artid);
381               ml_widget_repaint (b, session, windowid, io);
382               io_fputs ("&nbsp;", io);
383             }
384         }
385       b = get_button (w, session, BUTTON_NAME_SAVE, artid);
386       ml_widget_repaint (b, session, windowid, io);
387       io_fputs ("&nbsp;", io);
388       if (userid && userid == art.userid)
389         {
390           b = get_button (w, session, BUTTON_NAME_SUPERSEDE, artid);
391           ml_widget_repaint (b, session, windowid, io);
392           io_fputs ("&nbsp;", io);
393         }
394       if (0)                    /* XXX Administrator. */
395         {
396           b = get_button (w, session, BUTTON_NAME_CANCEL, artid);
397           ml_widget_repaint (b, session, windowid, io);
398           io_fputs ("&nbsp;", io);
399         }
400       io_fprintf (io, "</td></tr>\n");
401       io_fprintf (io, "</table>");
402
403       /* Mark article as read. */
404       if (art.read == 0) mark_read (session, dbh, w->resid, userid, artid);
405     }
406 }
407
408 static inline void
409 repaint_2pane (ml_discussion w, ml_session session,
410                const char *windowid, io_handle io,
411                db_handle dbh, int userid, vector artlist, vector artids)
412 {
413   abort (); /* XXX */
414 }
415
416 static inline vector
417 flatten_tree (pool pool, tree node, int depth)
418 {
419   int i;
420   vector v = new_vector (pool, struct d_art);
421   struct d_art d_art;
422
423   if (depth >= 0)
424     {
425       d_art.depth = depth;
426       tree_get_data (node, d_art.artid);
427       vector_push_back (v, d_art);
428     }
429
430   for (i = 0; i < tree_nr_subnodes (node); ++i)
431     {
432       tree t;
433
434       tree_get_subnode (node, i, t);
435       vector_push_back_vector (v, flatten_tree (pool, t, depth+1));
436     }
437
438   return v;
439 }
440
441 static inline void
442 repaint_display (ml_discussion w, ml_session session,
443                  const char *windowid, io_handle io)
444 {
445   db_handle dbh;
446   st_handle sth;
447   int userid, i;
448   char view = w->default_view;
449   char sort_order = 'd';
450   const char *sort_col, *order, *sql;
451   int artid, parentid;
452   hash articles;
453   tree top_node;
454   const int zero = 0;
455   int more_articles;
456   vector artlist, artids;
457
458 #if MD_DEBUG
459   fprintf (stderr, "ml_discussion.c: repaint_display called\n");
460 #endif
461
462   /* Get a database handle. */
463   dbh = get_db_handle (w->conninfo, DBI_THROW_ERRORS);
464 #if MD_DEBUG
465   db_set_debug (dbh, 1);
466 #endif
467
468   userid = ml_session_userid (session);
469
470   if (userid)
471     {
472       /* Pull out the user preferences, so we know if we are in 1-pane
473        * or 2-pane mode.
474        */
475       sth = st_prepare_cached
476         (dbh,
477          "select view, sort_order "
478          "from ml_discussion_userprefs "
479          "where userid = ?",
480          DBI_INT);
481       st_execute (sth, userid);
482       st_bind (sth, 0, view, DBI_CHAR);
483       st_bind (sth, 1, sort_order, DBI_CHAR);
484       st_fetch (sth);
485     }
486
487   /* Pull out the article IDs and thread them by hand. Relational databases
488    * really don't handle trees at all.
489    */
490   switch (sort_order) {
491   case 'd': case 'n': case 's': order = "desc"; break;
492   default:                      order = "asc";
493   }
494   switch (sort_order) {
495   case 'd': case 'D': sort_col = "a.posted_date"; break;
496   case 'n': case 'N': sort_col = "u.username"; break;
497   default:            sort_col = "a.subject";
498   }
499   sql = psprintf (w->pool,
500                   "select a.id, coalesce (a.parent, 0), %s "
501                   "from ml_discussion_article a "
502                   "left outer join ml_users u on a.author = u.userid "
503                   "where a.resid = ? "
504                   "order by 3 %s",
505                   sort_col, order);
506   sth = st_prepare_cached (dbh, sql, DBI_INT);
507   st_execute (sth, w->resid);
508
509   st_bind (sth, 0, artid, DBI_INT);
510   st_bind (sth, 1, parentid, DBI_INT);
511
512   /* This hash maps article IDs -> tree nodes. */
513   articles = new_hash (w->pool, int, tree);
514
515   /* The top_node is empty. Children of this node are the lead
516    * articles in each thread.
517    */
518   top_node = new_tree (w->pool, int);
519   tree_set_data (top_node, zero);
520
521   hash_insert (articles, zero, top_node);
522
523   while (st_fetch (sth))
524     {
525       tree parent_node;
526       tree article_node;
527
528       /* Have we seen this parent ID node before? */
529       if (! hash_get (articles, parentid, parent_node))
530         {
531           parent_node = new_tree (w->pool, int);
532           tree_set_data (parent_node, parentid);
533           hash_insert (articles, parentid, parent_node);
534         }
535
536       /* Have we seen this article ID node before? */
537       if (! hash_get (articles, artid, article_node))
538         {
539           article_node = new_tree (w->pool, int);
540           tree_set_data (article_node, artid);
541           hash_insert (articles, artid, article_node);
542         }
543
544       /* Set the relationship between the parent node and the article node. */
545       tree_push_back (parent_node, article_node);
546     }
547
548   /* Now flatten the tree into a list, preserving depth information. */
549   artlist = flatten_tree (w->pool, top_node, -1);
550
551   /* Extract just the nodes which are going to be displayed. */
552   more_articles = vector_size (artlist) - (w->first_item + w->nr_items) > 0;
553   artlist =
554     new_subvector (w->pool, artlist, w->first_item,
555                    (more_articles ?
556                     w->first_item + w->nr_items :
557                     vector_size (artlist)));
558
559   /* Enable the previous/next buttons (if necessary). */
560   if (w->first_item > 0)
561     ml_button_set_callback (w->prev, prev_button, session, w);
562   else
563     ml_button_set_callback (w->prev, 0, session, 0);
564
565   if (more_articles)
566     ml_button_set_callback (w->next, next_button, session, w);
567   else
568     ml_button_set_callback (w->next, 0, session, 0);
569
570   /* Get the IDs of the articles we are actually going to display. */
571   artids = new_vector (w->pool, int);
572   for (i = 0; i < vector_size (artlist); ++i)
573     {
574       struct d_art d_art;
575
576       vector_get (artlist, i, d_art);
577       vector_push_back (artids, d_art.artid);
578     }
579
580   /* Print the standard buttons along the top of the widget. */
581   io_fprintf (io,
582               "<table class=\"ml_discussion\">"
583               "<tr><td align=\"left\">");
584   ml_widget_repaint (w->prev, session, windowid, io);
585   ml_widget_repaint (w->next, session, windowid, io);
586   io_fprintf (io, "</td><td align=\"right\">");
587   ml_widget_repaint (w->post, session, windowid, io);
588   ml_widget_repaint (w->mark_all_read, session, windowid, io);
589   ml_widget_repaint (w->mark_all_unread, session, windowid, io);
590   io_fprintf (io, "</td></tr>\n<tr><td colspan=\"2\">");
591
592   if (view == '1')              /* 1-pane mode. */
593     repaint_1pane (w, session, windowid, io, dbh, userid, artlist, artids);
594   else                          /* 2-pane mode. */
595     repaint_2pane (w, session, windowid, io, dbh, userid, artlist, artids);
596
597   /* Finish off the widget. */
598   io_fprintf (io, "</td></tr></table>");
599
600   /* Commit changes (in article read/unread state) back to the database. */
601   db_commit (dbh);
602
603   /* Be polite: give back the database handle. */
604 #if MD_DEBUG
605   db_set_debug (dbh, 0);
606 #endif
607   put_db_handle (dbh);
608 }
609
610 static void
611 repaint (void *vw, ml_session session, const char *windowid, io_handle io)
612 {
613   ml_discussion w = (ml_discussion) vw;
614
615 #if MD_DEBUG
616   fprintf (stderr, "ml_discussion.c: repaint called\n");
617 #endif
618
619   repaint_display (w, session, windowid, io);
620 }
621
622 #define MR_DEBUG 0
623
624 static void mark_read_error (ml_session session, int resid, int userid, char type);
625
626 /* Mark the single article ID as read.
627  * Caution: This only works if the article is not already marked as read.
628  */
629 static void
630 mark_read (ml_session session, db_handle dbh, int resid, int userid, int artid)
631 {
632   /* The method used is as follows:
633    *
634    * From the ml_discussion_article table, find the largest article number
635    * which is < artid, and the smallest article number which is > artid.
636    * Call these 'prev_artid' and 'next_artid' respectively (either may
637    * be 0, indicating no such row).
638    *
639    * Now we have:
640    *
641    * prev_artid < artid < next_artid
642    *
643    * From the ml_discussion_read table, looking only at rows which correspond
644    * to the current (resid, userid), find any rows where:
645    *
646    * prev_artid < high and high <= artid,   [type A]
647    *
648    * and rows where:
649    *
650    * artid < low and low <= next_artid.     [type B]
651    *
652    * If we found no rows, create a new row.
653    *
654    * If we found one row of type A, modify this row so high = artid + 1.
655    *
656    * If we found one row of type B, modify this row so low = artid.
657    *
658    * If we found one row of both types, merge those rows together.
659    *
660    * Anything else indicates an internal error.
661    */
662
663   st_handle sth;
664   int prev_artid, next_artid;
665   int a_fetched = 0, a_low = 0, a_high = 0,
666     b_fetched = 0, b_low = 0, b_high = 0;
667
668   if (!userid) return;          /* Ignore anonymous users. */
669
670 #if MR_DEBUG
671   fprintf (stderr, "mark_read called: artid = %d\n", artid);
672 #endif
673
674   sth = st_prepare_cached
675     (dbh,
676      "select max (id) from ml_discussion_article "
677      "where resid = ? and id < ?",
678      DBI_INT, DBI_INT);
679   st_execute (sth, resid, artid);
680
681   st_bind (sth, 0, prev_artid, DBI_INT);
682
683   st_fetch (sth);
684
685   sth = st_prepare_cached
686     (dbh,
687      "select min (id) from ml_discussion_article "
688      "where resid = ? and id > ?",
689      DBI_INT, DBI_INT);
690   st_execute (sth, resid, artid);
691
692   st_bind (sth, 0, next_artid, DBI_INT);
693
694   st_fetch (sth);
695
696 #if MR_DEBUG
697   fprintf (stderr, "\tprev_artid = %d, next_artid = %d\n", prev_artid, next_artid);
698 #endif
699
700   if (prev_artid)
701     {
702       sth = st_prepare_cached
703         (dbh,
704          "select 1, low, high from ml_discussion_read "
705          "where resid = ? and userid = ? and "
706          "? < high and high <= ?",
707          DBI_INT, DBI_INT, DBI_INT, DBI_INT);
708       st_execute (sth, resid, userid, prev_artid, artid);
709
710       st_bind (sth, 0, a_fetched, DBI_INT);
711       st_bind (sth, 1, a_low, DBI_INT);
712       st_bind (sth, 2, a_high, DBI_INT);
713
714       if (st_fetch (sth))
715         if (st_fetch (sth))
716           mark_read_error (session, resid, userid, 'A');
717     }
718
719   if (next_artid)
720     {
721       sth = st_prepare_cached
722         (dbh,
723          "select 1, low, high from ml_discussion_read "
724          "where resid = ? and userid = ? and "
725          "? < low and low <= ?",
726          DBI_INT, DBI_INT, DBI_INT, DBI_INT);
727       st_execute (sth, resid, userid, artid, next_artid);
728
729       st_bind (sth, 0, b_fetched, DBI_INT);
730       st_bind (sth, 1, b_low, DBI_INT);
731       st_bind (sth, 2, b_high, DBI_INT);
732
733       if (st_fetch (sth))
734         if (st_fetch (sth))
735           mark_read_error (session, resid, userid, 'B');
736     }
737
738 #if MR_DEBUG
739   fprintf (stderr, "\ta_fetched = %d (low = %d, high = %d)\n",
740            a_fetched, a_low, a_high);
741   fprintf (stderr, "\tb_fetched = %d (low = %d, high = %d)\n",
742            b_fetched, b_low, b_high);
743 #endif
744
745   /* No rows fetched: create a new row. */
746   if (!a_fetched && !b_fetched)
747     {
748 #if MR_DEBUG
749       fprintf (stderr, "\tinserting low = %d, high = %d\n", artid, artid+1);
750 #endif
751       sth = st_prepare_cached
752         (dbh,
753          "insert into ml_discussion_read (resid, userid, low, high) "
754          "values (?, ?, ?, ?)",
755          DBI_INT, DBI_INT, DBI_INT, DBI_INT);
756       st_execute (sth, resid, userid, artid, artid + 1);
757     }
758   /* One row of type A: modify this row so high = artid + 1. */
759   else if (a_fetched && !b_fetched)
760     {
761 #if MR_DEBUG
762       fprintf (stderr, "\tmodifying low = %d, high = %d so high = %d\n",
763                a_low, a_high, artid + 1);
764 #endif
765       sth = st_prepare_cached
766         (dbh,
767          "update ml_discussion_read set high = ? "
768          "where resid = ? and userid = ? and low = ? and high = ?",
769          DBI_INT, DBI_INT, DBI_INT, DBI_INT, DBI_INT);
770       st_execute (sth, artid + 1, resid, userid, a_low, a_high);
771     }
772   /* One row of type B: modify this row so low = artid. */
773   else if (!a_fetched && b_fetched)
774     {
775 #if MR_DEBUG
776       fprintf (stderr, "\tmodifying low = %d, high = %d so low = %d\n",
777                b_low, b_high, artid);
778 #endif
779       sth = st_prepare_cached
780         (dbh,
781          "update ml_discussion_read set low = ? "
782          "where resid = ? and userid = ? and low = ? and high = ?",
783          DBI_INT, DBI_INT, DBI_INT, DBI_INT, DBI_INT);
784       st_execute (sth, artid, resid, userid, b_low, b_high);
785     }
786   /* Two rows fetched: merge them. */
787   else
788     {
789 #if MR_DEBUG
790       fprintf (stderr, "\tmerging\n");
791 #endif
792       sth = st_prepare_cached
793         (dbh,
794          "delete from ml_discussion_read "
795          "where resid = ? and userid = ? and low = ? and high = ?",
796          DBI_INT, DBI_INT, DBI_INT, DBI_INT);
797       st_execute (sth, resid, userid, a_low, a_high);
798
799       sth = st_prepare_cached
800         (dbh,
801          "update ml_discussion_read set low = ? "
802          "where resid = ? and userid = ? and low = ? and high = ?",
803          DBI_INT, DBI_INT, DBI_INT, DBI_INT, DBI_INT);
804       st_execute (sth, a_low, resid, userid, b_low, b_high);
805     }
806 }
807
808 static void
809 mark_read_error (ml_session session, int resid, int userid, char type)
810 {
811   pool pool = ml_session_pool (session);
812   char *msg;
813
814   msg = psprintf
815     (pool,
816      "INTERNAL ERROR in discussion widget: error type '%c'\n"
817      "\n"
818      "Please perform the following queries on the database and send the\n"
819      "full results and error type back to your technical support contact:\n"
820      "\n"
821      "SELECT id FROM ml_discussion_article WHERE resid = %d ORDER BY 1\n"
822      "\n"
823      "SELECT low, high FROM ml_discussion_read WHERE resid = %d AND userid = %d ORDER BY 1\n",
824      type, resid, resid, userid);
825
826   pth_die (msg);
827 }
828
829 /* Mark all articles as read - this is very simple. */
830 static void
831 mark_all_read (ml_session session, void *vw)
832 {
833   ml_discussion w = (ml_discussion) vw;
834   int userid = ml_session_userid (session);
835   int resid = w->resid;
836   db_handle dbh;
837   st_handle sth;
838   int max_artid;
839
840   if (!userid) return;          /* Ignore for anonymous users. */
841
842   dbh = get_db_handle (w->conninfo, DBI_THROW_ERRORS);
843
844   sth = st_prepare_cached
845     (dbh,
846      "delete from ml_discussion_read where resid = ? and userid = ?",
847      DBI_INT, DBI_INT);
848   st_execute (sth, resid, userid);
849
850   /* Get the max. article ID. */
851   sth = st_prepare_cached
852     (dbh,
853      "select max(id) from ml_discussion_article where resid = ?",
854      DBI_INT);
855   st_execute (sth, resid);
856
857   st_bind (sth, 0, max_artid, DBI_INT);
858   st_fetch (sth);
859
860   if (max_artid)
861     {
862       sth = st_prepare_cached
863         (dbh,
864          "insert into ml_discussion_read (resid, userid, low, high) "
865          "values (?, ?, ?, ?)",
866          DBI_INT, DBI_INT, DBI_INT, DBI_INT);
867       st_execute (sth, resid, userid, 1, max_artid + 1);
868     }
869
870   db_commit (dbh);
871   put_db_handle (dbh);
872 }
873
874 /* Mark all articles as unread - this is very simple. */
875 static void
876 mark_all_unread (ml_session session, void *vw)
877 {
878   ml_discussion w = (ml_discussion) vw;
879   int userid = ml_session_userid (session);
880   int resid = w->resid;
881   db_handle dbh;
882   st_handle sth;
883
884   if (!userid) return;          /* Ignore for anonymous users. */
885
886   dbh = get_db_handle (w->conninfo, DBI_THROW_ERRORS);
887
888   sth = st_prepare_cached
889     (dbh,
890      "delete from ml_discussion_read where resid = ? and userid = ?",
891      DBI_INT, DBI_INT);
892   st_execute (sth, resid, userid);
893
894   db_commit (dbh);
895   put_db_handle (dbh);
896 }
897
898 struct button_args
899 {
900   int artid;
901   ml_discussion w;
902 };
903
904 static ml_button
905 get_button (ml_discussion w, ml_session session,
906             int name, int artid)
907 {
908   struct button_entry entry;
909   ml_button b;
910   const char *text;
911   void (*fn) (ml_session session, void *args);
912   int popup;
913   struct button_args *args;
914
915   entry.name = name;
916   entry.artid = artid;
917
918   if (hash_get (w->button_cache, entry, b))
919     return b;
920
921   /* Create the button. */
922   switch (name)
923     {
924     case BUTTON_NAME_REPLY_IN_PUBLIC:
925       text = "Reply in public";
926       fn = reply_in_public_form;
927       popup = 1;
928       break;
929     case BUTTON_NAME_REPLY_IN_PRIVATE:
930       text = "Reply in private";
931       fn = reply_in_private_form;
932       popup = 1;
933       break;
934     case BUTTON_NAME_SAVE:
935       text = "Save";
936       //fn = save_form;
937       fn = 0; (void)save_form;
938       popup = 0;
939       break;
940     case BUTTON_NAME_CANCEL:
941       text = "Delete";
942       //fn = cancel_form;
943       fn = 0; (void)cancel_form;
944       popup = 0;
945       break;
946     case BUTTON_NAME_SUPERSEDE:
947       text = "Replace";
948       fn = supersede_form;
949       popup = 1;
950       break;
951     default:
952       abort ();
953     }
954
955   b = new_ml_button (w->pool, text);
956   args = pmalloc (w->pool, sizeof *args);
957   args->artid = artid;
958   args->w = w;
959   ml_button_set_callback (b, fn, session, args);
960
961   ml_widget_set_property (b, "button.style", "compact");
962
963   if (popup)
964     {
965       ml_button_set_popup (b, ML_DISCUSSION_POPUP_WIN);
966       ml_button_set_popup_size (b, 640, 480);
967     }
968
969   hash_insert (w->button_cache, entry, b);
970
971   return b;
972 }
973
974 static void _post_form (ml_discussion w, int artid, int operation);
975
976 #define OP_POST 1
977 #define OP_REPLY_IN_PUBLIC 2
978 #define OP_REPLY_IN_PRIVATE 3
979 #define OP_SUPERSEDE 4
980
981 static void
982 post_form (ml_session session, void *vw)
983 {
984   _post_form ((ml_discussion) vw, 0, OP_POST);
985 }
986
987 static void
988 reply_in_public_form (ml_session session, void *vargs)
989 {
990   struct button_args *args = (struct button_args *) vargs;
991
992   _post_form (args->w, args->artid, OP_REPLY_IN_PUBLIC);
993 }
994
995 static void
996 reply_in_private_form (ml_session session, void *vargs)
997 {
998   struct button_args *args = (struct button_args *) vargs;
999
1000   _post_form (args->w, args->artid, OP_REPLY_IN_PRIVATE);
1001 }
1002
1003 static void
1004 save_form (ml_session session, void *vargs)
1005 {
1006   abort ();
1007 }
1008
1009 static void
1010 cancel_form (ml_session session, void *vargs)
1011 {
1012   abort ();
1013 }
1014
1015 static void
1016 supersede_form (ml_session session, void *vargs)
1017 {
1018   struct button_args *args = (struct button_args *) vargs;
1019
1020   _post_form (args->w, args->artid, OP_SUPERSEDE);
1021 }
1022
1023 static void
1024 prev_button (ml_session session, void *vw)
1025 {
1026   ml_discussion w = (ml_discussion) vw;
1027
1028   w->first_item -= w->nr_items;
1029   if (w->first_item < 0) w->first_item = 0;
1030 }
1031
1032 static void
1033 next_button (ml_session session, void *vw)
1034 {
1035   ml_discussion w = (ml_discussion) vw;
1036
1037   w->first_item += w->nr_items;
1038 }
1039
1040 /* Structure which is used as the argument to _post. */
1041 struct post_args
1042 {
1043   int artid;
1044   int operation;
1045   ml_discussion w;
1046 };
1047
1048 static void _post (ml_session session, void *vargs);
1049
1050 /* This function generates the new window which is used when
1051  *  * posting,
1052  *  * replying in public or private to, or
1053  *  * superseding
1054  * an article.
1055  */
1056 static void
1057 _post_form (ml_discussion w, int artid, int operation)
1058 {
1059   ml_window win;
1060   ml_form form;
1061   ml_form_layout tbl;
1062   ml_form_submit submit;
1063   db_handle dbh;
1064   st_handle sth;
1065   const char *art_author, *art_subject, *art_body;
1066   char art_body_type;
1067   struct post_args *args = pmalloc (w->pool, sizeof *args);
1068
1069   assert (artid || operation == OP_POST);
1070
1071   /* Check that the user is allowed to post. */
1072   if (!w->allow_anon && !ml_session_userid (w->session))
1073     {
1074       ml_error_window
1075         (w->pool, w->session,
1076          "The administrator of this newsgroup has disallowed anonymous "
1077          "postings. You need to be logged in to post to this newsgroup.",
1078          ML_DIALOG_CLOSE_BUTTON);
1079       return;
1080     }
1081
1082   /* Fetch the original/parent article from the database. */
1083   if (artid)
1084     {
1085       dbh = get_db_handle (w->conninfo, DBI_THROW_ERRORS);
1086       sth = st_prepare_cached
1087         (dbh,
1088          "select a.subject, a.body, a.body_type, u.username "
1089          "from ml_discussion_article a "
1090          "     left outer join ml_users u on a.author = u.userid "
1091          "where a.id = ?",
1092          DBI_INT);
1093       st_execute (sth, artid);
1094
1095       st_bind (sth, 0, art_subject, DBI_STRING);
1096       st_bind (sth, 1, art_body, DBI_STRING);
1097       st_bind (sth, 2, art_body_type, DBI_CHAR);
1098       st_bind (sth, 3, art_author, DBI_STRING);
1099
1100       if (!st_fetch (sth))
1101         {
1102           ml_error_window
1103             (w->pool, w->session,
1104              "Cannot find that article in the database. Perhaps "
1105              "it has expired or been deleted by an administrator?",
1106              ML_DIALOG_CLOSE_BUTTON);
1107           return;
1108         }
1109
1110       put_db_handle (dbh);
1111     }
1112
1113   win = new_ml_window (w->session, w->pool);
1114
1115   /* Create and populate the form. */
1116   form = new_ml_form (w->pool);
1117   args->artid = artid;
1118   args->operation = operation;
1119   args->w = w;
1120   ml_form_set_callback (form, _post, w->session, args);
1121   ml_widget_set_property (form, "method", "GET");
1122   tbl = new_ml_form_layout (w->pool);
1123
1124   /* To: line. */
1125   if (operation != OP_REPLY_IN_PRIVATE)
1126     ml_form_layout_pack (tbl, "To:", new_ml_text_label (w->pool, w->name));
1127   else
1128     ml_form_layout_pack (tbl, "To:", new_ml_text_label (w->pool, art_author));
1129
1130   /* Subject: line. */
1131   w->subject = new_ml_form_text (w->pool, form);
1132   if (operation == OP_REPLY_IN_PRIVATE || operation == OP_REPLY_IN_PUBLIC)
1133     {
1134       if (strncasecmp (art_subject, "Re: ", 4) == 0)
1135         ml_form_input_set_value (w->subject, art_subject);
1136       else
1137         ml_form_input_set_value (w->subject,
1138                                  psprintf (w->pool, "Re: %s", art_subject));
1139     }
1140   else if (operation == OP_SUPERSEDE)
1141     ml_form_input_set_value (w->subject, art_subject);
1142   ml_widget_set_property (w->subject, "form.text.size", 50);
1143   ml_form_layout_pack (tbl, "Subject:", w->subject);
1144
1145   /* Body text. */
1146   w->body = new_ml_form_textarea (w->pool, form, 20, 60);
1147   if (operation == OP_SUPERSEDE)
1148     ml_form_input_set_value (w->body, art_body);
1149   ml_form_layout_pack (tbl, "Body:", w->body);
1150
1151   /* Body type. */
1152   w->body_type = new_ml_form_select (w->pool, form);
1153   ml_form_select_push_back (w->body_type, "Plain text");
1154   ml_form_select_push_back (w->body_type, "*Smart* text");
1155   ml_form_select_push_back (w->body_type, "HTML");
1156   if (operation != OP_SUPERSEDE)
1157     ml_form_select_set_selection (w->body_type, 1); /* XXX From preferences. */
1158   else
1159     {
1160       switch (art_body_type)
1161         {
1162         case 'p': ml_form_select_set_selection (w->body_type, 0); break;
1163         case 's': ml_form_select_set_selection (w->body_type, 1); break;
1164         case 'h': ml_form_select_set_selection (w->body_type, 2); break;
1165         }
1166     }
1167   ml_form_layout_pack (tbl, 0, w->body_type);
1168
1169   /* Submit button. */
1170   /* XXX Cancel. */
1171   submit = new_ml_form_submit (w->pool, form, "Post");
1172   ml_form_layout_pack (tbl, 0, submit);
1173
1174   ml_form_pack (form, tbl);
1175   ml_window_pack (win, form);
1176 }
1177
1178 static const char *clean_up_string (pool, const char *text);
1179
1180 static void
1181 _post (ml_session session, void *vargs)
1182 {
1183   struct post_args *args = (struct post_args *) vargs;
1184   ml_discussion w = args->w;
1185   int userid = ml_session_userid (w->session);
1186   db_handle dbh;
1187   st_handle sth;
1188   const char *subject, *body;
1189   char body_type;
1190
1191   /* Check that the user is allowed to post. */
1192   if (!w->allow_anon && !userid)
1193     {
1194       ml_error_window
1195         (w->pool, w->session,
1196          "The administrator of this newsgroup has disallowed anonymous "
1197          "postings. You need to be logged in to post to this newsgroup.",
1198          ML_DIALOG_CLOSE_BUTTON);
1199       return;
1200     }
1201
1202   /* Only logged-in users may reply in private, to allow tracability of spam.*/
1203   if (args->operation == OP_REPLY_IN_PRIVATE && !userid)
1204     {
1205       ml_error_window
1206         (w->pool, w->session,
1207          "You can only use the 'Reply in private' function if you "
1208          "are logged in.",
1209          ML_DIALOG_CLOSE_BUTTON);
1210       return;
1211     }
1212
1213   /* Check the user has put in a subject line and body. Otherwise
1214    * just return which redisplays the form.
1215    */
1216   subject = ml_form_input_get_value (w->subject);
1217   body = ml_form_input_get_value (w->body);
1218   if (!subject || strlen (subject) == 0 ||
1219       !body || strlen (body) == 0)
1220     return;
1221
1222   switch (ml_form_select_get_selection (w->body_type))
1223     {
1224     case 0:  body_type = 'p'; break;
1225     default: body_type = 's'; break;
1226     case 2:  body_type = 'h'; break;
1227     }
1228
1229   dbh = get_db_handle (w->conninfo, DBI_THROW_ERRORS);
1230
1231   if (args->operation == OP_POST ||
1232       args->operation == OP_REPLY_IN_PUBLIC)
1233     {
1234       /* Insert the article into the database. */
1235       sth = st_prepare_cached
1236         (dbh,
1237          "insert into ml_discussion_article "
1238          "(resid, parent, subject, author, body, body_type, original_ip) "
1239          "values (?, ?, ?, ?, ?, ?, ?)",
1240          DBI_INT, DBI_INT_OR_NULL, DBI_STRING, DBI_INT_OR_NULL,
1241          DBI_STRING, DBI_CHAR, DBI_STRING);
1242       st_execute (sth, w->resid, args->artid, subject, userid, body, body_type,
1243                   ml_session_get_peernamestr (session));
1244     }
1245   else if (args->operation == OP_SUPERSEDE)
1246     {
1247       /* Replace an existing article in the database. */
1248       sth = st_prepare_cached
1249         (dbh,
1250          "update ml_discussion_article "
1251          "  set subject = ?, body = ?, body_type = ?, original_ip = ? "
1252          "where id = ? and author = ?",
1253          DBI_STRING, DBI_STRING, DBI_CHAR, DBI_STRING, DBI_INT, DBI_INT);
1254       st_execute (sth, subject, body, body_type,
1255                   ml_session_get_peernamestr (session),
1256                   args->artid, userid);
1257     }
1258   else if (args->operation == OP_REPLY_IN_PRIVATE)
1259     {
1260       const char *to_username, *to_email;
1261       const char *from_username, *from_email;
1262       io_handle sendmail;
1263       const char *sendmail_cmd = "/usr/sbin/sendmail -t -i";
1264
1265       /* Get the original article username and email address. */
1266       sth = st_prepare_cached
1267         (dbh,
1268          "select a.id, u.username, u.email "
1269          "from ml_discussion_article a "
1270          "     left outer join ml_users u on a.author = u.userid "
1271          "where a.id = ?",
1272          DBI_INT);
1273       st_execute (sth, args->artid);
1274
1275       st_bind (sth, 1, to_username, DBI_STRING);
1276       st_bind (sth, 2, to_email, DBI_STRING);
1277
1278       if (!st_fetch (sth) || !to_username || !to_email)
1279         {
1280           ml_error_window
1281             (w->pool, w->session,
1282              "Could not retrieve the article author from the database.",
1283              ML_DIALOG_CLOSE_BUTTON);
1284           return;
1285         }
1286
1287       /* Get the logged-in username and email address. */
1288       sth = st_prepare_cached
1289         (dbh,
1290          "select username, email from ml_users where userid = ?",
1291          DBI_INT);
1292       st_execute (sth, userid);
1293
1294       st_bind (sth, 0, from_username, DBI_STRING);
1295       st_bind (sth, 1, from_email, DBI_STRING);
1296
1297       if (!st_fetch (sth) || !from_username || !from_email)
1298         {
1299           ml_error_window
1300             (w->pool, w->session,
1301              "Could not retrieve your username or email address "
1302              "from the database.",
1303              ML_DIALOG_CLOSE_BUTTON);
1304           return;
1305         }
1306
1307       /* Clean up the usernames and email addresses so we can safely
1308        * include them in the email.
1309        */
1310       to_username = clean_up_string (w->pool, to_username);
1311       to_email = clean_up_string (w->pool, to_email);
1312       from_username = clean_up_string (w->pool, from_username);
1313       from_email = clean_up_string (w->pool, from_email);
1314
1315       /* Similarly clean up the subject line. */
1316       subject = clean_up_string (w->pool, subject);
1317
1318       /* Oh dear. Sending an HTML-format email with no text alternative
1319        * is *not* very clever. XXX
1320        */
1321       sendmail = io_popen (sendmail_cmd, "w");
1322       if (!sendmail)
1323         pth_die ("could not invoke sendmail");
1324
1325       io_fprintf (sendmail,
1326                   "X-Monolith-Trace: %s %s %s\n"
1327                   "From: %s <%s>\n"
1328                   "To: %s <%s>\n"
1329                   "Subject: %s\n"
1330                   "Content-Type: text/html\n" /* Charset XXX */
1331                   "\n",
1332                   ml_session_get_peernamestr (session),
1333                   ml_session_host_header (session),
1334                   ml_session_canonical_path (session),
1335                   from_username, from_email,
1336                   to_username, to_email,
1337                   subject);
1338       io_fputs (body, sendmail);
1339
1340       io_pclose (sendmail);
1341     }
1342   else
1343     {
1344       abort ();                 /* Unknown operation. */
1345     }
1346
1347   db_commit (dbh);
1348   put_db_handle (dbh);
1349
1350   /* Confirmation page. */
1351   ml_ok_window (w->pool, session,
1352                 "The article was sent.",
1353                 ML_DIALOG_CLOSE_BUTTON | ML_DIALOG_CLOSE_RELOAD_OPENER);
1354 }
1355
1356 /* Remove CRs and LFs from the string. */
1357 static const char *
1358 clean_up_string (pool pool, const char *text)
1359 {
1360   if (strpbrk (text, "\n\r"))
1361     {
1362       char *copy = pstrdup (pool, text);
1363       char *t = copy;
1364
1365       while ((t = strpbrk (t, "\n\r")) != 0)
1366         *t++ = ' ';
1367
1368       return copy;
1369     }
1370   else
1371     return text;                /* String is safe. */
1372 }