/* Monolith discussion widget. * - by Richard W.M. Jones * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this library; if not, write to the Free * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. * * $Id: ml_discussion.c,v 1.13 2003/02/22 15:34:29 rich Exp $ */ #include "config.h" #include #include #ifdef HAVE_ASSERT_H #include #endif #ifdef HAVE_STRING_H #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ml_discussion.h" #define MD_DEBUG 0 #define ML_DISCUSSION_POPUP_WIN "ml_discussion_popup" static void repaint (void *, ml_session, const char *, io_handle); struct ml_widget_operations discussion_ops = { repaint: repaint }; struct ml_discussion { struct ml_widget_operations *ops; pool pool; /* Pool for allocations. */ ml_session session; /* Current session. */ const char *conninfo; /* Database connection. */ int resid; /* Resource ID. */ const char *name; /* Newsgroup (resource) name. */ int allow_anon; /* Allow anonymous postings. */ char default_view; /* Default view (either 1-pane or 2-pane). */ int first_item; /* First item to display. */ int nr_items; /* Number of items to display on each page. */ /* In article display mode, these are the buttons that appear across the * top of the widget. */ ml_button prev, next; /* Previous/next buttons. */ ml_button post; /* Post button. */ ml_button mark_all_read, mark_all_unread; /* Mark all as read / unread. */ /* To avoid creating the same buttons over and over again during the * repaint, cache buttons here. This is a hash of struct button_entry * -> ml_button. */ hash button_cache; /* These are used during posting. */ ml_form_text subject; /* Subject line. */ ml_form_textarea body; /* Body of the posting. */ ml_form_select body_type; /* Type (plain, smart, HTML). */ }; struct button_entry { int name; #define BUTTON_NAME_REPLY_IN_PUBLIC 1 #define BUTTON_NAME_REPLY_IN_PRIVATE 2 #define BUTTON_NAME_SAVE 3 #define BUTTON_NAME_CANCEL 4 #define BUTTON_NAME_SUPERSEDE 5 int artid; }; static inline void expire_old_articles (pool pool, db_handle dbh, int resid, int expiry_days) { st_handle sth; int rows, min_id; sth = st_prepare_cached (dbh, "delete from ml_discussion_article " "where resid = ? " "and current_timestamp - posted_date >= interval ?", DBI_INT, DBI_STRING); rows = st_execute (sth, resid, psprintf (pool, "%d days", expiry_days)); if (rows > 0) /* Any rows actually deleted? */ { sth = st_prepare_cached (dbh, "select coalesce (min (id), 0) from ml_discussion_article " "where resid = ?", DBI_INT); st_execute (sth, resid); st_bind (sth, 0, min_id, DBI_INT); sth = st_prepare_cached (dbh, "delete from ml_discussion_read " "where resid = ? " "and high <= ?", DBI_INT, DBI_INT); st_execute (sth, resid, min_id); } } static void post_form (ml_session session, void *vw); static void mark_all_read (ml_session, void *vw); static void mark_all_unread (ml_session, void *vw); ml_discussion new_ml_discussion (pool pool, ml_session session, const char *conninfo, const char *res_name) { ml_discussion w = pmalloc (pool, sizeof *w); db_handle dbh; st_handle sth; char *expiry_days_str; #if MD_DEBUG fprintf (stderr, "new_ml_discussion: creating new widget for %s\n", res_name); #endif w->ops = &discussion_ops; w->pool = pool; w->session = session; w->conninfo = conninfo; w->name = res_name; /* Get the resource ID and a few other details. */ dbh = get_db_handle (conninfo, DBI_THROW_ERRORS); sth = st_prepare_cached (dbh, "select r.resid, g.allow_anon, g.default_view, g.expiry_days " "from ml_resources r, ml_discussion_group g " "where r.name = ? " "and r.resid = g.resid", DBI_STRING); st_execute (sth, res_name); st_bind (sth, 0, w->resid, DBI_INT); st_bind (sth, 1, w->allow_anon, DBI_BOOL); st_bind (sth, 2, w->default_view, DBI_CHAR); st_bind (sth, 3, expiry_days_str, DBI_STRING); if (!st_fetch (sth)) return 0; /* Not found. */ /* Rather than running cron jobs, just expire old articles here * if necessary. */ if (expiry_days_str) { int expiry_days; if (sscanf (expiry_days_str, "%d", &expiry_days) == 1 && expiry_days > 0) expire_old_articles (pool, dbh, w->resid, expiry_days); } /* Be polite, put back the database handle. */ db_commit (dbh); put_db_handle (dbh); w->first_item = 0; w->nr_items = 20; /* Create the buttons. Previous and next buttons start disabled, but * are enabled in the repaint function, if appropriate. */ w->prev = new_ml_button (pool, "<<"); w->next = new_ml_button (pool, ">>"); w->post = new_ml_button (pool, "New message"); ml_button_set_popup (w->post, ML_DISCUSSION_POPUP_WIN); ml_button_set_popup_size (w->post, 640, 480); ml_button_set_callback (w->post, post_form, session, w); w->mark_all_read = new_ml_button (pool, "Mark all read"); ml_button_set_callback (w->mark_all_read, mark_all_read, session, w); w->mark_all_unread = new_ml_button (pool, "Mark all unread"); ml_button_set_callback (w->mark_all_unread, mark_all_unread, session, w); /* Cache of buttons. */ w->button_cache = new_hash (pool, struct button_entry, ml_button); return w; } static ml_button get_button (ml_discussion w, ml_session session, int name, int artid); static void reply_in_public_form (ml_session session, void *vargs); static void reply_in_private_form (ml_session session, void *vargs); static void save_form (ml_session session, void *vargs); static void cancel_form (ml_session session, void *vargs); static void supersede_form (ml_session session, void *vargs); static void prev_button (ml_session session, void *vw); static void next_button (ml_session session, void *vw); static void mark_read (ml_session, db_handle dbh, int resid, int userid, int artid); /* when we have flattened the representation of the tree into a vector, * each vector element will have the following type. */ struct d_art { int depth; /* 0 = top-level. */ int artid; /* Article ID. */ }; static inline void repaint_1pane (ml_discussion w, ml_session session, const char *windowid, io_handle io, db_handle dbh, int userid, vector artlist, vector artids) { st_handle sth; hash articles; struct article { const char *subject, *username, *body, *posted_date; char body_type; int userid; int read; }; int i; assert (vector_size (artids) == vector_size (artlist)); /* Maps article ID -> article contents. */ articles = new_hash (w->pool, int, struct article); /* In 1-pane mode we are going to display the full article contents. */ if (vector_size (artids) > 0) { int artid; struct article art; art.read = 0; sth = st_prepare_cached (dbh, "select a.id, a.subject, u.userid, u.username, a.body, a.body_type, " "a.posted_date " "from ml_discussion_article a " "left outer join ml_users u on a.author = u.userid " "where a.resid = ? and a.id in (@)", DBI_INT, DBI_VECTOR_INT); st_execute (sth, w->resid, artids); st_bind (sth, 0, artid, DBI_INT); st_bind (sth, 1, art.subject, DBI_STRING); st_bind (sth, 2, art.userid, DBI_INT); st_bind (sth, 3, art.username, DBI_STRING); st_bind (sth, 4, art.body, DBI_STRING); st_bind (sth, 5, art.body_type, DBI_CHAR); st_bind (sth, 6, art.posted_date, DBI_STRING); while (st_fetch (sth)) hash_insert (articles, artid, art); } /* Pull out the read/unread status of each article (but not for * anonymous users - they see all articles as unread). */ if (userid) { int low, high; /* Each range is [low, high-1]. */ sth = st_prepare_cached (dbh, "select low, high from ml_discussion_read " "where resid = ? and userid = ?", DBI_INT, DBI_INT); st_execute (sth, w->resid, userid); st_bind (sth, 0, low, DBI_INT); st_bind (sth, 1, high, DBI_INT); while (st_fetch (sth)) { struct article *artp; int artid; /* Mark each article in the range [low, high-1] as read. */ for (artid = low; artid < high; ++artid) { if (hash_get_ptr (articles, artid, artp)) artp->read = 1; } } } /* Display the articles. */ for (i = 0; i < vector_size (artlist); ++i) { int artid; struct d_art d_art; struct article art; ml_button b; const char *h_class; vector_get (artlist, i, d_art); artid = d_art.artid; assert (artid > 0); if (!hash_get (articles, artid, art)) abort (); io_fprintf (io, "\n", d_art.depth * 2); /* Unread postings appear in bold. */ h_class = art.read ? "read" : "unread"; /* The article subject, username, date. */ io_fprintf (io, "\n", art.posted_date); /* The article body. */ io_fputs ("", io); /* Buttons. */ io_fprintf (io, "\n"); io_fprintf (io, "
", h_class); ml_plaintext_print (io, art.subject); io_fputs ("
\nby ", io); ml_plaintext_print (io, (art.userid ? art.username : "anonymous")); /* XXX Date formatting. */ io_fprintf (io, " on %s
", io); ml_anytext_print (io, art.body, art.body_type); io_fputs ("
"); if (w->allow_anon || userid) { b = get_button (w, session, BUTTON_NAME_REPLY_IN_PUBLIC, artid); ml_widget_repaint (b, session, windowid, io); io_fputs (" ", io); if (userid && art.userid) { b = get_button (w, session, BUTTON_NAME_REPLY_IN_PRIVATE, artid); ml_widget_repaint (b, session, windowid, io); io_fputs (" ", io); } } b = get_button (w, session, BUTTON_NAME_SAVE, artid); ml_widget_repaint (b, session, windowid, io); io_fputs (" ", io); if (userid && userid == art.userid) { b = get_button (w, session, BUTTON_NAME_SUPERSEDE, artid); ml_widget_repaint (b, session, windowid, io); io_fputs (" ", io); } if (0) /* XXX Administrator. */ { b = get_button (w, session, BUTTON_NAME_CANCEL, artid); ml_widget_repaint (b, session, windowid, io); io_fputs (" ", io); } io_fprintf (io, "
"); /* Mark article as read. */ if (art.read == 0) mark_read (session, dbh, w->resid, userid, artid); } } static inline void repaint_2pane (ml_discussion w, ml_session session, const char *windowid, io_handle io, db_handle dbh, int userid, vector artlist, vector artids) { abort (); /* XXX */ } static inline vector flatten_tree (pool pool, tree node, int depth) { int i; vector v = new_vector (pool, struct d_art); struct d_art d_art; if (depth >= 0) { d_art.depth = depth; tree_get_data (node, d_art.artid); vector_push_back (v, d_art); } for (i = 0; i < tree_nr_subnodes (node); ++i) { tree t; tree_get_subnode (node, i, t); vector_push_back_vector (v, flatten_tree (pool, t, depth+1)); } return v; } static inline void repaint_display (ml_discussion w, ml_session session, const char *windowid, io_handle io) { db_handle dbh; st_handle sth; int userid, i; char view = w->default_view; char sort_order = 'd'; const char *sort_col, *order, *sql; int artid, parentid; hash articles; tree top_node; const int zero = 0; int more_articles; vector artlist, artids; #if MD_DEBUG fprintf (stderr, "ml_discussion.c: repaint_display called\n"); #endif /* Get a database handle. */ dbh = get_db_handle (w->conninfo, DBI_THROW_ERRORS); #if MD_DEBUG db_set_debug (dbh, 1); #endif userid = ml_session_userid (session); if (userid) { /* Pull out the user preferences, so we know if we are in 1-pane * or 2-pane mode. */ sth = st_prepare_cached (dbh, "select view, sort_order " "from ml_discussion_userprefs " "where userid = ?", DBI_INT); st_execute (sth, userid); st_bind (sth, 0, view, DBI_CHAR); st_bind (sth, 1, sort_order, DBI_CHAR); st_fetch (sth); } /* Pull out the article IDs and thread them by hand. Relational databases * really don't handle trees at all. */ switch (sort_order) { case 'd': case 'n': case 's': order = "desc"; break; default: order = "asc"; } switch (sort_order) { case 'd': case 'D': sort_col = "a.posted_date"; break; case 'n': case 'N': sort_col = "u.username"; break; default: sort_col = "a.subject"; } sql = psprintf (w->pool, "select a.id, coalesce (a.parent, 0), %s " "from ml_discussion_article a " "left outer join ml_users u on a.author = u.userid " "where a.resid = ? " "order by 3 %s", sort_col, order); sth = st_prepare_cached (dbh, sql, DBI_INT); st_execute (sth, w->resid); st_bind (sth, 0, artid, DBI_INT); st_bind (sth, 1, parentid, DBI_INT); /* This hash maps article IDs -> tree nodes. */ articles = new_hash (w->pool, int, tree); /* The top_node is empty. Children of this node are the lead * articles in each thread. */ top_node = new_tree (w->pool, int); tree_set_data (top_node, zero); hash_insert (articles, zero, top_node); while (st_fetch (sth)) { tree parent_node; tree article_node; /* Have we seen this parent ID node before? */ if (! hash_get (articles, parentid, parent_node)) { parent_node = new_tree (w->pool, int); tree_set_data (parent_node, parentid); hash_insert (articles, parentid, parent_node); } /* Have we seen this article ID node before? */ if (! hash_get (articles, artid, article_node)) { article_node = new_tree (w->pool, int); tree_set_data (article_node, artid); hash_insert (articles, artid, article_node); } /* Set the relationship between the parent node and the article node. */ tree_push_back (parent_node, article_node); } /* Now flatten the tree into a list, preserving depth information. */ artlist = flatten_tree (w->pool, top_node, -1); /* Extract just the nodes which are going to be displayed. */ more_articles = vector_size (artlist) - (w->first_item + w->nr_items) > 0; artlist = new_subvector (w->pool, artlist, w->first_item, (more_articles ? w->first_item + w->nr_items : vector_size (artlist))); /* Enable the previous/next buttons (if necessary). */ if (w->first_item > 0) ml_button_set_callback (w->prev, prev_button, session, w); else ml_button_set_callback (w->prev, 0, session, 0); if (more_articles) ml_button_set_callback (w->next, next_button, session, w); else ml_button_set_callback (w->next, 0, session, 0); /* Get the IDs of the articles we are actually going to display. */ artids = new_vector (w->pool, int); for (i = 0; i < vector_size (artlist); ++i) { struct d_art d_art; vector_get (artlist, i, d_art); vector_push_back (artids, d_art.artid); } /* Print the standard buttons along the top of the widget. */ io_fprintf (io, "" "\n
"); ml_widget_repaint (w->prev, session, windowid, io); ml_widget_repaint (w->next, session, windowid, io); io_fprintf (io, ""); ml_widget_repaint (w->post, session, windowid, io); ml_widget_repaint (w->mark_all_read, session, windowid, io); ml_widget_repaint (w->mark_all_unread, session, windowid, io); io_fprintf (io, "
"); if (view == '1') /* 1-pane mode. */ repaint_1pane (w, session, windowid, io, dbh, userid, artlist, artids); else /* 2-pane mode. */ repaint_2pane (w, session, windowid, io, dbh, userid, artlist, artids); /* Finish off the widget. */ io_fprintf (io, "
"); /* Commit changes (in article read/unread state) back to the database. */ db_commit (dbh); /* Be polite: give back the database handle. */ #if MD_DEBUG db_set_debug (dbh, 0); #endif put_db_handle (dbh); } static void repaint (void *vw, ml_session session, const char *windowid, io_handle io) { ml_discussion w = (ml_discussion) vw; #if MD_DEBUG fprintf (stderr, "ml_discussion.c: repaint called\n"); #endif repaint_display (w, session, windowid, io); } #define MR_DEBUG 0 static void mark_read_error (ml_session session, int resid, int userid, char type); /* Mark the single article ID as read. * Caution: This only works if the article is not already marked as read. */ static void mark_read (ml_session session, db_handle dbh, int resid, int userid, int artid) { /* The method used is as follows: * * From the ml_discussion_article table, find the largest article number * which is < artid, and the smallest article number which is > artid. * Call these 'prev_artid' and 'next_artid' respectively (either may * be 0, indicating no such row). * * Now we have: * * prev_artid < artid < next_artid * * From the ml_discussion_read table, looking only at rows which correspond * to the current (resid, userid), find any rows where: * * prev_artid < high and high <= artid, [type A] * * and rows where: * * artid < low and low <= next_artid. [type B] * * If we found no rows, create a new row. * * If we found one row of type A, modify this row so high = artid + 1. * * If we found one row of type B, modify this row so low = artid. * * If we found one row of both types, merge those rows together. * * Anything else indicates an internal error. */ st_handle sth; int prev_artid, next_artid; int a_fetched = 0, a_low = 0, a_high = 0, b_fetched = 0, b_low = 0, b_high = 0; if (!userid) return; /* Ignore anonymous users. */ #if MR_DEBUG fprintf (stderr, "mark_read called: artid = %d\n", artid); #endif sth = st_prepare_cached (dbh, "select max (id) from ml_discussion_article " "where resid = ? and id < ?", DBI_INT, DBI_INT); st_execute (sth, resid, artid); st_bind (sth, 0, prev_artid, DBI_INT); st_fetch (sth); sth = st_prepare_cached (dbh, "select min (id) from ml_discussion_article " "where resid = ? and id > ?", DBI_INT, DBI_INT); st_execute (sth, resid, artid); st_bind (sth, 0, next_artid, DBI_INT); st_fetch (sth); #if MR_DEBUG fprintf (stderr, "\tprev_artid = %d, next_artid = %d\n", prev_artid, next_artid); #endif if (prev_artid) { sth = st_prepare_cached (dbh, "select 1, low, high from ml_discussion_read " "where resid = ? and userid = ? and " "? < high and high <= ?", DBI_INT, DBI_INT, DBI_INT, DBI_INT); st_execute (sth, resid, userid, prev_artid, artid); st_bind (sth, 0, a_fetched, DBI_INT); st_bind (sth, 1, a_low, DBI_INT); st_bind (sth, 2, a_high, DBI_INT); if (st_fetch (sth)) if (st_fetch (sth)) mark_read_error (session, resid, userid, 'A'); } if (next_artid) { sth = st_prepare_cached (dbh, "select 1, low, high from ml_discussion_read " "where resid = ? and userid = ? and " "? < low and low <= ?", DBI_INT, DBI_INT, DBI_INT, DBI_INT); st_execute (sth, resid, userid, artid, next_artid); st_bind (sth, 0, b_fetched, DBI_INT); st_bind (sth, 1, b_low, DBI_INT); st_bind (sth, 2, b_high, DBI_INT); if (st_fetch (sth)) if (st_fetch (sth)) mark_read_error (session, resid, userid, 'B'); } #if MR_DEBUG fprintf (stderr, "\ta_fetched = %d (low = %d, high = %d)\n", a_fetched, a_low, a_high); fprintf (stderr, "\tb_fetched = %d (low = %d, high = %d)\n", b_fetched, b_low, b_high); #endif /* No rows fetched: create a new row. */ if (!a_fetched && !b_fetched) { #if MR_DEBUG fprintf (stderr, "\tinserting low = %d, high = %d\n", artid, artid+1); #endif sth = st_prepare_cached (dbh, "insert into ml_discussion_read (resid, userid, low, high) " "values (?, ?, ?, ?)", DBI_INT, DBI_INT, DBI_INT, DBI_INT); st_execute (sth, resid, userid, artid, artid + 1); } /* One row of type A: modify this row so high = artid + 1. */ else if (a_fetched && !b_fetched) { #if MR_DEBUG fprintf (stderr, "\tmodifying low = %d, high = %d so high = %d\n", a_low, a_high, artid + 1); #endif sth = st_prepare_cached (dbh, "update ml_discussion_read set high = ? " "where resid = ? and userid = ? and low = ? and high = ?", DBI_INT, DBI_INT, DBI_INT, DBI_INT, DBI_INT); st_execute (sth, artid + 1, resid, userid, a_low, a_high); } /* One row of type B: modify this row so low = artid. */ else if (!a_fetched && b_fetched) { #if MR_DEBUG fprintf (stderr, "\tmodifying low = %d, high = %d so low = %d\n", b_low, b_high, artid); #endif sth = st_prepare_cached (dbh, "update ml_discussion_read set low = ? " "where resid = ? and userid = ? and low = ? and high = ?", DBI_INT, DBI_INT, DBI_INT, DBI_INT, DBI_INT); st_execute (sth, artid, resid, userid, b_low, b_high); } /* Two rows fetched: merge them. */ else { #if MR_DEBUG fprintf (stderr, "\tmerging\n"); #endif sth = st_prepare_cached (dbh, "delete from ml_discussion_read " "where resid = ? and userid = ? and low = ? and high = ?", DBI_INT, DBI_INT, DBI_INT, DBI_INT); st_execute (sth, resid, userid, a_low, a_high); sth = st_prepare_cached (dbh, "update ml_discussion_read set low = ? " "where resid = ? and userid = ? and low = ? and high = ?", DBI_INT, DBI_INT, DBI_INT, DBI_INT, DBI_INT); st_execute (sth, a_low, resid, userid, b_low, b_high); } } static void mark_read_error (ml_session session, int resid, int userid, char type) { pool pool = ml_session_pool (session); char *msg; msg = psprintf (pool, "INTERNAL ERROR in discussion widget: error type '%c'\n" "\n" "Please perform the following queries on the database and send the\n" "full results and error type back to your technical support contact:\n" "\n" "SELECT id FROM ml_discussion_article WHERE resid = %d ORDER BY 1\n" "\n" "SELECT low, high FROM ml_discussion_read WHERE resid = %d AND userid = %d ORDER BY 1\n", type, resid, resid, userid); pth_die (msg); } /* Mark all articles as read - this is very simple. */ static void mark_all_read (ml_session session, void *vw) { ml_discussion w = (ml_discussion) vw; int userid = ml_session_userid (session); int resid = w->resid; db_handle dbh; st_handle sth; int max_artid; if (!userid) return; /* Ignore for anonymous users. */ dbh = get_db_handle (w->conninfo, DBI_THROW_ERRORS); sth = st_prepare_cached (dbh, "delete from ml_discussion_read where resid = ? and userid = ?", DBI_INT, DBI_INT); st_execute (sth, resid, userid); /* Get the max. article ID. */ sth = st_prepare_cached (dbh, "select max(id) from ml_discussion_article where resid = ?", DBI_INT); st_execute (sth, resid); st_bind (sth, 0, max_artid, DBI_INT); st_fetch (sth); if (max_artid) { sth = st_prepare_cached (dbh, "insert into ml_discussion_read (resid, userid, low, high) " "values (?, ?, ?, ?)", DBI_INT, DBI_INT, DBI_INT, DBI_INT); st_execute (sth, resid, userid, 1, max_artid + 1); } db_commit (dbh); put_db_handle (dbh); } /* Mark all articles as unread - this is very simple. */ static void mark_all_unread (ml_session session, void *vw) { ml_discussion w = (ml_discussion) vw; int userid = ml_session_userid (session); int resid = w->resid; db_handle dbh; st_handle sth; if (!userid) return; /* Ignore for anonymous users. */ dbh = get_db_handle (w->conninfo, DBI_THROW_ERRORS); sth = st_prepare_cached (dbh, "delete from ml_discussion_read where resid = ? and userid = ?", DBI_INT, DBI_INT); st_execute (sth, resid, userid); db_commit (dbh); put_db_handle (dbh); } struct button_args { int artid; ml_discussion w; }; static ml_button get_button (ml_discussion w, ml_session session, int name, int artid) { struct button_entry entry; ml_button b; const char *text; void (*fn) (ml_session session, void *args); int popup; struct button_args *args; entry.name = name; entry.artid = artid; if (hash_get (w->button_cache, entry, b)) return b; /* Create the button. */ switch (name) { case BUTTON_NAME_REPLY_IN_PUBLIC: text = "Reply in public"; fn = reply_in_public_form; popup = 1; break; case BUTTON_NAME_REPLY_IN_PRIVATE: text = "Reply in private"; fn = reply_in_private_form; popup = 1; break; case BUTTON_NAME_SAVE: text = "Save"; //fn = save_form; fn = 0; (void)save_form; popup = 0; break; case BUTTON_NAME_CANCEL: text = "Delete"; //fn = cancel_form; fn = 0; (void)cancel_form; popup = 0; break; case BUTTON_NAME_SUPERSEDE: text = "Replace"; fn = supersede_form; popup = 1; break; default: abort (); } b = new_ml_button (w->pool, text); args = pmalloc (w->pool, sizeof *args); args->artid = artid; args->w = w; ml_button_set_callback (b, fn, session, args); ml_widget_set_property (b, "button.style", "compact"); if (popup) { ml_button_set_popup (b, ML_DISCUSSION_POPUP_WIN); ml_button_set_popup_size (b, 640, 480); } hash_insert (w->button_cache, entry, b); return b; } static void _post_form (ml_discussion w, int artid, int operation); #define OP_POST 1 #define OP_REPLY_IN_PUBLIC 2 #define OP_REPLY_IN_PRIVATE 3 #define OP_SUPERSEDE 4 static void post_form (ml_session session, void *vw) { _post_form ((ml_discussion) vw, 0, OP_POST); } static void reply_in_public_form (ml_session session, void *vargs) { struct button_args *args = (struct button_args *) vargs; _post_form (args->w, args->artid, OP_REPLY_IN_PUBLIC); } static void reply_in_private_form (ml_session session, void *vargs) { struct button_args *args = (struct button_args *) vargs; _post_form (args->w, args->artid, OP_REPLY_IN_PRIVATE); } static void save_form (ml_session session, void *vargs) { abort (); } static void cancel_form (ml_session session, void *vargs) { abort (); } static void supersede_form (ml_session session, void *vargs) { struct button_args *args = (struct button_args *) vargs; _post_form (args->w, args->artid, OP_SUPERSEDE); } static void prev_button (ml_session session, void *vw) { ml_discussion w = (ml_discussion) vw; w->first_item -= w->nr_items; if (w->first_item < 0) w->first_item = 0; } static void next_button (ml_session session, void *vw) { ml_discussion w = (ml_discussion) vw; w->first_item += w->nr_items; } /* Structure which is used as the argument to _post. */ struct post_args { int artid; int operation; ml_discussion w; }; static void _post (ml_session session, void *vargs); /* This function generates the new window which is used when * * posting, * * replying in public or private to, or * * superseding * an article. */ static void _post_form (ml_discussion w, int artid, int operation) { ml_window win; ml_form form; ml_form_layout tbl; ml_form_submit submit; db_handle dbh; st_handle sth; const char *art_author, *art_subject, *art_body; char art_body_type; struct post_args *args = pmalloc (w->pool, sizeof *args); assert (artid || operation == OP_POST); /* Check that the user is allowed to post. */ if (!w->allow_anon && !ml_session_userid (w->session)) { ml_error_window (w->pool, w->session, "The administrator of this newsgroup has disallowed anonymous " "postings. You need to be logged in to post to this newsgroup.", ML_DIALOG_CLOSE_BUTTON); return; } /* Fetch the original/parent article from the database. */ if (artid) { dbh = get_db_handle (w->conninfo, DBI_THROW_ERRORS); sth = st_prepare_cached (dbh, "select a.subject, a.body, a.body_type, u.username " "from ml_discussion_article a " " left outer join ml_users u on a.author = u.userid " "where a.id = ?", DBI_INT); st_execute (sth, artid); st_bind (sth, 0, art_subject, DBI_STRING); st_bind (sth, 1, art_body, DBI_STRING); st_bind (sth, 2, art_body_type, DBI_CHAR); st_bind (sth, 3, art_author, DBI_STRING); if (!st_fetch (sth)) { ml_error_window (w->pool, w->session, "Cannot find that article in the database. Perhaps " "it has expired or been deleted by an administrator?", ML_DIALOG_CLOSE_BUTTON); return; } put_db_handle (dbh); } win = new_ml_window (w->session, w->pool); /* Create and populate the form. */ form = new_ml_form (w->pool); args->artid = artid; args->operation = operation; args->w = w; ml_form_set_callback (form, _post, w->session, args); ml_widget_set_property (form, "method", "GET"); tbl = new_ml_form_layout (w->pool); /* To: line. */ if (operation != OP_REPLY_IN_PRIVATE) ml_form_layout_pack (tbl, "To:", new_ml_text_label (w->pool, w->name)); else ml_form_layout_pack (tbl, "To:", new_ml_text_label (w->pool, art_author)); /* Subject: line. */ w->subject = new_ml_form_text (w->pool, form); if (operation == OP_REPLY_IN_PRIVATE || operation == OP_REPLY_IN_PUBLIC) { if (strncasecmp (art_subject, "Re: ", 4) == 0) ml_form_input_set_value (w->subject, art_subject); else ml_form_input_set_value (w->subject, psprintf (w->pool, "Re: %s", art_subject)); } else if (operation == OP_SUPERSEDE) ml_form_input_set_value (w->subject, art_subject); ml_widget_set_property (w->subject, "form.text.size", 50); ml_form_layout_pack (tbl, "Subject:", w->subject); /* Body text. */ w->body = new_ml_form_textarea (w->pool, form, 20, 60); if (operation == OP_SUPERSEDE) ml_form_input_set_value (w->body, art_body); ml_form_layout_pack (tbl, "Body:", w->body); /* Body type. */ w->body_type = new_ml_form_select (w->pool, form); ml_form_select_push_back (w->body_type, "Plain text"); ml_form_select_push_back (w->body_type, "*Smart* text"); ml_form_select_push_back (w->body_type, "HTML"); if (operation != OP_SUPERSEDE) ml_form_select_set_selection (w->body_type, 1); /* XXX From preferences. */ else { switch (art_body_type) { case 'p': ml_form_select_set_selection (w->body_type, 0); break; case 's': ml_form_select_set_selection (w->body_type, 1); break; case 'h': ml_form_select_set_selection (w->body_type, 2); break; } } ml_form_layout_pack (tbl, 0, w->body_type); /* Submit button. */ /* XXX Cancel. */ submit = new_ml_form_submit (w->pool, form, "Post"); ml_form_layout_pack (tbl, 0, submit); ml_form_pack (form, tbl); ml_window_pack (win, form); } static const char *clean_up_string (pool, const char *text); static void _post (ml_session session, void *vargs) { struct post_args *args = (struct post_args *) vargs; ml_discussion w = args->w; int userid = ml_session_userid (w->session); db_handle dbh; st_handle sth; const char *subject, *body; char body_type; /* Check that the user is allowed to post. */ if (!w->allow_anon && !userid) { ml_error_window (w->pool, w->session, "The administrator of this newsgroup has disallowed anonymous " "postings. You need to be logged in to post to this newsgroup.", ML_DIALOG_CLOSE_BUTTON); return; } /* Only logged-in users may reply in private, to allow tracability of spam.*/ if (args->operation == OP_REPLY_IN_PRIVATE && !userid) { ml_error_window (w->pool, w->session, "You can only use the 'Reply in private' function if you " "are logged in.", ML_DIALOG_CLOSE_BUTTON); return; } /* Check the user has put in a subject line and body. Otherwise * just return which redisplays the form. */ subject = ml_form_input_get_value (w->subject); body = ml_form_input_get_value (w->body); if (!subject || strlen (subject) == 0 || !body || strlen (body) == 0) return; switch (ml_form_select_get_selection (w->body_type)) { case 0: body_type = 'p'; break; default: body_type = 's'; break; case 2: body_type = 'h'; break; } dbh = get_db_handle (w->conninfo, DBI_THROW_ERRORS); if (args->operation == OP_POST || args->operation == OP_REPLY_IN_PUBLIC) { /* Insert the article into the database. */ sth = st_prepare_cached (dbh, "insert into ml_discussion_article " "(resid, parent, subject, author, body, body_type, original_ip) " "values (?, ?, ?, ?, ?, ?, ?)", DBI_INT, DBI_INT_OR_NULL, DBI_STRING, DBI_INT_OR_NULL, DBI_STRING, DBI_CHAR, DBI_STRING); st_execute (sth, w->resid, args->artid, subject, userid, body, body_type, ml_session_get_peernamestr (session)); } else if (args->operation == OP_SUPERSEDE) { /* Replace an existing article in the database. */ sth = st_prepare_cached (dbh, "update ml_discussion_article " " set subject = ?, body = ?, body_type = ?, original_ip = ? " "where id = ? and author = ?", DBI_STRING, DBI_STRING, DBI_CHAR, DBI_STRING, DBI_INT, DBI_INT); st_execute (sth, subject, body, body_type, ml_session_get_peernamestr (session), args->artid, userid); } else if (args->operation == OP_REPLY_IN_PRIVATE) { const char *to_username, *to_email; const char *from_username, *from_email; io_handle sendmail; const char *sendmail_cmd = "/usr/sbin/sendmail -t -i"; /* Get the original article username and email address. */ sth = st_prepare_cached (dbh, "select a.id, u.username, u.email " "from ml_discussion_article a " " left outer join ml_users u on a.author = u.userid " "where a.id = ?", DBI_INT); st_execute (sth, args->artid); st_bind (sth, 1, to_username, DBI_STRING); st_bind (sth, 2, to_email, DBI_STRING); if (!st_fetch (sth) || !to_username || !to_email) { ml_error_window (w->pool, w->session, "Could not retrieve the article author from the database.", ML_DIALOG_CLOSE_BUTTON); return; } /* Get the logged-in username and email address. */ sth = st_prepare_cached (dbh, "select username, email from ml_users where userid = ?", DBI_INT); st_execute (sth, userid); st_bind (sth, 0, from_username, DBI_STRING); st_bind (sth, 1, from_email, DBI_STRING); if (!st_fetch (sth) || !from_username || !from_email) { ml_error_window (w->pool, w->session, "Could not retrieve your username or email address " "from the database.", ML_DIALOG_CLOSE_BUTTON); return; } /* Clean up the usernames and email addresses so we can safely * include them in the email. */ to_username = clean_up_string (w->pool, to_username); to_email = clean_up_string (w->pool, to_email); from_username = clean_up_string (w->pool, from_username); from_email = clean_up_string (w->pool, from_email); /* Similarly clean up the subject line. */ subject = clean_up_string (w->pool, subject); /* Oh dear. Sending an HTML-format email with no text alternative * is *not* very clever. XXX */ sendmail = io_popen (sendmail_cmd, "w"); if (!sendmail) pth_die ("could not invoke sendmail"); io_fprintf (sendmail, "X-Monolith-Trace: %s %s %s\n" "From: %s <%s>\n" "To: %s <%s>\n" "Subject: %s\n" "Content-Type: text/html\n" /* Charset XXX */ "\n", ml_session_get_peernamestr (session), ml_session_host_header (session), ml_session_canonical_path (session), from_username, from_email, to_username, to_email, subject); io_fputs (body, sendmail); io_pclose (sendmail); } else { abort (); /* Unknown operation. */ } db_commit (dbh); put_db_handle (dbh); /* Confirmation page. */ ml_ok_window (w->pool, session, "The article was sent.", ML_DIALOG_CLOSE_BUTTON | ML_DIALOG_CLOSE_RELOAD_OPENER); } /* Remove CRs and LFs from the string. */ static const char * clean_up_string (pool pool, const char *text) { if (strpbrk (text, "\n\r")) { char *copy = pstrdup (pool, text); char *t = copy; while ((t = strpbrk (t, "\n\r")) != 0) *t++ = ' '; return copy; } else return text; /* String is safe. */ }