Index: database/database.mysql
===================================================================
RCS file: /cvs/drupal/drupal/database/database.mysql,v
retrieving revision 1.174
diff -u -r1.174 database.mysql
--- database/database.mysql	23 Mar 2005 20:36:41 -0000	1.174
+++ database/database.mysql	4 Apr 2005 23:44:35 -0000
@@ -695,6 +695,7 @@
   signature varchar(255) NOT NULL default '',
   created int(11) NOT NULL default '0',
   changed int(11) NOT NULL default '0',
+  lastlogin int(11) NOT NULL default '0',  
   status tinyint(4) NOT NULL default '0',
   timezone varchar(8) default NULL,
   language varchar(12) NOT NULL default '',
Index: database/database.pgsql
===================================================================
RCS file: /cvs/drupal/drupal/database/database.pgsql,v
retrieving revision 1.109
diff -u -r1.109 database.pgsql
--- database/database.pgsql	23 Mar 2005 20:36:41 -0000	1.109
+++ database/database.pgsql	4 Apr 2005 23:44:36 -0000
@@ -693,6 +693,7 @@
   signature varchar(255) NOT NULL default '',
   created integer NOT NULL default '0',
   changed integer NOT NULL default '0',
+  lastlogin integer NOT NULL default '0',
   status smallint NOT NULL default '0',
   timezone varchar(8) default NULL,
   language varchar(12) NOT NULL default '',
Index: database/updates.inc
===================================================================
RCS file: /cvs/drupal/drupal/database/updates.inc,v
retrieving revision 1.100
diff -u -r1.100 updates.inc
--- database/updates.inc	23 Mar 2005 06:12:30 -0000	1.100
+++ database/updates.inc	4 Apr 2005 23:44:42 -0000
@@ -104,7 +104,8 @@
   "2005-02-23" => "update_125",
   "2005-03-03" => "update_126",
   "2005-03-18" => "update_127",
-  "2005-03-21" => "update_128"
+  "2005-03-21" => "update_128",
+  "2005-04-05" => "update_129"
 );
 
 function update_32() {
@@ -2350,4 +2351,20 @@
   return $ret;
 }
 
+function update_129() {
+  $ret = array();
+
+  if ($GLOBALS['db_type'] == 'mysql') {
+    $ret[] = update_sql("ALTER TABLE {users} ADD lastlogin int(11) NOT NULL default '0'");
+  }
+  elseif ($GLOBALS['db_type'] == 'pgsql') {
+    $ret[] = update_sql("ALTER TABLE {users} ADD lastlogin integer");
+    $ret[] = update_sql("ALTER TABLE {users} ALTER COLUMN lastlogin SET NOT NULL");
+    $ret[] = update_sql("ALTER TABLE {users} ALTER COLUMN lastlogin SET DEFAULT '0'");
+    
+  }
+
+  return $ret;
+}
+
 ?>
Index: modules/user.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/user.module,v
retrieving revision 1.453
diff -u -r1.453 user.module
--- modules/user.module	31 Mar 2005 09:25:33 -0000	1.453
+++ modules/user.module	4 Apr 2005 23:44:49 -0000
@@ -392,7 +392,7 @@
     }
     else {
       // Make sure we return the default fields at least
-      $fields = array('uid', 'name', 'pass', 'mail', 'picture', 'mode', 'sort', 'threshold', 'theme', 'signature', 'created', 'changed', 'status', 'timezone', 'language', 'init', 'data');
+      $fields = array('uid', 'name', 'pass', 'mail', 'picture', 'mode', 'sort', 'threshold', 'theme', 'signature', 'created', 'changed', 'lastlogin', 'status', 'timezone', 'language', 'init', 'data');
     }
   }
 
@@ -636,6 +636,8 @@
       'callback' => 'user_page', 'access' => $user->uid == 0 && variable_get('user_register', 1), 'type' => MENU_LOCAL_TASK);
     $items[] = array('path' => 'user/password', 'title' => t('request new password'),
       'callback' => 'user_pass', 'access' => $user->uid == 0, 'type' => MENU_LOCAL_TASK);
+    $items[] = array('path' => 'user/reset', 'title' => t('reset password'),
+      'callback' => 'user_pass_reset', 'access' => $user->uid == 0, 'type' => MENU_CALLBACK);
     $items[] = array('path' => 'user/help', 'title' => t('help'),
       'callback' => 'user_help_page', 'type' => MENU_CALLBACK);
 
