Add to git.
[monolith.git] / widgets / ml_bulletins.c
1 /* Monolith bulletins (recent news spool).
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_bulletins.c,v 1.12 2003/02/22 15:34:32 rich Exp $
19  */
20
21 #include "config.h"
22
23 #include <stdio.h>
24 #include <stdlib.h>
25
26 #ifdef HAVE_STRING_H
27 #include <string.h>
28 #endif
29
30 #include <pool.h>
31 #include <pstring.h>
32 #include <pre.h>
33 #include <pthr_iolib.h>
34
35 #include "monolith.h"
36 #include "ml_widget.h"
37 #include "ml_button.h"
38 #include "ml_window.h"
39 #include "ml_table_layout.h"
40 #include "ml_form_layout.h"
41 #include "ml_text_label.h"
42 #include "ml_dialog.h"
43 #include "ml_form.h"
44 #include "ml_form_input.h"
45 #include "ml_form_textarea.h"
46 #include "ml_form_select.h"
47 #include "ml_form_text.h"
48 #include "ml_form_submit.h"
49 #include "ml_bulletins.h"
50
51 static void repaint (void *, ml_session, const char *, io_handle);
52
53 struct ml_widget_operations bulletins_ops =
54   {
55     repaint: repaint
56   };
57
58 struct ml_bulletins
59 {
60   struct ml_widget_operations *ops;
61   pool pool;                    /* Pool for allocations. */
62   ml_session session;           /* Current session. */
63   const char *conninfo;         /* Database connection. */
64   int sectionid;                /* Which section? */
65   int first_item;               /* First item to display. */
66   int nr_items;                 /* Number of items to display on each page. */
67   ml_button post, home, prev, next; /* Buttons along the bottom. */
68
69   /* These are used during posting. */
70   ml_form_textarea post_item;
71   ml_form_select post_type;
72   ml_form_text post_link;
73   ml_form_text post_link_text;
74 };
75
76 static int can_post (db_handle dbh, int sectionid, int userid);
77 static void post (ml_session, void *vw);
78 static void update_buttons (ml_bulletins w);
79 static void home_button (ml_session, void *vw);
80 static void prev_button (ml_session, void *vw);
81 static void next_button (ml_session, void *vw);
82 static void post_button (ml_session, void *vw);
83
84 ml_bulletins
85 new_ml_bulletins (pool pool, ml_session session, const char *conninfo,
86                   const char *section_name)
87 {
88   ml_bulletins w = pmalloc (pool, sizeof *w);
89   db_handle dbh;
90   st_handle sth;
91
92   w->ops = &bulletins_ops;
93   w->pool = pool;
94   w->session = session;
95   w->conninfo = conninfo;
96   w->first_item = 0;
97   w->nr_items = 10;
98
99   /* Get the sectionid. */
100   dbh = get_db_handle (conninfo, DBI_THROW_ERRORS);
101
102   sth = st_prepare_cached
103     (dbh,
104      "select resid from ml_resources where name = ?", DBI_STRING);
105   st_execute (sth, section_name);
106
107   st_bind (sth, 0, w->sectionid, DBI_INT);
108
109   if (!st_fetch (sth)) return 0;
110
111   put_db_handle (dbh);
112
113   /* Create the buttons for the bottom of the page. The home/prev/next
114    * buttons get enabled (possibly) in update_buttons. The post button
115    * is always enabled, but only shown to eligible posters.
116    */
117   w->post = new_ml_button (pool, "Post");
118   ml_button_set_callback (w->post, post_button, session, w);
119   ml_button_set_popup (w->post, "bulletins_post_window");
120   ml_button_set_popup_size (w->post, 400, 300);
121
122   w->home = new_ml_button (pool, "Most recent");
123   w->prev = new_ml_button (pool, "&lt;&lt;");
124   w->next = new_ml_button (pool, "&gt;&gt;");
125   update_buttons (w);
126
127   return w;
128 }
129
130 /* Callback for the "home" button. */
131 static void
132 home_button (ml_session session, void *vw)
133 {
134   ml_bulletins w = (ml_bulletins) vw;
135
136   w->first_item = 0;
137   update_buttons (w);
138 }
139
140 /* Callback for the "prev" button. */
141 static void
142 prev_button (ml_session session, void *vw)
143 {
144   ml_bulletins w = (ml_bulletins) vw;
145
146   w->first_item -= w->nr_items;
147   if (w->first_item < 0) w->first_item = 0;
148   update_buttons (w);
149 }
150
151 /* Callback for the "next" button. */
152 static void
153 next_button (ml_session session, void *vw)
154 {
155   ml_bulletins w = (ml_bulletins) vw;
156
157   w->first_item += w->nr_items;
158   update_buttons (w);
159 }
160
161 static inline void
162 enable_home (ml_bulletins w)
163 {
164   ml_button_set_callback (w->home, home_button, w->session, w);
165 }
166
167 static inline void
168 disable_home (ml_bulletins w)
169 {
170   ml_button_set_callback (w->home, 0, w->session, 0);
171 }
172
173 static inline void
174 enable_prev (ml_bulletins w)
175 {
176   ml_button_set_callback (w->prev, prev_button, w->session, w);
177 }
178
179 static inline void
180 disable_prev (ml_bulletins w)
181 {
182   ml_button_set_callback (w->prev, 0, w->session, 0);
183 }
184
185 static inline void
186 enable_next (ml_bulletins w)
187 {
188   ml_button_set_callback (w->next, next_button, w->session, w);
189 }
190
191 static inline void
192 disable_next (ml_bulletins w)
193 {
194   ml_button_set_callback (w->next, 0, w->session, 0);
195 }
196
197 /* This function updates the state of each button. It consults the
198  * database to find out how many articles are present.
199  */
200 static void
201 update_buttons (ml_bulletins w)
202 {
203   db_handle dbh;
204   st_handle sth;
205   int count;
206
207   /* Get a database handle. */
208   dbh = get_db_handle (w->conninfo, DBI_THROW_ERRORS);
209
210   /* Find out how many articles are present. */
211   sth = st_prepare_cached
212     (dbh, "select count (*) from ml_bulletins");
213   st_execute (sth);
214
215   st_bind (sth, 0, count, DBI_INT);
216   if (!st_fetch (sth))
217     pth_die ("select count(*) returned no rows!");
218
219   /* Make sure first_item is sensible. */
220   if (w->first_item >= count)
221     w->first_item = count - w->nr_items;
222   if (w->first_item < 0)
223     w->first_item = 0;
224
225   /* Decide which buttons to enable. */
226   if (w->first_item > w->nr_items)
227     enable_home (w);
228   else
229     disable_home (w);
230
231   if (w->first_item >= w->nr_items)
232     enable_prev (w);
233   else
234     disable_prev (w);
235
236   if (w->first_item + w->nr_items < count)
237     enable_next (w);
238   else
239     disable_next (w);
240 }
241
242 static void
243 post_button (ml_session session, void *vw)
244 {
245   ml_bulletins w = (ml_bulletins) vw;
246   db_handle dbh;
247   ml_window win;
248   ml_form_layout tbl;
249   ml_form form;
250   ml_form_submit sub;
251
252   /* Get a database handle. */
253   dbh = get_db_handle (w->conninfo, DBI_THROW_ERRORS);
254
255   /* Is the current user allowed to post? It can happen that this
256    * function is called even if the user is not a legitimate poster.
257    * For example:
258    * (1) User displays the front page, with "Post" button, then logs
259    *     out in another window, then presses the "Post" button.
260    * (2) User is attempting to predict action IDs (note that the "Post"
261    *     button is registered, even if it not displayed).
262    * It's always a good idea to check permissions inside callback
263    * functions.
264    */
265   if (!can_post (dbh, w->sectionid, ml_session_userid (session)))
266     {
267       ml_error_window
268         (w->pool, session,
269          "You do not have permission to post in this section.",
270          ML_DIALOG_CLOSE_BUTTON);
271       return;
272     }
273
274   /* Display the posting form. */
275   win = new_ml_window (session, w->pool);
276   form = new_ml_form (w->pool);
277   ml_form_set_callback (form, post, session, w);
278   ml_widget_set_property (form, "method", "GET");
279   tbl = new_ml_form_layout (w->pool);
280
281   w->post_item = new_ml_form_textarea (w->pool, form, 3, 40);
282   ml_form_layout_pack (tbl, "Message:", w->post_item);
283
284   w->post_type = new_ml_form_select (w->pool, form);
285   ml_form_select_push_back (w->post_type, "Plain text");
286   ml_form_select_push_back (w->post_type, "*Smart* text");
287   ml_form_select_push_back (w->post_type, "HTML");
288   ml_form_select_set_selection (w->post_type, 1); /* XXX From preferences. */
289   ml_form_layout_pack (tbl, 0, w->post_type);
290
291   w->post_link = new_ml_form_text (w->pool, form);
292   ml_form_layout_pack (tbl, "URL:", w->post_link);
293
294   w->post_link_text = new_ml_form_text (w->pool, form);
295   ml_form_layout_pack (tbl, "Link text:", w->post_link_text);
296
297   sub = new_ml_form_submit (w->pool, form, "Post");
298   ml_form_layout_pack (tbl, 0, sub);
299
300   /* Pack everything up. */
301   ml_form_pack (form, tbl);
302   ml_window_pack (win, form);
303 }
304
305 static void
306 post (ml_session session, void *vw)
307 {
308   ml_bulletins w = (ml_bulletins) vw;
309   db_handle dbh;
310   st_handle sth;
311   const char *item, *item_type, *link, *link_text;
312   int type, userid;
313
314   /* Verify the details of the posting, otherwise do nothing, which just
315    * represents the form back to the user.
316    */
317   item = ml_form_input_get_value (w->post_item);
318   link = ml_form_input_get_value (w->post_link);
319   link_text = ml_form_input_get_value (w->post_link_text);
320
321   if (!item || strlen (item) == 0) return;
322
323   type = ml_form_select_get_selection (w->post_type);
324
325   /* Turn empty strings into nulls for the database. */
326   if (strlen (link) == 0) link = 0;
327   if (strlen (link_text) == 0) link_text = 0;
328
329   /* Set the item type. */
330   switch (type) {
331   case 0: item_type = "p"; break;
332   case 1: item_type = "s"; break;
333   case 2: item_type = "h"; break;
334   default: item_type = "s";
335   }
336
337   /* Get a database handle. */
338   dbh = get_db_handle (w->conninfo, DBI_THROW_ERRORS);
339
340   /* Verify the user can post. See notes above. */
341   userid = ml_session_userid (session);
342   if (!can_post (dbh, w->sectionid, userid))
343     {
344       ml_error_window
345         (w->pool, session,
346          "You do not have permission to post in this section.",
347          ML_DIALOG_CLOSE_BUTTON);
348       return;
349     }
350
351   /* Insert the posting. */
352   sth = st_prepare_cached
353     (dbh,
354      "insert into ml_bulletins "
355      "(sectionid, authorid, item, item_type, link, link_text) "
356      "values (?, ?, ?, ?, ?, ?)",
357      DBI_INT, DBI_INT, DBI_STRING, DBI_STRING, DBI_STRING, DBI_STRING);
358   st_execute (sth, w->sectionid, userid, item, item_type, link, link_text);
359
360   /* Commit to the database. */
361   db_commit (dbh);
362   put_db_handle (dbh);
363
364   /* Present a confirmation page. */
365   ml_ok_window (w->pool, session,
366                 "Item was successfully posted.",
367                 ML_DIALOG_CLOSE_BUTTON | ML_DIALOG_CLOSE_RELOAD_OPENER);
368 }
369
370 static inline void
371 show_item (io_handle io, int n, char *item, char *item_type, char *username,
372            char *posted_date, char *timediff, char *link, char *link_text)
373 {
374   /* XXX Lots of issues in this block:
375    * (2) parsing/printing of dates
376    * (3) smart text support
377    * (4) escaping of link text
378    * (5) styling of the whole thing
379    */
380   io_fprintf (io, "<table width=\"100%%\"><tr>"
381               "<td rowspan=\"3\" valign=\"top\">%d.</td>",
382               n);
383   io_fprintf (io, "<td>Posted by <strong>%s</strong> on "
384               "<strong>%s</strong></td></tr>",
385               username, posted_date);
386   io_fprintf (io, "<tr><td>%s</td></tr>", item);
387   if (link && link_text)
388     io_fprintf (io, "<tr><td align=\"right\">"
389                 "<a href=\"%s\">%s</a></td></tr>",
390                 link, link_text);
391   else if (link)
392     io_fprintf (io, "<tr><td align=\"right\">"
393                 "<a href=\"%s\">%s</a></td></tr>",
394                 link, link);
395   else
396     io_fprintf (io, "<tr><td></td></tr>");
397   io_fprintf (io, "</table>");
398 }
399
400 static void
401 repaint (void *vw, ml_session session, const char *windowid, io_handle io)
402 {
403   ml_bulletins w = (ml_bulletins) vw;
404   db_handle dbh;
405   st_handle sth;
406   char *item, *item_type, *username, *posted_date, *timediff,
407     *link, *link_text;
408   int n, is_poster;
409
410   /* Get a database handle. */
411   dbh = get_db_handle (w->conninfo, DBI_THROW_ERRORS);
412
413   /* Is the current user allowed to post/remove articles? */
414   is_poster = can_post (dbh, w->sectionid, ml_session_userid (session));
415
416   /* Pull out the headlines. */
417   sth = st_prepare_cached
418     (dbh,
419      "select b.item, b.item_type, u.username, b.posted_date, "
420      "       current_timestamp - b.posted_date, "
421      "       b.link, b.link_text "
422      "from ml_bulletins b, ml_users u "
423      "where b.sectionid = ? and b.authorid = u.userid "
424      "order by 4 desc "
425      "limit ? "
426      "offset ?",
427      DBI_INT, DBI_INT, DBI_INT);
428   st_execute (sth, w->sectionid, w->nr_items, w->first_item);
429
430   st_bind (sth, 0, item, DBI_STRING);
431   st_bind (sth, 1, item_type, DBI_STRING);
432   st_bind (sth, 2, username, DBI_STRING);
433   st_bind (sth, 3, posted_date, DBI_STRING);
434   st_bind (sth, 4, timediff, DBI_STRING);
435   st_bind (sth, 5, link, DBI_STRING);
436   st_bind (sth, 6, link_text, DBI_STRING);
437
438   /* Display them. */
439   io_fprintf (io, "<table><tr><td><table>");
440
441   n = w->first_item + 1;
442
443   while (st_fetch (sth))
444     {
445       io_fprintf (io, "<tr><td>");
446
447       show_item (io, n, item, item_type, username, posted_date, timediff,
448                  link, link_text);
449
450       io_fprintf (io, "</td></tr>");
451
452       n++;
453     }
454
455   /* Finish off the page with the buttons at the bottom. */
456   io_fprintf (io, "</table></td></tr><tr><td align=\"right\">");
457   if (is_poster)
458     ml_widget_repaint (w->post, session, windowid, io);
459   ml_widget_repaint (w->home, session, windowid, io);
460   ml_widget_repaint (w->prev, session, windowid, io);
461   ml_widget_repaint (w->next, session, windowid, io);
462   io_fprintf (io, "</td></tr></table>");
463
464   /* Be polite: give back the database handle. */
465   put_db_handle (dbh);
466 }
467
468 static int
469 can_post (db_handle dbh, int sectionid, int userid)
470 {
471   st_handle sth;
472
473   if (userid)
474     {
475       sth = st_prepare_cached
476         (dbh,
477          "select 1 from ml_bulletins_posters "
478          "where sectionid = ? and userid = ?", DBI_INT, DBI_INT);
479       st_execute (sth, sectionid, userid);
480
481       return st_fetch (sth);
482     }
483   else
484     return 0;
485 }