1 /* Monolith discussion widget.
2 * - by Richard W.M. Jones <rich@annexia.org>
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.
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.
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.
18 * $Id: ml_discussion.c,v 1.13 2003/02/22 15:34:29 rich Exp $
40 #include <pthr_iolib.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>
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>
57 #include "ml_discussion.h"
61 #define ML_DISCUSSION_POPUP_WIN "ml_discussion_popup"
63 static void repaint (void *, ml_session, const char *, io_handle);
65 struct ml_widget_operations discussion_ops =
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. */
83 /* In article display mode, these are the buttons that appear across the
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. */
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
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). */
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
114 expire_old_articles (pool pool, db_handle dbh, int resid, int expiry_days)
119 sth = st_prepare_cached
121 "delete from ml_discussion_article "
123 "and current_timestamp - posted_date >= interval ?",
124 DBI_INT, DBI_STRING);
125 rows = st_execute (sth,
127 psprintf (pool, "%d days", expiry_days));
129 if (rows > 0) /* Any rows actually deleted? */
131 sth = st_prepare_cached
133 "select coalesce (min (id), 0) from ml_discussion_article "
136 st_execute (sth, resid);
138 st_bind (sth, 0, min_id, DBI_INT);
140 sth = st_prepare_cached
142 "delete from ml_discussion_read "
146 st_execute (sth, resid, min_id);
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);
155 new_ml_discussion (pool pool, ml_session session, const char *conninfo, const char *res_name)
157 ml_discussion w = pmalloc (pool, sizeof *w);
160 char *expiry_days_str;
163 fprintf (stderr, "new_ml_discussion: creating new widget for %s\n",
167 w->ops = &discussion_ops;
169 w->session = session;
170 w->conninfo = conninfo;
173 /* Get the resource ID and a few other details. */
174 dbh = get_db_handle (conninfo, DBI_THROW_ERRORS);
176 sth = st_prepare_cached
178 "select r.resid, g.allow_anon, g.default_view, g.expiry_days "
179 "from ml_resources r, ml_discussion_group g "
181 "and r.resid = g.resid",
183 st_execute (sth, res_name);
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);
190 if (!st_fetch (sth)) return 0; /* Not found. */
192 /* Rather than running cron jobs, just expire old articles here
199 if (sscanf (expiry_days_str, "%d", &expiry_days) == 1 &&
201 expire_old_articles (pool, dbh, w->resid, expiry_days);
204 /* Be polite, put back the database handle. */
211 /* Create the buttons. Previous and next buttons start disabled, but
212 * are enabled in the repaint function, if appropriate.
214 w->prev = new_ml_button (pool, "<<");
215 w->next = new_ml_button (pool, ">>");
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);
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);
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);
228 /* Cache of buttons. */
229 w->button_cache = new_hash (pool, struct button_entry, ml_button);
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);
245 /* when we have flattened the representation of the tree into a vector,
246 * each vector element will have the following type.
250 int depth; /* 0 = top-level. */
251 int artid; /* Article ID. */
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)
262 const char *subject, *username, *body, *posted_date;
269 assert (vector_size (artids) == vector_size (artlist));
271 /* Maps article ID -> article contents. */
272 articles = new_hash (w->pool, int, struct article);
274 /* In 1-pane mode we are going to display the full article contents. */
275 if (vector_size (artids) > 0)
282 sth = st_prepare_cached
284 "select a.id, a.subject, u.userid, u.username, a.body, a.body_type, "
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);
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);
300 while (st_fetch (sth))
301 hash_insert (articles, artid, art);
304 /* Pull out the read/unread status of each article (but not for
305 * anonymous users - they see all articles as unread).
309 int low, high; /* Each range is [low, high-1]. */
311 sth = st_prepare_cached
313 "select low, high from ml_discussion_read "
314 "where resid = ? and userid = ?",
316 st_execute (sth, w->resid, userid);
318 st_bind (sth, 0, low, DBI_INT);
319 st_bind (sth, 1, high, DBI_INT);
321 while (st_fetch (sth))
323 struct article *artp;
326 /* Mark each article in the range [low, high-1] as read. */
327 for (artid = low; artid < high; ++artid)
329 if (hash_get_ptr (articles, artid, artp))
335 /* Display the articles. */
336 for (i = 0; i < vector_size (artlist); ++i)
344 vector_get (artlist, i, d_art);
347 if (!hash_get (articles, artid, art)) abort ();
350 "<table class=\"ml_discussion_posting\" "
351 "style=\"margin-left: %dem\">\n",
354 /* Unread postings appear in bold. */
355 h_class = art.read ? "read" : "unread";
357 /* The article subject, username, date. */
358 io_fprintf (io, "<tr><th class=\"ml_discussion_posting_%s\">",
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);
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);
372 io_fprintf (io, "<tr><td>");
373 if (w->allow_anon || userid)
375 b = get_button (w, session, BUTTON_NAME_REPLY_IN_PUBLIC, artid);
376 ml_widget_repaint (b, session, windowid, io);
377 io_fputs (" ", io);
378 if (userid && art.userid)
380 b = get_button (w, session, BUTTON_NAME_REPLY_IN_PRIVATE, artid);
381 ml_widget_repaint (b, session, windowid, io);
382 io_fputs (" ", io);
385 b = get_button (w, session, BUTTON_NAME_SAVE, artid);
386 ml_widget_repaint (b, session, windowid, io);
387 io_fputs (" ", io);
388 if (userid && userid == art.userid)
390 b = get_button (w, session, BUTTON_NAME_SUPERSEDE, artid);
391 ml_widget_repaint (b, session, windowid, io);
392 io_fputs (" ", io);
394 if (0) /* XXX Administrator. */
396 b = get_button (w, session, BUTTON_NAME_CANCEL, artid);
397 ml_widget_repaint (b, session, windowid, io);
398 io_fputs (" ", io);
400 io_fprintf (io, "</td></tr>\n");
401 io_fprintf (io, "</table>");
403 /* Mark article as read. */
404 if (art.read == 0) mark_read (session, dbh, w->resid, userid, artid);
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)
417 flatten_tree (pool pool, tree node, int depth)
420 vector v = new_vector (pool, struct d_art);
426 tree_get_data (node, d_art.artid);
427 vector_push_back (v, d_art);
430 for (i = 0; i < tree_nr_subnodes (node); ++i)
434 tree_get_subnode (node, i, t);
435 vector_push_back_vector (v, flatten_tree (pool, t, depth+1));
442 repaint_display (ml_discussion w, ml_session session,
443 const char *windowid, io_handle io)
448 char view = w->default_view;
449 char sort_order = 'd';
450 const char *sort_col, *order, *sql;
456 vector artlist, artids;
459 fprintf (stderr, "ml_discussion.c: repaint_display called\n");
462 /* Get a database handle. */
463 dbh = get_db_handle (w->conninfo, DBI_THROW_ERRORS);
465 db_set_debug (dbh, 1);
468 userid = ml_session_userid (session);
472 /* Pull out the user preferences, so we know if we are in 1-pane
475 sth = st_prepare_cached
477 "select view, sort_order "
478 "from ml_discussion_userprefs "
481 st_execute (sth, userid);
482 st_bind (sth, 0, view, DBI_CHAR);
483 st_bind (sth, 1, sort_order, DBI_CHAR);
487 /* Pull out the article IDs and thread them by hand. Relational databases
488 * really don't handle trees at all.
490 switch (sort_order) {
491 case 'd': case 'n': case 's': order = "desc"; break;
492 default: order = "asc";
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";
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 "
506 sth = st_prepare_cached (dbh, sql, DBI_INT);
507 st_execute (sth, w->resid);
509 st_bind (sth, 0, artid, DBI_INT);
510 st_bind (sth, 1, parentid, DBI_INT);
512 /* This hash maps article IDs -> tree nodes. */
513 articles = new_hash (w->pool, int, tree);
515 /* The top_node is empty. Children of this node are the lead
516 * articles in each thread.
518 top_node = new_tree (w->pool, int);
519 tree_set_data (top_node, zero);
521 hash_insert (articles, zero, top_node);
523 while (st_fetch (sth))
528 /* Have we seen this parent ID node before? */
529 if (! hash_get (articles, parentid, parent_node))
531 parent_node = new_tree (w->pool, int);
532 tree_set_data (parent_node, parentid);
533 hash_insert (articles, parentid, parent_node);
536 /* Have we seen this article ID node before? */
537 if (! hash_get (articles, artid, article_node))
539 article_node = new_tree (w->pool, int);
540 tree_set_data (article_node, artid);
541 hash_insert (articles, artid, article_node);
544 /* Set the relationship between the parent node and the article node. */
545 tree_push_back (parent_node, article_node);
548 /* Now flatten the tree into a list, preserving depth information. */
549 artlist = flatten_tree (w->pool, top_node, -1);
551 /* Extract just the nodes which are going to be displayed. */
552 more_articles = vector_size (artlist) - (w->first_item + w->nr_items) > 0;
554 new_subvector (w->pool, artlist, w->first_item,
556 w->first_item + w->nr_items :
557 vector_size (artlist)));
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);
563 ml_button_set_callback (w->prev, 0, session, 0);
566 ml_button_set_callback (w->next, next_button, session, w);
568 ml_button_set_callback (w->next, 0, session, 0);
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)
576 vector_get (artlist, i, d_art);
577 vector_push_back (artids, d_art.artid);
580 /* Print the standard buttons along the top of the widget. */
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\">");
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);
597 /* Finish off the widget. */
598 io_fprintf (io, "</td></tr></table>");
600 /* Commit changes (in article read/unread state) back to the database. */
603 /* Be polite: give back the database handle. */
605 db_set_debug (dbh, 0);
611 repaint (void *vw, ml_session session, const char *windowid, io_handle io)
613 ml_discussion w = (ml_discussion) vw;
616 fprintf (stderr, "ml_discussion.c: repaint called\n");
619 repaint_display (w, session, windowid, io);
624 static void mark_read_error (ml_session session, int resid, int userid, char type);
626 /* Mark the single article ID as read.
627 * Caution: This only works if the article is not already marked as read.
630 mark_read (ml_session session, db_handle dbh, int resid, int userid, int artid)
632 /* The method used is as follows:
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).
641 * prev_artid < artid < next_artid
643 * From the ml_discussion_read table, looking only at rows which correspond
644 * to the current (resid, userid), find any rows where:
646 * prev_artid < high and high <= artid, [type A]
650 * artid < low and low <= next_artid. [type B]
652 * If we found no rows, create a new row.
654 * If we found one row of type A, modify this row so high = artid + 1.
656 * If we found one row of type B, modify this row so low = artid.
658 * If we found one row of both types, merge those rows together.
660 * Anything else indicates an internal error.
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;
668 if (!userid) return; /* Ignore anonymous users. */
671 fprintf (stderr, "mark_read called: artid = %d\n", artid);
674 sth = st_prepare_cached
676 "select max (id) from ml_discussion_article "
677 "where resid = ? and id < ?",
679 st_execute (sth, resid, artid);
681 st_bind (sth, 0, prev_artid, DBI_INT);
685 sth = st_prepare_cached
687 "select min (id) from ml_discussion_article "
688 "where resid = ? and id > ?",
690 st_execute (sth, resid, artid);
692 st_bind (sth, 0, next_artid, DBI_INT);
697 fprintf (stderr, "\tprev_artid = %d, next_artid = %d\n", prev_artid, next_artid);
702 sth = st_prepare_cached
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);
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);
716 mark_read_error (session, resid, userid, 'A');
721 sth = st_prepare_cached
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);
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);
735 mark_read_error (session, resid, userid, 'B');
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);
745 /* No rows fetched: create a new row. */
746 if (!a_fetched && !b_fetched)
749 fprintf (stderr, "\tinserting low = %d, high = %d\n", artid, artid+1);
751 sth = st_prepare_cached
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);
758 /* One row of type A: modify this row so high = artid + 1. */
759 else if (a_fetched && !b_fetched)
762 fprintf (stderr, "\tmodifying low = %d, high = %d so high = %d\n",
763 a_low, a_high, artid + 1);
765 sth = st_prepare_cached
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);
772 /* One row of type B: modify this row so low = artid. */
773 else if (!a_fetched && b_fetched)
776 fprintf (stderr, "\tmodifying low = %d, high = %d so low = %d\n",
777 b_low, b_high, artid);
779 sth = st_prepare_cached
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);
786 /* Two rows fetched: merge them. */
790 fprintf (stderr, "\tmerging\n");
792 sth = st_prepare_cached
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);
799 sth = st_prepare_cached
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);
809 mark_read_error (ml_session session, int resid, int userid, char type)
811 pool pool = ml_session_pool (session);
816 "INTERNAL ERROR in discussion widget: error type '%c'\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"
821 "SELECT id FROM ml_discussion_article WHERE resid = %d ORDER BY 1\n"
823 "SELECT low, high FROM ml_discussion_read WHERE resid = %d AND userid = %d ORDER BY 1\n",
824 type, resid, resid, userid);
829 /* Mark all articles as read - this is very simple. */
831 mark_all_read (ml_session session, void *vw)
833 ml_discussion w = (ml_discussion) vw;
834 int userid = ml_session_userid (session);
835 int resid = w->resid;
840 if (!userid) return; /* Ignore for anonymous users. */
842 dbh = get_db_handle (w->conninfo, DBI_THROW_ERRORS);
844 sth = st_prepare_cached
846 "delete from ml_discussion_read where resid = ? and userid = ?",
848 st_execute (sth, resid, userid);
850 /* Get the max. article ID. */
851 sth = st_prepare_cached
853 "select max(id) from ml_discussion_article where resid = ?",
855 st_execute (sth, resid);
857 st_bind (sth, 0, max_artid, DBI_INT);
862 sth = st_prepare_cached
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);
874 /* Mark all articles as unread - this is very simple. */
876 mark_all_unread (ml_session session, void *vw)
878 ml_discussion w = (ml_discussion) vw;
879 int userid = ml_session_userid (session);
880 int resid = w->resid;
884 if (!userid) return; /* Ignore for anonymous users. */
886 dbh = get_db_handle (w->conninfo, DBI_THROW_ERRORS);
888 sth = st_prepare_cached
890 "delete from ml_discussion_read where resid = ? and userid = ?",
892 st_execute (sth, resid, userid);
905 get_button (ml_discussion w, ml_session session,
908 struct button_entry entry;
911 void (*fn) (ml_session session, void *args);
913 struct button_args *args;
918 if (hash_get (w->button_cache, entry, b))
921 /* Create the button. */
924 case BUTTON_NAME_REPLY_IN_PUBLIC:
925 text = "Reply in public";
926 fn = reply_in_public_form;
929 case BUTTON_NAME_REPLY_IN_PRIVATE:
930 text = "Reply in private";
931 fn = reply_in_private_form;
934 case BUTTON_NAME_SAVE:
937 fn = 0; (void)save_form;
940 case BUTTON_NAME_CANCEL:
943 fn = 0; (void)cancel_form;
946 case BUTTON_NAME_SUPERSEDE:
955 b = new_ml_button (w->pool, text);
956 args = pmalloc (w->pool, sizeof *args);
959 ml_button_set_callback (b, fn, session, args);
961 ml_widget_set_property (b, "button.style", "compact");
965 ml_button_set_popup (b, ML_DISCUSSION_POPUP_WIN);
966 ml_button_set_popup_size (b, 640, 480);
969 hash_insert (w->button_cache, entry, b);
974 static void _post_form (ml_discussion w, int artid, int operation);
977 #define OP_REPLY_IN_PUBLIC 2
978 #define OP_REPLY_IN_PRIVATE 3
979 #define OP_SUPERSEDE 4
982 post_form (ml_session session, void *vw)
984 _post_form ((ml_discussion) vw, 0, OP_POST);
988 reply_in_public_form (ml_session session, void *vargs)
990 struct button_args *args = (struct button_args *) vargs;
992 _post_form (args->w, args->artid, OP_REPLY_IN_PUBLIC);
996 reply_in_private_form (ml_session session, void *vargs)
998 struct button_args *args = (struct button_args *) vargs;
1000 _post_form (args->w, args->artid, OP_REPLY_IN_PRIVATE);
1004 save_form (ml_session session, void *vargs)
1010 cancel_form (ml_session session, void *vargs)
1016 supersede_form (ml_session session, void *vargs)
1018 struct button_args *args = (struct button_args *) vargs;
1020 _post_form (args->w, args->artid, OP_SUPERSEDE);
1024 prev_button (ml_session session, void *vw)
1026 ml_discussion w = (ml_discussion) vw;
1028 w->first_item -= w->nr_items;
1029 if (w->first_item < 0) w->first_item = 0;
1033 next_button (ml_session session, void *vw)
1035 ml_discussion w = (ml_discussion) vw;
1037 w->first_item += w->nr_items;
1040 /* Structure which is used as the argument to _post. */
1048 static void _post (ml_session session, void *vargs);
1050 /* This function generates the new window which is used when
1052 * * replying in public or private to, or
1057 _post_form (ml_discussion w, int artid, int operation)
1062 ml_form_submit submit;
1065 const char *art_author, *art_subject, *art_body;
1067 struct post_args *args = pmalloc (w->pool, sizeof *args);
1069 assert (artid || operation == OP_POST);
1071 /* Check that the user is allowed to post. */
1072 if (!w->allow_anon && !ml_session_userid (w->session))
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);
1082 /* Fetch the original/parent article from the database. */
1085 dbh = get_db_handle (w->conninfo, DBI_THROW_ERRORS);
1086 sth = st_prepare_cached
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 "
1093 st_execute (sth, artid);
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);
1100 if (!st_fetch (sth))
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);
1110 put_db_handle (dbh);
1113 win = new_ml_window (w->session, w->pool);
1115 /* Create and populate the form. */
1116 form = new_ml_form (w->pool);
1117 args->artid = artid;
1118 args->operation = operation;
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);
1125 if (operation != OP_REPLY_IN_PRIVATE)
1126 ml_form_layout_pack (tbl, "To:", new_ml_text_label (w->pool, w->name));
1128 ml_form_layout_pack (tbl, "To:", new_ml_text_label (w->pool, art_author));
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)
1134 if (strncasecmp (art_subject, "Re: ", 4) == 0)
1135 ml_form_input_set_value (w->subject, art_subject);
1137 ml_form_input_set_value (w->subject,
1138 psprintf (w->pool, "Re: %s", art_subject));
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);
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);
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. */
1160 switch (art_body_type)
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;
1167 ml_form_layout_pack (tbl, 0, w->body_type);
1169 /* Submit button. */
1171 submit = new_ml_form_submit (w->pool, form, "Post");
1172 ml_form_layout_pack (tbl, 0, submit);
1174 ml_form_pack (form, tbl);
1175 ml_window_pack (win, form);
1178 static const char *clean_up_string (pool, const char *text);
1181 _post (ml_session session, void *vargs)
1183 struct post_args *args = (struct post_args *) vargs;
1184 ml_discussion w = args->w;
1185 int userid = ml_session_userid (w->session);
1188 const char *subject, *body;
1191 /* Check that the user is allowed to post. */
1192 if (!w->allow_anon && !userid)
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);
1202 /* Only logged-in users may reply in private, to allow tracability of spam.*/
1203 if (args->operation == OP_REPLY_IN_PRIVATE && !userid)
1206 (w->pool, w->session,
1207 "You can only use the 'Reply in private' function if you "
1209 ML_DIALOG_CLOSE_BUTTON);
1213 /* Check the user has put in a subject line and body. Otherwise
1214 * just return which redisplays the form.
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)
1222 switch (ml_form_select_get_selection (w->body_type))
1224 case 0: body_type = 'p'; break;
1225 default: body_type = 's'; break;
1226 case 2: body_type = 'h'; break;
1229 dbh = get_db_handle (w->conninfo, DBI_THROW_ERRORS);
1231 if (args->operation == OP_POST ||
1232 args->operation == OP_REPLY_IN_PUBLIC)
1234 /* Insert the article into the database. */
1235 sth = st_prepare_cached
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));
1245 else if (args->operation == OP_SUPERSEDE)
1247 /* Replace an existing article in the database. */
1248 sth = st_prepare_cached
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);
1258 else if (args->operation == OP_REPLY_IN_PRIVATE)
1260 const char *to_username, *to_email;
1261 const char *from_username, *from_email;
1263 const char *sendmail_cmd = "/usr/sbin/sendmail -t -i";
1265 /* Get the original article username and email address. */
1266 sth = st_prepare_cached
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 "
1273 st_execute (sth, args->artid);
1275 st_bind (sth, 1, to_username, DBI_STRING);
1276 st_bind (sth, 2, to_email, DBI_STRING);
1278 if (!st_fetch (sth) || !to_username || !to_email)
1281 (w->pool, w->session,
1282 "Could not retrieve the article author from the database.",
1283 ML_DIALOG_CLOSE_BUTTON);
1287 /* Get the logged-in username and email address. */
1288 sth = st_prepare_cached
1290 "select username, email from ml_users where userid = ?",
1292 st_execute (sth, userid);
1294 st_bind (sth, 0, from_username, DBI_STRING);
1295 st_bind (sth, 1, from_email, DBI_STRING);
1297 if (!st_fetch (sth) || !from_username || !from_email)
1300 (w->pool, w->session,
1301 "Could not retrieve your username or email address "
1302 "from the database.",
1303 ML_DIALOG_CLOSE_BUTTON);
1307 /* Clean up the usernames and email addresses so we can safely
1308 * include them in the email.
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);
1315 /* Similarly clean up the subject line. */
1316 subject = clean_up_string (w->pool, subject);
1318 /* Oh dear. Sending an HTML-format email with no text alternative
1319 * is *not* very clever. XXX
1321 sendmail = io_popen (sendmail_cmd, "w");
1323 pth_die ("could not invoke sendmail");
1325 io_fprintf (sendmail,
1326 "X-Monolith-Trace: %s %s %s\n"
1330 "Content-Type: text/html\n" /* Charset XXX */
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,
1338 io_fputs (body, sendmail);
1340 io_pclose (sendmail);
1344 abort (); /* Unknown operation. */
1348 put_db_handle (dbh);
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);
1356 /* Remove CRs and LFs from the string. */
1358 clean_up_string (pool pool, const char *text)
1360 if (strpbrk (text, "\n\r"))
1362 char *copy = pstrdup (pool, text);
1365 while ((t = strpbrk (t, "\n\r")) != 0)
1371 return text; /* String is safe. */