1 /* Monolith login widget (email validation, no password).
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_login_nopw.c,v 1.8 2003/02/22 15:34:32 rich Exp $
40 #include <pthr_iolib.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"
52 #include "ml_form_text.h"
53 #include "ml_form_submit.h"
54 #include "ml_dialog.h"
55 #include "ml_login_nopw.h"
57 static void repaint (void *, ml_session, const char *, io_handle);
59 struct ml_widget_operations login_nopw_ops =
66 struct ml_widget_operations *ops;
67 pool pool; /* Pool for allocations. */
68 ml_session session; /* Current session. */
69 const char *conninfo; /* Database connection. */
71 /* The following is displayed when no user is logged in: */
74 /* The following is displayed when a user is logged in: */
76 ml_text_label in_name;
78 /* Form input for login form. */
79 ml_form_text email_input;
81 /* The email callback action - we need to unregister this before it
82 * is re-registered, for security reasons.
86 /* The email address which is trying to be registered. */
89 /* Secret string sent in the email. */
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 *);
100 new_ml_login_nopw (pool pool, ml_session session, const char *conninfo)
102 ml_login_nopw w = pmalloc (pool, sizeof *w);
105 w->ops = &login_nopw_ops;
107 w->session = session;
108 w->conninfo = conninfo;
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);
119 /* Generate the widget which is displayed when a user is logged in: */
120 w->in = new_ml_flow_layout (pool);
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);
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);
134 login_button (ml_session session, void *vw)
136 ml_login_nopw w = (ml_login_nopw) vw;
141 ml_form_submit submit;
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);
148 ml_form_set_callback (form, login_send_email, session, w);
149 ml_widget_set_property (form, "method", "GET");
151 text = new_ml_text_label
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);
164 w->email_input = new_ml_form_text (w->pool, form);
165 ml_table_layout_pack (tbl, w->email_input, 1, 0);
167 submit = new_ml_form_submit (w->pool, form, "Submit");
168 ml_table_layout_pack (tbl, submit, 1, 1);
170 ml_form_pack (form, tbl);
171 ml_window_pack (win, form);
174 static inline const char *
175 generate_secret (pool pool)
178 unsigned char buffer[16];
179 char *secret = pmalloc (pool, 33 * sizeof (char));
181 fd = open ("/dev/urandom", O_RDONLY);
182 if (fd == -1) abort ();
183 if (read (fd, buffer, 16) != 16) abort ();
186 for (i = 0; i < 16; ++i)
187 sprintf (secret + i*2, "%02x", buffer[i]);
192 static const char *clean_up_string (pool pool, const char *text);
195 login_send_email (ml_session session, void *vw)
197 ml_login_nopw w = (ml_login_nopw) vw;
198 const char *email_c, *windowid;
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);
208 /* Create the window, and get the windowid which is passed in the
211 win = new_ml_window (session, w->pool);
212 windowid = _ml_window_get_windowid (win);
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);
220 if (strlen (email) == 0) return;
221 if (strchr (email, '@') == 0) return;
222 w->email = clean_up_string (w->pool, email);
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
232 w->secret = generate_secret (w->pool);
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
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
245 if (w->actionid) ml_unregister_action (session, w->actionid);
247 /* Register a callback action. */
248 w->actionid = ml_register_action (session, login, w);
251 sendmail = io_popen (sendmail_cmd, "w");
253 pth_die ("could not invoke sendmail");
257 "X-Monolith-Trace: %s %s %s\n"
258 "From: DO NOT REPLY TO THIS EMAIL <do_not_reply@annexia.org>\n"
260 "Subject: Email validation from %s\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"
266 "To complete the log in to the site, you need to visit the link\n"
269 "http://%s%s?ml_window=%s&ml_action=%s&secret=%s\n"
271 "Do not reply to this email!\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"
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,
285 io_pclose (sendmail);
287 /* Display a confirmation page. */
288 dlg = new_ml_dialog (w->pool);
291 "A confirmation email was sent.\n\n"
292 "Click on the link in the email to complete your site registration / "
294 ml_dialog_add_close_button (dlg, "Close window", 0);
297 /* Remove CRs and LFs from the string. */
299 clean_up_string (pool pool, const char *text)
301 if (strpbrk (text, "\n\r"))
303 char *copy = pstrdup (pool, text);
306 while ((t = strpbrk (t, "\n\r")) != 0)
312 return text; /* String is safe. */
316 login (ml_session session, void *vw)
318 ml_login_nopw w = (ml_login_nopw) vw;
327 /* Before proceeding, check the secret. */
328 submitted_args = _ml_session_submitted_args (session);
329 secret = cgi_param (submitted_args, "secret");
331 if (!secret || strlen (secret) == 0 ||
332 !w->secret || strcmp (secret, w->secret) != 0)
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);
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);
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);
351 sth = st_prepare_cached
353 "select userid from ml_users where email = ?", DBI_STRING);
354 st_execute (sth, w->email);
356 st_bind (sth, 0, userid, DBI_INT);
358 if (st_fetch (sth)) /* Existing account. */
360 sth = st_prepare_cached
362 "update ml_users set lastlogin_date = current_date, "
363 "nr_logins = nr_logins + 1 where userid = ?", DBI_INT);
364 st_execute (sth, userid);
366 else /* New account. */
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 '@'.
374 username = pstrdup (w->pool, w->email);
375 if (!(t = strchr (username, '@'))) abort ();
378 sth = st_prepare_cached
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);
385 /* Get the userid. */
386 userid = st_serial (sth, "ml_users_userid_seq");
389 /* Commit changes to the database. */
392 ml_session_login (session, userid, "/", "+1y");
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);
400 "You are now logged into this site.");
401 ml_window_pack (win, dlg);
405 logout_button (ml_session session, void *vw)
407 //ml_login_nopw w = (ml_login_nopw) vw;
409 ml_session_logout (session, "/");
413 repaint (void *vw, ml_session session, const char *windowid, io_handle io)
415 ml_login_nopw w = (ml_login_nopw) vw;
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.
423 userid = ml_session_userid (session);
425 if (!userid) /* Not logged in. */
427 ml_widget_repaint (w->out, session, windowid, io);
429 else /* Logged in. */
433 const char *email = 0;
435 /* Update the current user information string. */
436 dbh = get_db_handle (w->conninfo, DBI_THROW_ERRORS);
438 sth = st_prepare_cached
440 "select email from ml_users where userid = ?",
442 st_execute (sth, userid);
444 st_bind (sth, 0, email, DBI_STRING);
446 st_fetch (sth); /* XXX or die ... */
448 ml_widget_set_property (w->in_name, "text", email);
451 ml_widget_repaint (w->in, session, windowid, io);