Add to git.
[monolith.git] / widgets / ml_login_nopw.c
1 /* Monolith login widget (email validation, no password).
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_login_nopw.c,v 1.8 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 #ifdef HAVE_UNISTD_H
31 #include <unistd.h>
32 #endif
33
34 #ifdef HAVE_FCNTL_H
35 #include <fcntl.h>
36 #endif
37
38 #include <pool.h>
39 #include <pstring.h>
40 #include <pthr_iolib.h>
41 #include <pthr_dbi.h>
42 #include <pthr_cgi.h>
43
44 #include "monolith.h"
45 #include "ml_widget.h"
46 #include "ml_flow_layout.h"
47 #include "ml_table_layout.h"
48 #include "ml_text_label.h"
49 #include "ml_button.h"
50 #include "ml_window.h"
51 #include "ml_form.h"
52 #include "ml_form_text.h"
53 #include "ml_form_submit.h"
54 #include "ml_dialog.h"
55 #include "ml_login_nopw.h"
56
57 static void repaint (void *, ml_session, const char *, io_handle);
58
59 struct ml_widget_operations login_nopw_ops =
60   {
61     repaint: repaint
62   };
63
64 struct ml_login_nopw
65 {
66   struct ml_widget_operations *ops;
67   pool pool;                    /* Pool for allocations. */
68   ml_session session;           /* Current session. */
69   const char *conninfo;         /* Database connection. */
70
71   /* The following is displayed when no user is logged in: */
72   ml_button out;
73
74   /* The following is displayed when a user is logged in: */
75   ml_flow_layout in;
76   ml_text_label in_name;
77
78   /* Form input for login form. */
79   ml_form_text email_input;
80
81   /* The email callback action - we need to unregister this before it
82    * is re-registered, for security reasons.
83    */
84   const char *actionid;
85
86   /* The email address which is trying to be registered. */
87   const char *email;
88
89   /* Secret string sent in the email. */
90   const char *secret;
91 };
92
93 /* Callback function prototypes. */
94 static void login_button (ml_session, void *);
95 static void login_send_email (ml_session, void *);
96 static void login (ml_session, void *);
97 static void logout_button (ml_session, void *);
98
99 ml_login_nopw
100 new_ml_login_nopw (pool pool, ml_session session, const char *conninfo)
101 {
102   ml_login_nopw w = pmalloc (pool, sizeof *w);
103   ml_button button;
104
105   w->ops = &login_nopw_ops;
106   w->pool = pool;
107   w->session = session;
108   w->conninfo = conninfo;
109   w->actionid = 0;
110   w->email = 0;
111   w->secret = 0;
112
113   /* Generate the widget which is displayed when no user is logged in: */
114   w->out = new_ml_button (pool, "Login ...");
115   ml_button_set_callback (w->out, login_button, session, w);
116   ml_button_set_popup (w->out, "login_nopw_window");
117   ml_button_set_popup_size (w->out, 400, 300);
118
119   /* Generate the widget which is displayed when a user is logged in: */
120   w->in = new_ml_flow_layout (pool);
121
122   w->in_name = new_ml_text_label (pool, 0);
123   ml_widget_set_property (w->in_name, "font.weight", "bold");
124   ml_flow_layout_pack (w->in, w->in_name);
125
126   button = new_ml_button (pool, "Logout");
127   ml_button_set_callback (button, logout_button, session, w);
128   ml_flow_layout_pack (w->in, button);
129
130   return w;
131 }
132
133 static void
134 login_button (ml_session session, void *vw)
135 {
136   ml_login_nopw w = (ml_login_nopw) vw;
137   ml_window win;
138   ml_form form;
139   ml_table_layout tbl;
140   ml_text_label text;
141   ml_form_submit submit;
142
143   /* Create the login window. */
144   win = new_ml_window (session, w->pool);
145   form = new_ml_form (w->pool);
146   tbl = new_ml_table_layout (w->pool, 2, 2);
147
148   ml_form_set_callback (form, login_send_email, session, w);
149   ml_widget_set_property (form, "method", "GET");
150
151   text = new_ml_text_label
152     (w->pool,
153      "To log in to this site, or to create a user account, please type "
154      "your email address in the box below.\n\n"
155      "You will be sent a single confirmation email which contains a "
156      "special link that gives you access to the site.\n\n"
157      "Your email address is stored in our database, but you will not "
158      "receive any further emails, nor will your email address be shared "
159      "with others or displayed on the site (unless you specifically "
160      "ask for this in your settings).\n\n");
161   ml_table_layout_pack (tbl, text, 0, 0);
162   ml_table_layout_set_colspan (tbl, 0, 0, 2);
163
164   w->email_input = new_ml_form_text (w->pool, form);
165   ml_table_layout_pack (tbl, w->email_input, 1, 0);
166
167   submit = new_ml_form_submit (w->pool, form, "Submit");
168   ml_table_layout_pack (tbl, submit, 1, 1);
169
170   ml_form_pack (form, tbl);
171   ml_window_pack (win, form);
172 }
173
174 static inline const char *
175 generate_secret (pool pool)
176 {
177   int fd, i;
178   unsigned char buffer[16];
179   char *secret = pmalloc (pool, 33 * sizeof (char));
180
181   fd = open ("/dev/urandom", O_RDONLY);
182   if (fd == -1) abort ();
183   if (read (fd, buffer, 16) != 16) abort ();
184   close (fd);
185
186   for (i = 0; i < 16; ++i)
187     sprintf (secret + i*2, "%02x", buffer[i]);
188
189   return secret;
190 }
191
192 static const char *clean_up_string (pool pool, const char *text);
193
194 static void
195 login_send_email (ml_session session, void *vw)
196 {
197   ml_login_nopw w = (ml_login_nopw) vw;
198   const char *email_c, *windowid;
199   char *email;
200   io_handle sendmail;
201   const char *sendmail_cmd
202     = "/usr/sbin/sendmail -t -i -f do_not_reply@annexia.org";
203   const char *site = ml_session_host_header (session);
204   const char *canonical_path = ml_session_canonical_path (session);
205   ml_window win;
206   ml_dialog dlg;
207
208   /* Create the window, and get the windowid which is passed in the
209    * email.
210    */
211   win = new_ml_window (session, w->pool);
212   windowid = _ml_window_get_windowid (win);
213
214   /* Get the email address which was typed in, and tidy it up a bit. */
215   email_c = ml_form_input_get_value (w->email_input);
216   if (!email_c) return;
217   email = pstrdup (w->pool, email_c);
218   ptrim (email);
219   pstrlwr (email);
220   if (strlen (email) == 0) return;
221   if (strchr (email, '@') == 0) return;
222   w->email = clean_up_string (w->pool, email);
223
224   /* Action IDs are predictable, so if all we did was to send back
225    * an action ID, then this would not offer security, since someone
226    * could guess the next action ID, and thus register as another
227    * user. Thus we also generate and send back a secret random string,
228    * and in the login() function we check that the secret string
229    * was passed back to us, proving that the user really received
230    * the email.
231    */
232   w->secret = generate_secret (w->pool);
233
234   /* Unregister old actionid, if there was one. For security reasons, since
235    * otherwise a user would be able to register as anyone in the following
236    * way:
237    * (1) Request registration for realname@example.com
238    * (2) Request registration for rich@annexia.org
239    * (3) Click confirmation of email from (1)
240    * (4) Registered as 'rich@annexia.org'!
241    * By unregistering the action, we remove this possibility (hopefully,
242    * at least, but if anyone else can think of a way around this, please
243    * tell me).
244    */
245   if (w->actionid) ml_unregister_action (session, w->actionid);
246
247   /* Register a callback action. */
248   w->actionid = ml_register_action (session, login, w);
249
250   /* Run sendmail. */
251   sendmail = io_popen (sendmail_cmd, "w");
252   if (!sendmail)
253     pth_die ("could not invoke sendmail");
254
255   io_fprintf
256     (sendmail,
257      "X-Monolith-Trace: %s %s %s\n"
258      "From: DO NOT REPLY TO THIS EMAIL <do_not_reply@annexia.org>\n"
259      "To: %s\n"
260      "Subject: Email validation from %s\n"
261      "\n"
262      "Someone, possibly you, typed your email address into %s.\n"
263      "If this was not you, then our sincere apologies; please ignore this\n"
264      "e-mail.\n"
265      "\n"
266      "To complete the log in to the site, you need to visit the link\n"
267      "below:\n"
268      "\n"
269      "http://%s%s?ml_window=%s&ml_action=%s&secret=%s\n"
270      "\n"
271      "Do not reply to this email!\n"
272      "\n"
273      "Notes for geeks:\n"
274      "\n"
275      "1. You need to use the same browser to fetch this URL. So 'wget'\n"
276      "   won't work. Instead, paste the URL if you cannot click on it\n"
277      "   directly.\n"
278      "2. This URL is valid until your current session ends.\n",
279      ml_session_get_peernamestr (session),
280      ml_session_host_header (session),
281      ml_session_canonical_path (session),
282      email, site, site, site, canonical_path, windowid, w->actionid,
283      w->secret);
284
285   io_pclose (sendmail);
286
287   /* Display a confirmation page. */
288   dlg = new_ml_dialog (w->pool);
289   ml_dialog_set_text
290     (dlg,
291      "A confirmation email was sent.\n\n"
292      "Click on the link in the email to complete your site registration / "
293      "login.");
294   ml_dialog_add_close_button (dlg, "Close window", 0);
295 }
296
297 /* Remove CRs and LFs from the string. */
298 static const char *
299 clean_up_string (pool pool, const char *text)
300 {
301   if (strpbrk (text, "\n\r"))
302     {
303       char *copy = pstrdup (pool, text);
304       char *t = copy;
305
306       while ((t = strpbrk (t, "\n\r")) != 0)
307         *t++ = ' ';
308
309       return copy;
310     }
311   else
312     return text;                /* String is safe. */
313 }
314
315 static void
316 login (ml_session session, void *vw)
317 {
318   ml_login_nopw w = (ml_login_nopw) vw;
319   cgi submitted_args;
320   const char *secret;
321   db_handle dbh;
322   st_handle sth;
323   int userid;
324   ml_window win;
325   ml_dialog dlg;
326
327   /* Before proceeding, check the secret. */
328   submitted_args = _ml_session_submitted_args (session);
329   secret = cgi_param (submitted_args, "secret");
330
331   if (!secret || strlen (secret) == 0 ||
332       !w->secret || strcmp (secret, w->secret) != 0)
333     {
334       /* Failed. */
335       /* XXX Eventually ml_dialog will have a close window button. */
336       win = new_ml_window (session, w->pool);
337       dlg = new_ml_dialog (w->pool);
338       ml_dialog_set_text
339         (dlg,
340          "Email validation failed.\n\n"
341          "If you copied and pasted the URL out of an email message, please "
342          "make sure you copied it correctly.");
343       ml_window_pack (win, dlg);
344
345       return;
346     }
347
348   /* Secret is OK. Email address is valid. Log in or create a user account. */
349   dbh = get_db_handle (w->conninfo, DBI_THROW_ERRORS);
350
351   sth = st_prepare_cached
352     (dbh,
353      "select userid from ml_users where email = ?", DBI_STRING);
354   st_execute (sth, w->email);
355
356   st_bind (sth, 0, userid, DBI_INT);
357
358   if (st_fetch (sth))           /* Existing account. */
359     {
360       sth = st_prepare_cached
361         (dbh,
362          "update ml_users set lastlogin_date = current_date, "
363          "nr_logins = nr_logins + 1 where userid = ?", DBI_INT);
364       st_execute (sth, userid);
365     }
366   else                          /* New account. */
367     {
368       char *username, *t;
369
370       /* Extract the username from the email address. Usernames are not
371        * unique (email addresses are), so just use the first part of
372        * the email address, before the first '@'.
373        */
374       username = pstrdup (w->pool, w->email);
375       if (!(t = strchr (username, '@'))) abort ();
376       *t = '\0';
377
378       sth = st_prepare_cached
379         (dbh,
380          "insert into ml_users (email, username, lastlogin_date, nr_logins) "
381          "values (?, ?, current_date, 1)",
382          DBI_STRING, DBI_STRING);
383       st_execute (sth, w->email, username);
384
385       /* Get the userid. */
386       userid = st_serial (sth, "ml_users_userid_seq");
387     }
388
389   /* Commit changes to the database. */
390   db_commit (dbh);
391
392   ml_session_login (session, userid, "/", "+1y");
393
394   /* Success. */
395   /* XXX Eventually ml_dialog will have a close window button. */
396   win = new_ml_window (session, w->pool);
397   dlg = new_ml_dialog (w->pool);
398   ml_dialog_set_text
399     (dlg,
400      "You are now logged into this site.");
401   ml_window_pack (win, dlg);
402 }
403
404 static void
405 logout_button (ml_session session, void *vw)
406 {
407   //ml_login_nopw w = (ml_login_nopw) vw;
408
409   ml_session_logout (session, "/");
410 }
411
412 static void
413 repaint (void *vw, ml_session session, const char *windowid, io_handle io)
414 {
415   ml_login_nopw w = (ml_login_nopw) vw;
416   int userid;
417
418   /* Depending on whether a user is currently logged in or not, we display
419    * different things. This has to happen in the 'repaint' function
420    * because the currently logged in user can change unexpectedly
421    * when other people log in to other applications.
422    */
423   userid = ml_session_userid (session);
424
425   if (!userid)                  /* Not logged in. */
426     {
427       ml_widget_repaint (w->out, session, windowid, io);
428     }
429   else                          /* Logged in. */
430     {
431       db_handle dbh;
432       st_handle sth;
433       const char *email = 0;
434
435       /* Update the current user information string. */
436       dbh = get_db_handle (w->conninfo, DBI_THROW_ERRORS);
437
438       sth = st_prepare_cached
439         (dbh,
440          "select email from ml_users where userid = ?",
441          DBI_INT);
442       st_execute (sth, userid);
443
444       st_bind (sth, 0, email, DBI_STRING);
445
446       st_fetch (sth);           /* XXX or die ... */
447
448       ml_widget_set_property (w->in_name, "text", email);
449
450       /* Repaint. */
451       ml_widget_repaint (w->in, session, windowid, io);
452     }
453 }