@@ -798,7 +800,7 @@
       watchdog('user', t('Session opened for %name.', array('%name' => theme('placeholder', $user->name))));
 
       // Update the user table timestamp noting user has logged in.
-      db_query("UPDATE {users} SET changed = '%d' WHERE uid = '%s'", time(), $user->uid);
+      db_query("UPDATE {users} SET lastlogin = '%d' WHERE uid = '%s'", time(), $user->uid);
 
       user_module_invoke('login', $edit, $user);
 
@@ -911,31 +913,27 @@
   }
   if ($account) {
     $from = variable_get('site_mail', ini_get('sendmail_from'));
-    $pass = user_password();
 
-    // Save new password:
-    user_save($account, array('pass' => $pass));
-
-    // Mail new password:
-    $variables = array('%username' => $account->name, '%site' => variable_get('site_name', 'drupal'), '%password' => $pass, '%uri' => $base_url, '%uri_brief' => substr($base_url, strlen('http://')), '%mailto' => $account->mail, '%date' => format_date(time()), '%login_uri' => url('user', NULL, NULL, TRUE), '%edit_uri' => url('user/'. $account->uid .'/edit', NULL, NULL, TRUE));
+    // Mail one time login URL and instructions.
+    $variables = array('%username' => $account->name, '%site' => variable_get('site_name', 'drupal'), '%login_url' => user_pass_reset_url($account), '%uri' => $base_url, '%uri_brief' => substr($base_url, strlen('http://')), '%mailto' => $account->mail, '%date' => format_date(time()), '%login_uri' => url('user', NULL, NULL, TRUE), '%edit_uri' => url('user/'. $account->uid .'/edit', NULL, NULL, TRUE));
     $subject = _user_mail_text('pass_subject', $variables);
     $body = _user_mail_text('pass_body', $variables);
     $headers = "From: $from\nReply-to: $from\nX-Mailer: Drupal\nReturn-path: $from\nErrors-to: $from";
     $mail_success = user_mail($account->mail, $subject, $body, $headers);
 
     if ($mail_success) {
-      watchdog('user', t('Password mailed to %name at %email.', array('%name' => theme('placeholder', $account->mail))));
-      drupal_set_message(t('Your password and further instructions have been sent to your e-mail address.'));
+      watchdog('user', t('Password reset instructions mailed to %name at %email.', array('%name' => '<em>'. $account->name .'</em>', '%email' => '<em>'. $account->mail .'</em>')));
+      drupal_set_message(t('Further instructions have been sent to your e-mail address.'));
     }
     else {
-      watchdog('user', t('Error mailing password to %name at %email.', array('%name' => theme('placeholder', $account->name), '%email' => theme('placeholder', $account->mail))), WATCHDOG_ERROR);
+      watchdog('user', t('Error mailing password reset instructions to %name at %email.', array('%name' => '<em>'. $account->name .'</em>', '%email' => '<em>'. $account->mail .'</em>')), WATCHDOG_ERROR);
       drupal_set_message(t('Unable to send mail. Please contact the site admin.'));
     }
     drupal_goto('user');
   }
   else {
     if ($edit) {
-      drupal_set_message(t('You must provider either a username or e-mail address.'), 'error');
+      drupal_set_message(t('You must provide either a username or e-mail address.'), 'error');
     }
     // Display form:
     $output = '<p>'. t('Enter your username <strong><em>or</em></strong> your e-mail address.') .'</p>';
@@ -946,6 +944,47 @@
   }
 }
 
