--- modules/user/user.module 2011-02-11 08:01:09.000000000 +0100 +++ modules/user/user.module 2011-02-11 09:46:17.000000000 +0100 @@ -446,6 +446,34 @@ } } +function user_check_password($password, $account) { + $hash = hash('md5', $password, FALSE); + return ($hash && $account->pass == $hash); +} + +/** + * Form validation handler for the current password on the user_account_form(). + */ +function user_validate_current_pass(&$form, &$form_state) { + global $user; + + $account = $form['#user']; + foreach ($form_state['values']['current_pass_required_values'] as $key => $name) { + // This validation only works for required textfields (like mail) or + // form values like password_confirm that have their own validation + // that prevent them from being empty if they are changed. + if ((strlen(trim($form_state['values'][$key])) > 0) && ($form_state['values'][$key] != $account->$key)) { + $current_pass_failed = empty($form_state['values']['current_pass']) || !user_check_password($form_state['values']['current_pass'], $user); + if ($current_pass_failed) { + form_set_error('current_pass', t("Your current password is missing or incorrect; it's required to change the %name.", array('%name' => $name))); + form_set_error($key); + } + // We only need to check the password once. + break; + } + } +} + /** * Generate a random alphanumeric password. */ @@ -929,7 +957,7 @@ 'title' => 'Request new password', 'page callback' => 'drupal_get_form', 'page arguments' => array('user_pass'), - 'access callback' => 'user_is_anonymous', + 'access callback' => TRUE, 'type' => MENU_LOCAL_TASK, 'file' => 'user.pages.inc', ); @@ -1486,6 +1514,7 @@ '#maxlength' => USERNAME_MAX_LENGTH, '#description' => t('Spaces are allowed; punctuation is not allowed except for periods, hyphens, and underscores.'), '#required' => TRUE, + '#weight' => -10, ); } $form['account']['mail'] = array('#type' => 'textfield', @@ -1500,6 +1529,36 @@ '#description' => t('To change the current user password, enter the new password in both fields.'), '#size' => 25, ); + // To skip the current password field, the user must have logged in via a + // one-time link and have the token in the URL. + $pass_reset = isset($_SESSION['pass_reset_' . $uid]) && isset($_GET['pass-reset-token']) && ($_GET['pass-reset-token'] == $_SESSION['pass_reset_' . $uid]); + $protected_values = array(); + $current_pass_description = ''; + // The user may only change their own password without their current + // password if they logged in via a one-time login link. + if (!$pass_reset) { + $protected_values['mail'] = $form['account']['mail']['#title']; + $protected_values['pass'] = t('Password'); + $request_new = l(t('Request new password'), 'user/password', array('attributes' => array('title' => t('Request new password via e-mail.')))); + $current_pass_description = t('Enter your current password to change the %mail or %pass. !request_new.', array('%mail' => $protected_values['mail'], '%pass' => $protected_values['pass'], '!request_new' => $request_new)); + } + global $user; + // The user must enter their current password to change to a new one. + if ($user->uid == $uid) { + $form['account']['current_pass_required_values'] = array( + '#type' => 'value', + '#value' => $protected_values, + ); + $form['account']['current_pass'] = array( + '#type' => 'password', + '#title' => t('Current password'), + '#size' => 25, + '#access' => !empty($protected_values), + '#description' => $current_pass_description, + '#weight' => -5, + ); + $form['#validate'][] = 'user_validate_current_pass'; + } } elseif (!variable_get('user_email_verification', TRUE) || $admin) { $form['account']['pass'] = array( @@ -1627,6 +1686,9 @@ if (isset($edit['roles'])) { $edit['roles'] = array_filter($edit['roles']); } + // Unset current password to avoid it being saved. + unset($edit['current_pass_required_values']); + unset($edit['current_pass']); } /** --- modules/user/user.pages.inc 2011-02-11 08:01:16.000000000 +0100 +++ modules/user/user.pages.inc 2011-02-11 09:40:21.000000000 +0100 @@ -29,6 +29,8 @@ * @see user_pass_submit() */ function user_pass() { + global $user; + $form['name'] = array( '#type' => 'textfield', '#title' => t('Username or e-mail address'), @@ -36,6 +38,16 @@ '#maxlength' => max(USERNAME_MAX_LENGTH, EMAIL_MAX_LENGTH), '#required' => TRUE, ); + // Allow logged in users to request this also. + if ($user->uid > 0) { + $form['name']['#type'] = 'value'; + $form['name']['#value'] = $user->mail; + $form['mail'] = array( + '#prefix' => '
', + '#markup' => t('Password reset instructions will be mailed to %email. You must log out to use the password reset link in the e-mail.', array('%email' => $user->mail)), + '#suffix' => '
', + ); + } $form['submit'] = array('#type' => 'submit', '#value' => t('E-mail new password')); return $form; @@ -79,6 +91,47 @@ return; } +function drupal_random_bytes($count) { + // $random_state does not use drupal_static as it stores random bytes. + static $random_state, $bytes; + // Initialize on the first call. The contents of $_SERVER includes a mix of + // user-specific and system information that varies a little with each page. + if (!isset($random_state)) { + $random_state = print_r($_SERVER, TRUE); + if (function_exists('getmypid')) { + // Further initialize with the somewhat random PHP process ID. + $random_state .= getmypid(); + } + $bytes = ''; + } + if (strlen($bytes) < $count) { + // /dev/urandom is available on many *nix systems and is considered the + // best commonly available pseudo-random source. + if ($fh = @fopen('/dev/urandom', 'rb')) { + // PHP only performs buffered reads, so in reality it will always read + // at least 4096 bytes. Thus, it costs nothing extra to read and store + // that much so as to speed any additional invocations. + $bytes .= fread($fh, max(4096, $count)); + fclose($fh); + } + // If /dev/urandom is not available or returns no bytes, this loop will + // generate a good set of pseudo-random bytes on any system. + // Note that it may be important that our $random_state is passed + // through hash() prior to being rolled into $output, that the two hash() + // invocations are different, and that the extra input into the first one - + // the microtime() - is prepended rather than appended. This is to avoid + // directly leaking $random_state via the $output stream, which could + // allow for trivial prediction of further "random" numbers. + while (strlen($bytes) < $count) { + $random_state = hash('sha256', microtime() . mt_rand() . $random_state); + $bytes .= hash('sha256', mt_rand() . $random_state, TRUE); + } + } + $output = substr($bytes, 0, $count); + $bytes = substr($bytes, $count); + return $output; +} + /** * Menu callback; process one time login link and redirects to the user page on success. */ @@ -117,7 +170,10 @@ // user, which invalidates further use of the one-time login link. user_authenticate_finalize($form_state['values']); drupal_set_message(t('You have just used your one-time login link. It is no longer necessary to use this link to login. Please change your password.')); - drupal_goto('user/'. $user->uid .'/edit'); + // Let the user's password be changed without the current password check. + $token = md5(drupal_random_bytes(55)); + $_SESSION['pass_reset_' . $user->uid] = $token; + drupal_goto('user/' . $user->uid . '/edit', array('pass-reset-token' => $token)); } else { $form['message'] = array('#value' => t('This is a one-time login for %user_name and will expire on %expiration_date.
Click on this button to login to the site and change your password.
', array('%user_name' => $account->name, '%expiration_date' => format_date($timestamp + $timeout)))); @@ -292,6 +348,11 @@ user_module_invoke('submit', $form_state['values'], $account, $category); user_save($account, $form_state['values'], $category); + if ($category == 'account' && !empty($form_state['values']['pass'])) { + // Remove the password reset tag since a new password was saved. + unset($_SESSION['pass_reset_'. $account->uid]); + } + // Clear the page cache because pages can contain usernames and/or profile information: cache_clear_all();