+function user_pass_reset($uid, $timestamp, $hashed_pass) {
+  global $user;
+  // Time out, in seconds, until login URL expires. 24 hours = 86400 seconds.
+  $timeout = 86400;
+  $current = time();
+  // Some redundant checks for extra security ?
+  if ($timestamp < $current && is_numeric($uid) && $account = user_load(array('uid' => $uid, 'status' => 1)) ) {
+    // No time out for first time login.  
+    if ($account->lastlogin && $current - $timestamp > $timeout) {
+      drupal_set_message(t('You have tried to use a one time login URL which has expired. Please request a new one using the form below.'));
+      drupal_goto('user/password');
+    }
+    if ($account->uid && !$user->uid && !empty($account) && $timestamp > $account->lastlogin && $timestamp < $current &&
+        $hashed_pass == user_pass_rehash($account->pass, $timestamp, $account->lastlogin)) {
+      watchdog('user', t('One time login URL used for %name with timestamp %timestamp.', array('%name' => "<em>$account->name</em>", '%timestamp' => $timestamp)));
+      // Update the user table noting user has logged in.
+      // And this also makes this hashed password a one-time-only login.
+      db_query("UPDATE {users} SET lastlogin = '%d' WHERE uid = %d", time(), $account->uid);
+      // Now we can set the new user.
+      $user = $account;
+      // And proceed with normal login, going to user page.
+      user_module_invoke('login', $edit, $user);  
+      drupal_set_message(t("You have used a one-time login, which won't be valid anymore."));
+      drupal_set_message(t('Please change your password.'));
+      drupal_goto('user/'. $user->uid .'/edit');
+    }
+  } 
+  // Deny access, no more clues.
+  // Everything will be in the watchdog's URL for the admin to check.
+  drupal_access_denied();
+}
+
+function user_pass_reset_url($account){
+  $timestamp = time();
+  return url("user/reset/$account->uid/$timestamp/".user_pass_rehash($account->pass, $timestamp, $account->lastlogin), NULL, NULL, TRUE);
+}
+
+function user_pass_rehash($password, $timestamp, $last_login){
+  return md5($timestamp . $password . $last_login);
+}
+
 function user_register($edit = array()) {
   global $user, $base_url;
 
@@ -966,7 +1005,7 @@
       $account = user_save('', array_merge(array('name' => $edit['name'], 'pass' => $pass, 'init' => $edit['mail'], 'mail' => $edit['mail'], 'roles' => array(_user_authenticated_id()), 'status' => (variable_get('user_register', 1) == 1 ? 1 : 0)), $edit));
       watchdog('user', t('New user: %name %email.', array('%name' => theme('placeholder', $edit['name']), '%email' => theme('placeholder', '<'. $edit['mail'] .'>'))), WATCHDOG_NOTICE, l(t('edit'), 'user/'. $account->uid .'/edit'));
 
-      $variables = array('%username' => $edit['name'], '%site' => variable_get('site_name', 'drupal'), '%password' => $pass, '%uri' => $base_url, '%uri_brief' => substr($base_url, strlen('http://')), '%mailto' => $edit['mail'], '%date' => format_date(time()), '%login_uri' => url('user', NULL, NULL, TRUE), '%edit_uri' => url('user/'. $account->uid .'/edit', NULL, NULL, TRUE));
+      $variables = array('%username' => $edit['name'], '%site' => variable_get('site_name', 'drupal'), '%password' => $pass, '%uri' => $base_url, '%uri_brief' => substr($base_url, strlen('http://')), '%mailto' => $edit['mail'], '%date' => format_date(time()), '%login_uri' => url('user', NULL, NULL, TRUE), '%edit_uri' => url('user/'. $account->uid .'/edit', NULL, NULL, TRUE), '%login_url' => user_pass_reset_url($account));
 
       // The first user may login immediately, and receives a customized welcome e-mail.
       if ($account->uid == 1) {
@@ -1245,15 +1284,15 @@
       case 'welcome_subject':
         return t('Account details for %username at %site', $variables);
       case 'welcome_body':
-      return t("%username,\n\nThank you for registering at %site. You may now log in to %login_uri using the following username and password:\n\nusername: %username\npassword: %password\n\nAfter logging in, you may wish to change your password at %edit_uri\n\nYour new %site membership also enables to you to login to other Drupal powered websites (e.g. http://www.drop.org/) without registering. Just use the following Drupal ID and password:\n\nDrupal ID: %username@%uri_brief\npassword: %password\n\n\n--  %site team", $variables);
+      return t("%username,\n\nThank you for registering at %site. You may now log in to %login_uri using the following username and password:\n\nusername: %username\npassword: %password\n\nYou may also log in by clicking on this link or copying and pasting it in your browser:\n\n%login_url\n\nThis is a one-time login, so it can be used only once.\n\nAfter logging in, you will be redirected to %edit_uri so you can change your password.\n\nYour new %site membership also enables to you to login to other Drupal powered websites (e.g. http://www.drop.org/) without registering. Just use the following Drupal ID and password:\n\nDrupal ID: %username@%uri_brief\npassword: %password\n\n\n--  %site team", $variables);
       case 'approval_subject':
         return t('Account details for %username at %site (pending admin approval)', $variables);
       case 'approval_body':
-        return t("%username,\n\nThank you for registering at %site. Your application for an account is currently pending approval. Once it has been granted, you may log in to %login_uri using the following username and password:\n\nusername: %username\npassword: %password\n\nAfter logging in, you may wish to change your password at %edit_uri\n\nYour new %site membership also enables to you to login to other Drupal powered websites (e.g. http://www.drop.org/) without registering. Just use the following Drupal ID and password:\n\nDrupal ID: %username@%uri_brief\npassword: %password\n\n\n--  %site team", $variables);
+        return t("%username,\n\nThank you for registering at %site. Your application for an account is currently pending approval. Once it has been granted, you may log in to %login_uri using the following username and password:\n\nusername: %username\npassword: %password\n\nYou may also log in by clicking on this link or copying and pasting it in your browser:\n\n%login_url\n\nThis is a one-time login, so it can be used only once.\n\nAfter logging in, you may wish to change your password at %edit_uri\n\nYour new %site membership also enables to you to login to other Drupal powered websites (e.g. http://www.drop.org/) without registering. Just use the following Drupal ID and password:\n\nDrupal ID: %username@%uri_brief\npassword: %password\n\n\n--  %site team", $variables);
       case 'pass_subject':
-        return t('Replacement login information for %username at %site', $variables);
+        return t('New login information for %username at %site', $variables);
       case 'pass_body':
-        return t("%username,\n\nHere is your new password for %site. You may now login to %login_uri using the following username and password:\n\nusername: %username\npassword: %password\n\nAfter logging in, you may wish to change your password at %edit_uri", $variables);
+        return t("%username,\n\nYou have requested to reset your password for %site.\n\nYou may now log in to %uri_brief clicking on this link or copying and pasting it in your browser:\n\n%login_url\n\nThis is a one-time login, so it can be used only once. It expires after one day.\n\nAfter logging in, you will be redirected to %edit_uri so you can change your password.", $variables);
     }
   }
 }
@@ -1265,12 +1304,12 @@
   $output = form_group(t('User registration settings'), $group);
 
   // User e-mail settings.
-  $group = form_textfield(t('Subject of welcome e-mail'), 'user_mail_welcome_subject', _user_mail_text('welcome_subject'), 70, 180, t('Customize the subject of your welcome e-mail, which is sent to new members upon registering.') .' '. t('Available variables are:') .' %username, %site, %password, %uri, %uri_brief, %mailto, %date, %login_uri, %edit_uri.');
-  $group .= form_textarea(t('Body of welcome e-mail'), 'user_mail_welcome_body', _user_mail_text('welcome_body'), 70, 10, t('Customize the body of the welcome e-mail, which is sent to new members upon registering.') .' '. t('Available variables are:') .' %username, %site, %password, %uri, %uri_brief, %mailto, %login_uri, %edit_uri.');
-  $group .= form_textfield(t('Subject of welcome e-mail (awaiting admin approval)'), 'user_mail_approval_subject', _user_mail_text('approval_subject'), 70, 180, t('Customize the subject of your awaiting approval welcome e-mail, which is sent to new members upon registering.') .' '. t('Available variables are:') .' %username, %site, %password, %uri, %uri_brief, %mailto, %date, %login_uri, %edit_uri.');
-  $group .= form_textarea(t('Body of welcome e-mail (awaiting admin approval)'), 'user_mail_approval_body', _user_mail_text('approval_body'), 70, 10, t('Customize the body of the awaiting approval welcome e-mail, which is sent to new members upon registering.') .' '. t('Available variables are:') .' %username, %site, %password, %uri, %uri_brief, %mailto, %login_uri, %edit_uri.');
-  $group .= form_textfield(t('Subject of password recovery e-mail'), 'user_mail_pass_subject', _user_mail_text('pass_subject'), 70, 180, t('Customize the Subject of your forgotten password e-mail.') .' '. t('Available variables are:') .' %username, %site, %password, %uri, %uri_brief, %mailto, %date, %login_uri, %edit_uri.');
-  $group .= form_textarea(t('Body of password recovery e-mail'), 'user_mail_pass_body', _user_mail_text('pass_body'), 70, 10, t('Customize the body of the forgotten password e-mail.') .' '. t('Available variables are:') .' %username, %site, %password, %uri, %uri_brief, %mailto, %login_uri, %edit_uri.');
+  $group = form_textfield(t('Subject of welcome e-mail'), 'user_mail_welcome_subject', _user_mail_text('welcome_subject'), 70, 180, t('Customize the subject of your welcome e-mail, which is sent to new members upon registering.') .' '. t('Available variables are:') .' %username, %site, %password, %uri, %uri_brief, %mailto, %date, %login_uri, %edit_uri, %login_url.');
+  $group .= form_textarea(t('Body of welcome e-mail'), 'user_mail_welcome_body', _user_mail_text('welcome_body'), 70, 10, t('Customize the body of the welcome e-mail, which is sent to new members upon registering.') .' '. t('Available variables are:') .' %username, %site, %password, %uri, %uri_brief, %mailto, %login_uri, %edit_uri, %login_url.');
+  $group .= form_textfield(t('Subject of welcome e-mail (awaiting admin approval)'), 'user_mail_approval_subject', _user_mail_text('approval_subject'), 70, 180, t('Customize the subject of your awaiting approval welcome e-mail, which is sent to new members upon registering.') .' '. t('Available variables are:') .' %username, %site, %password, %uri, %uri_brief, %mailto, %date, %login_uri, %edit_uri, %login_url.');
+  $group .= form_textarea(t('Body of welcome e-mail (awaiting admin approval)'), 'user_mail_approval_body', _user_mail_text('approval_body'), 70, 10, t('Customize the body of the awaiting approval welcome e-mail, which is sent to new members upon registering.') .' '. t('Available variables are:') .' %username, %site, %password, %uri, %uri_brief, %mailto, %login_uri, %edit_uri, %login_url.');
+  $group .= form_textfield(t('Subject of password recovery e-mail'), 'user_mail_pass_subject', _user_mail_text('pass_subject'), 70, 180, t('Customize the Subject of your forgotten password e-mail.') .' '. t('Available variables are:') .' %username, %site, %login_url, %uri, %uri_brief, %mailto, %date, %login_uri, %edit_uri.');
+  $group .= form_textarea(t('Body of password recovery e-mail'), 'user_mail_pass_body', _user_mail_text('pass_body'), 70, 10, t('Customize the body of the forgotten password e-mail.') .' '. t('Available variables are:') .' %username, %site, %login_url, %uri, %uri_brief, %mailto, %login_uri, %edit_uri.');
   $output .= form_group(t('User email settings'), $group);
 
   // Picture settings.
@@ -1615,10 +1654,11 @@
     array('data' => t('Username'), 'field' => 'u.name'),
     array('data' => t('Status'), 'field' => 'u.status'),
     array('data' => t('Roles')),
-    array('data' => t('Last access'), 'field' => 'u.changed', 'sort' => 'desc'),
+    array('data' => t('Last access'), 'field' => 'u.lastlogin', 'sort' => 'desc'),
+    array('data' => t('Last changed'), 'field' => 'u.changed'),
     t('Operations')
   );
-  $sql = 'SELECT u.uid, u.name, u.status, u.changed FROM {users} u WHERE uid != 0';
+  $sql = 'SELECT u.uid, u.name, u.status, u.lastlogin, u.changed FROM {users} u WHERE uid != 0';
   $sql .= tablesort_sql($header);
   $result = pager_query($sql, 50);
 
@@ -1632,7 +1672,7 @@
       $roles[] = $role->name;
     }
 
-    $rows[] = array($account->uid, format_name($account), $status[$account->status], implode(',<br />', $roles), format_date($account->changed, 'small'), l(t('edit'), "user/$account->uid/edit", array(), $destination));
+    $rows[] = array($account->uid, format_name($account), $status[$account->status], implode(',<br />', $roles), $account->lastlogin ? format_date($account->lastlogin, 'small') : t('never'), format_date($account->changed, 'small'), l(t('edit'), "user/$account->uid/edit", array(), $destination));
   }
 
   $pager = theme('pager', NULL, 50, 0, tablesort_pager());
