updated core to 7.80

This commit is contained in:
2021-07-12 10:11:08 +02:00
parent 7b1e954f7f
commit 5656f5a68a
236 changed files with 4149 additions and 888 deletions

View File

@@ -0,0 +1,11 @@
name = "User module flood control tests"
description = "Support module for user flood control testing."
package = Testing
version = VERSION
core = 7.x
hidden = TRUE
; Information added by Drupal.org packaging script on 2021-04-21
version = "7.80"
project = "drupal"
datestamp = "1619021862"

View File

@@ -0,0 +1,18 @@
<?php
/**
* @file
* Dummy module implementing hook_user_flood_control.
*/
/**
* Implements hook_user_flood_control().
*/
function user_flood_test_user_flood_control($ip, $username = FALSE) {
if (!empty($username)) {
watchdog('user_flood_test', 'hook_user_flood_control was passed username %username and IP %ip.', array('%username' => $username, '%ip' => $ip));
}
else {
watchdog('user_flood_test', 'hook_user_flood_control was passed IP %ip.', array('%ip' => $ip));
}
}

View File

@@ -5,7 +5,7 @@ version = VERSION
core = 7.x
hidden = TRUE
; Information added by Drupal.org packaging script on 2019-05-08
version = "7.67"
; Information added by Drupal.org packaging script on 2021-04-21
version = "7.80"
project = "drupal"
datestamp = "1557336079"
datestamp = "1619021862"

View File

@@ -35,7 +35,7 @@ function user_form_test_current_password($form, &$form_state, $account) {
'#description' => t('A field that would require a correct password to change.'),
'#required' => TRUE,
);
$form['current_pass'] = array(
'#type' => 'password',
'#title' => t('Current password'),
@@ -80,3 +80,42 @@ function user_form_test_user_account_submit($form, &$form_state) {
// test for bugs that can be triggered in contributed modules.
$form_state['rebuild'] = TRUE;
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function user_form_test_form_user_pass_reset_alter(&$form, &$form_state) {
// An unaltered form has no form values; the uid/timestmap/"confirm" are in
// the URL arguments. (If for some reason a form_alter method needs the
// hash, it can be retrieved from $form['#action'].)
if (!is_numeric(arg(2)) || !is_numeric(arg(3)) || !is_string(arg(4)) || arg(4) !== 'confirm') {
throw new Exception("Something unexpected changed in the user_pass_reset form; we are not getting the UID/timestamp/'confirm' passed anymore.");
}
// Use the variable system to communicate to the test code, since we don't
// share a session with it.
variable_set('user_test_pass_reset_form_build_' . arg(2), TRUE);
$form['#submit'][] = 'user_form_test_form_user_pass_reset_submit';
// We must cache the form to ensure the form builder (user_pass_reset()) is
// skipped when processing the submitted form, otherwise we get redirected
// already during form build.
$form_state['cache'] = TRUE;
}
/**
* Submit function for user_pass_reset().
*/
function user_form_test_form_user_pass_reset_submit($form, &$form_state) {
// On submit, the hash is in arg(4).
if (!is_numeric(arg(2)) || !is_numeric(arg(3)) || !is_string(arg(4)) || strlen(arg(4)) < 32) {
throw new Exception("Something unexpected changed in the user_pass_reset form; we are not getting the UID/timestamp/hash passed anymore.");
}
variable_set('user_test_pass_reset_form_submit_' . arg(2), TRUE);
// Because the form does no further processing and has no redirect set,
// drupal_redirect_form() will redirect back to ourselves
// (user/reset/UID/TIMESTAMP/HASH/login); we will be logged in and redirected
// while the form is built again. FYI: we cannot set $form_state['rebuild']
// to get around the first redirect without further hacks, because then the
// form won't pass the hash. (Our original $form_state['build_info']['args']
// contains "confirm" for the 3rd argument.)
}

View File

@@ -0,0 +1,11 @@
name = "User module session tests"
description = "Support module for user session testing."
package = Testing
version = VERSION
core = 7.x
hidden = TRUE
; Information added by Drupal.org packaging script on 2021-04-21
version = "7.80"
project = "drupal"
datestamp = "1619021862"

View File

@@ -0,0 +1,29 @@
<?php
/**
* @file
* Dummy module implementing a page callback to create an anon session.
*/
/**
* Implements hook_menu().
*/
function user_session_test_menu() {
$items = array();
$items['user_session_test_anon_session'] = array(
'page callback' => 'user_session_test_anon_session',
'access callback' => TRUE,
);
return $items;
}
/**
* Page callback.
*
* Creates an anonymous user session.
*/
function user_session_test_anon_session() {
$data = 'This dummy data will be stored in a user session.';
$_SESSION[__FUNCTION__] = $data;
return $data;
}

View File

@@ -61,7 +61,7 @@ function user_filter_form() {
if ($type == 'permission') {
// Merge arrays of module permissions into one.
// Slice past the first element '[any]' whose value is not an array.
$options = call_user_func_array('array_merge', array_slice($filters[$type]['options'], 1));
$options = call_user_func_array('array_merge', array_values(array_slice($filters[$type]['options'], 1)));
$value = $options[$value];
}
else {
@@ -1050,4 +1050,3 @@ function user_admin_role_delete_confirm_submit($form, &$form_state) {
drupal_set_message(t('The role has been deleted.'));
$form_state['redirect'] = 'admin/people/permissions/roles';
}

View File

@@ -472,6 +472,36 @@ function hook_user_role_delete($role) {
->execute();
}
/**
* Respond to user flood control events.
*
* This hook allows you act when an unsuccessful user login has triggered
* flood control. This means that either an IP address or a specific user
* account has been temporarily blocked from logging in.
*
* @param $ip
* The IP address that triggered flood control.
* @param $username
* The username that has been temporarily blocked.
*
* @see user_login_final_validate()
*/
function hook_user_flood_control($ip, $username = FALSE) {
if (!empty($username)) {
// Do something with the blocked $username and $ip. For example, send an
// e-mail to the user and/or site administrator.
// Drupal core uses this hook to log the event:
watchdog('user', 'Flood control blocked login attempt for %user from %ip.', array('%user' => $username, '%ip' => $ip));
}
else {
// Do something with the blocked $ip. For example, add it to a block-list.
// Drupal core uses this hook to log the event:
watchdog('user', 'Flood control blocked login attempt from %ip.', array('%ip' => $ip));
}
}
/**
* @} End of "addtogroup hooks".
*/

View File

@@ -9,7 +9,7 @@ required = TRUE
configure = admin/config/people
stylesheets[all][] = user.css
; Information added by Drupal.org packaging script on 2019-05-08
version = "7.67"
; Information added by Drupal.org packaging script on 2021-04-21
version = "7.80"
project = "drupal"
datestamp = "1557336079"
datestamp = "1619021862"

View File

@@ -2225,11 +2225,17 @@ function user_login_final_validate($form, &$form_state) {
if (isset($form_state['flood_control_triggered'])) {
if ($form_state['flood_control_triggered'] == 'user') {
form_set_error('name', format_plural(variable_get('user_failed_login_user_limit', 5), 'Sorry, there has been more than one failed login attempt for this account. It is temporarily blocked. Try again later or <a href="@url">request a new password</a>.', 'Sorry, there have been more than @count failed login attempts for this account. It is temporarily blocked. Try again later or <a href="@url">request a new password</a>.', array('@url' => url('user/password'))));
module_invoke_all('user_flood_control', ip_address(), $form_state['values']['name']);
}
else {
// We did not find a uid, so the limit is IP-based.
form_set_error('name', t('Sorry, too many failed login attempts from your IP address. This IP address is temporarily blocked. Try again later or <a href="@url">request a new password</a>.', array('@url' => url('user/password'))));
module_invoke_all('user_flood_control', ip_address());
}
// We cannot call drupal_access_denied() here as that can result in an
// infinite loop if the login form is rendered on the 403 page (e.g. in a
// block). So add the 403 header and allow form processing to finish.
drupal_add_http_header('Status', '403 Forbidden');
}
else {
// Use $form_state['input']['name'] here to guarantee that we send
@@ -2247,6 +2253,23 @@ function user_login_final_validate($form, &$form_state) {
}
}
/**
* Implements hook_user_flood_control().
*/
function user_user_flood_control($ip, $username = FALSE) {
if (variable_get('log_user_flood_control', TRUE)) {
if (!empty($username)) {
watchdog('user', 'Flood control blocked login attempt for %user from %ip.', array(
'%user' => $username,
'%ip' => $ip
));
}
else {
watchdog('user', 'Flood control blocked login attempt from %ip.', array('%ip' => $ip));
}
}
}
/**
* Try to validate the user's login credentials locally.
*

View File

@@ -66,6 +66,22 @@ function user_pass() {
* @see user_pass_submit()
*/
function user_pass_validate($form, &$form_state) {
if (isset($form_state['values']['name']) && !is_scalar($form_state['values']['name'])) {
form_set_error('name', t('An illegal value has been detected. Please contact the site administrator.'));
return;
}
$user_pass_reset_ip_window = variable_get('user_pass_reset_ip_window', 3600);
// Do not allow any password reset from the current user's IP if the limit
// has been reached. Default is 50 attempts allowed in one hour. This is
// independent of the per-user limit to catch attempts from one IP to request
// resets for many different user accounts. We have a reasonably high limit
// since there may be only one apparent IP for all users at an institution.
if (!flood_is_allowed('pass_reset_ip', variable_get('user_pass_reset_ip_limit', 50), $user_pass_reset_ip_window)) {
form_set_error('name', t('Sorry, too many password reset attempts from your IP address. This IP address is temporarily blocked. Try again later or <a href="@url">request a new password</a>.', array('@url' => url('user/password'))));
return;
}
// Always register an per-IP event.
flood_register_event('pass_reset_ip', $user_pass_reset_ip_window);
$name = trim($form_state['values']['name']);
// Try to load by email.
$users = user_load_multiple(array(), array('mail' => $name, 'status' => '1'));
@@ -76,6 +92,19 @@ function user_pass_validate($form, &$form_state) {
$account = reset($users);
}
if (isset($account->uid)) {
// Register user flood events based on the uid only, so they can be cleared
// when a password is reset successfully.
$identifier = $account->uid;
$user_pass_reset_user_window = variable_get('user_pass_reset_user_window', 21600);
$user_pass_reset_user_limit = variable_get('user_pass_reset_user_limit', 5);
// Don't allow password reset if the limit for this user has been reached.
// Default is to allow 5 passwords resets every 6 hours.
if (!flood_is_allowed('pass_reset_user', $user_pass_reset_user_limit, $user_pass_reset_user_window, $identifier)) {
form_set_error('name', format_plural($user_pass_reset_user_limit, 'Sorry, there has been more than one password reset attempt for this account. It is temporarily blocked. Try again later or <a href="@url">login with your password</a>.', 'Sorry, there have been more than @count password reset attempts for this account. It is temporarily blocked. Try again later or <a href="@url">login with your password</a>.', array('@url' => url('user/login'))));
return;
}
// Register a per-user event.
flood_register_event('pass_reset_user', $user_pass_reset_user_window, $identifier);
form_set_value(array('#parents' => array('account')), $account, $form_state);
}
else {
@@ -105,10 +134,25 @@ function user_pass_submit($form, &$form_state) {
/**
* Menu callback; process one time login link and redirects to the user page on success.
*
* In order to never disclose password reset hashes via referrer headers or
* web browser history, this function always issues a redirect when a valid
* password reset hash is in the URL.
*/
function user_pass_reset($form, &$form_state, $uid, $timestamp, $hashed_pass, $action = NULL) {
global $user;
// Check for a reset hash in the session. Immediately remove it (to prevent it
// from being reused, for example if this page is visited again via the
// browser history) and store it for later use.
if (isset($_SESSION['pass_reset_hash'])) {
$session_reset_hash = $_SESSION['pass_reset_hash'];
unset($_SESSION['pass_reset_hash']);
}
else {
$session_reset_hash = NULL;
}
// When processing the one-time login link, we have to make sure that a user
// isn't already logged in.
if ($user->uid) {
@@ -153,7 +197,36 @@ function user_pass_reset($form, &$form_state, $uid, $timestamp, $hashed_pass, $a
drupal_set_message(t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.'), 'error');
drupal_goto('user/password');
}
elseif ($account->uid && $timestamp >= $account->login && $timestamp <= $current && $hashed_pass == user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)) {
// Validate the reset hash from the URL or from the session.
$is_valid = FALSE;
if ($account->uid && $timestamp >= $account->login && $timestamp <= $current) {
// If the reset hash in the URL is valid, put it in the session and
// redirect to the same URL but with the hash replaced by an invalid
// one ("confirm"). This prevents disclosing the hash via referrer
// headers or web browser history.
if ($hashed_pass == user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)) {
if ($action === 'login') {
// The 'login' action redirects directly to the user edit form.
$is_valid = TRUE;
}
else {
$_SESSION['pass_reset_hash'] = $hashed_pass;
$args = arg();
foreach ($args as &$arg) {
if ($arg == $hashed_pass) {
$arg = 'confirm';
}
}
$path = implode('/', $args);
drupal_goto($path, array('query' => drupal_get_query_parameters()));
}
}
// If the reset hash from the session is valid, use that.
elseif ($session_reset_hash && $session_reset_hash == user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)) {
$is_valid = TRUE;
}
}
if ($is_valid) {
// First stage is a confirmation form, then login
if ($action == 'login') {
// Set the new user.
@@ -161,6 +234,8 @@ function user_pass_reset($form, &$form_state, $uid, $timestamp, $hashed_pass, $a
// user_login_finalize() also updates the login timestamp of the
// user, which invalidates further use of the one-time login link.
user_login_finalize();
// Clear any password reset flood events for this user.
flood_clear_event('pass_reset_user', $account->uid);
watchdog('user', 'User %name used one-time login link at time %timestamp.', array('%name' => $account->name, '%timestamp' => $timestamp));
drupal_set_message(t('You have just used your one-time login link. It is no longer necessary to use this link to log in. Please change your password.'));
// Let the user's password be changed without the current password check.
@@ -173,7 +248,11 @@ function user_pass_reset($form, &$form_state, $uid, $timestamp, $hashed_pass, $a
$form['help'] = array('#markup' => '<p>' . t('This login can be used only once.') . '</p>');
$form['actions'] = array('#type' => 'actions');
$form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Log in'));
$form['#action'] = url("user/reset/$uid/$timestamp/$hashed_pass/login");
$form['#action'] = url("user/reset/$uid/$timestamp/$session_reset_hash/login");
// Prevent the browser from storing this page so that the token will
// not be visible in the form action if the back button is used to
// revisit this page.
drupal_add_http_header('Cache-Control', 'no-store');
return $form;
}
}

View File

@@ -321,6 +321,10 @@ class UserLoginTestCase extends DrupalWebTestCase {
);
}
function setUp() {
parent::setUp('user_session_test', 'user_flood_test');
}
/**
* Test the global login flood control.
*/
@@ -421,6 +425,17 @@ class UserLoginTestCase extends DrupalWebTestCase {
$this->assertIdentical(_password_get_count_log2($account->pass), DRUPAL_HASH_COUNT + 1);
}
/**
* Test logging in when an anon session already exists.
*/
function testLoginWithAnonSession() {
// Visit the callback to generate a session for this anon user.
$this->drupalGet('user_session_test_anon_session');
// Now login.
$account = $this->drupalCreateUser(array());
$this->drupalLogin($account);
}
/**
* Make an unsuccessful login attempt.
*
@@ -438,12 +453,19 @@ class UserLoginTestCase extends DrupalWebTestCase {
$this->drupalPost('user', $edit, t('Log in'));
$this->assertNoFieldByXPath("//input[@name='pass' and @value!='']", NULL, 'Password value attribute is blank.');
if (isset($flood_trigger)) {
$this->assertResponse(403);
$user_log = db_query_range('SELECT message FROM {watchdog} WHERE type = :type ORDER BY wid DESC', 0, 1, array(':type' => 'user'))->fetchField();
$user_flood_test_log = db_query_range('SELECT message FROM {watchdog} WHERE type = :type ORDER BY wid DESC', 0, 1, array(':type' => 'user_flood_test'))->fetchField();
if ($flood_trigger == 'user') {
$this->assertRaw(format_plural(variable_get('user_failed_login_user_limit', 5), 'Sorry, there has been more than one failed login attempt for this account. It is temporarily blocked. Try again later or <a href="@url">request a new password</a>.', 'Sorry, there have been more than @count failed login attempts for this account. It is temporarily blocked. Try again later or <a href="@url">request a new password</a>.', array('@url' => url('user/password'))));
$this->assertRaw(t('Sorry, there have been more than @count failed login attempts for this account. It is temporarily blocked. Try again later or <a href="@url">request a new password</a>.', array('@url' => url('user/password'), '@count' => variable_get('user_failed_login_user_limit', 5))));
$this->assertEqual('Flood control blocked login attempt for %user from %ip.', $user_log, 'A watchdog message was logged for the login attempt blocked by flood control per user');
$this->assertEqual('hook_user_flood_control was passed username %username and IP %ip.', $user_flood_test_log, 'hook_user_flood_control was invoked by flood control per user');
}
else {
// No uid, so the limit is IP-based.
$this->assertRaw(t('Sorry, too many failed login attempts from your IP address. This IP address is temporarily blocked. Try again later or <a href="@url">request a new password</a>.', array('@url' => url('user/password'))));
$this->assertEqual('Flood control blocked login attempt from %ip.', $user_log, 'A watchdog message was logged for the login attempt blocked by flood control per IP');
$this->assertEqual('hook_user_flood_control was passed IP %ip.', $user_flood_test_log, 'hook_user_flood_control was invoked by flood control per IP');
}
}
else {
@@ -458,6 +480,10 @@ class UserLoginTestCase extends DrupalWebTestCase {
class UserPasswordResetTestCase extends DrupalWebTestCase {
protected $profile = 'standard';
function setUp() {
parent::setUp('user_form_test');
}
public static function getInfo() {
return array(
'name' => 'Reset password',
@@ -469,20 +495,38 @@ class UserPasswordResetTestCase extends DrupalWebTestCase {
/**
* Retrieves password reset email and extracts the login link.
*/
public function getResetURL() {
public function getResetURL($bypass_form = FALSE) {
// Assume the most recent email.
$_emails = $this->drupalGetMails();
$email = end($_emails);
$urls = array();
preg_match('#.+user/reset/.+#', $email['body'], $urls);
return $urls[0];
return $urls[0] . ($bypass_form ? '/login' : '');
}
/**
* Generates login link.
*/
public function generateResetURL($account, $bypass_form = FALSE) {
return user_pass_reset_url($account) . ($bypass_form ? '/login' : '');
}
/**
* Turns a password reset URL into a 'confirm' URL.
*/
public function getConfirmURL($reset_url) {
// Last part is always the hash; replace with "confirm".
$parts = explode('/', $reset_url);
array_pop($parts);
array_push($parts, 'confirm');
return implode('/', $parts);
}
/**
* Tests password reset functionality.
*/
function testUserPasswordReset() {
function testUserPasswordReset($use_direct_login_link = FALSE) {
// Create a user.
$account = $this->drupalCreateUser();
$this->drupalLogin($account);
@@ -492,6 +536,8 @@ class UserPasswordResetTestCase extends DrupalWebTestCase {
$this->drupalPost('user/password', $edit, t('E-mail new password'));
// Confirm the password reset.
$this->assertText(t('Further instructions have been sent to your e-mail address.'), 'Password reset instructions mailed message displayed.');
// Ensure that flood control was not triggered.
$this->assertNoText(t('is temporarily blocked. Try again later'), 'Flood control was not triggered by single password reset.');
// Create an image field to enable an Ajax request on the user profile page.
$field = array(
@@ -516,11 +562,19 @@ class UserPasswordResetTestCase extends DrupalWebTestCase {
);
field_create_instance($instance);
$resetURL = $this->getResetURL();
variable_del("user_test_pass_reset_form_submit_{$account->uid}");
$resetURL = $this->getResetURL($use_direct_login_link);
$this->drupalGet($resetURL);
// Check successful login.
$this->drupalPost(NULL, NULL, t('Log in'));
if (!$use_direct_login_link) {
$this->assertUrl($this->getConfirmURL($resetURL), array(), 'The user is redirected to the reset password confirm form.');
$this->drupalPost(NULL, NULL, t('Log in'));
// The form was fully processed before redirecting.
$form_submit_handled = variable_get("user_test_pass_reset_form_submit_{$account->uid}", FALSE);
$this->assertTrue($form_submit_handled, 'A custom submit handler executed.');
}
$this->assertText('You have just used your one-time login link. It is no longer necessary to use this link to log in. Please change your password.');
// Make sure the Ajax request from uploading a file does not invalidate the
// reset token.
@@ -535,26 +589,144 @@ class UserPasswordResetTestCase extends DrupalWebTestCase {
$edit = array('pass[pass1]' => $password, 'pass[pass2]' => $password);
$this->drupalPost(NULL, $edit, t('Save'));
$this->assertText(t('The changes have been saved.'), 'Forgotten password changed.');
// Ensure blocked and deleted accounts can't access the direct login link.
$this->drupalLogout();
$reset_url = $this->generateResetURL($account, $use_direct_login_link);
user_save($account, array('status' => 0));
$this->drupalGet($reset_url);
$this->assertResponse(403);
user_delete($account->uid);
$this->drupalGet($reset_url);
$this->assertResponse(403);
}
/**
* Test user-based flood control on password reset.
*/
function testPasswordResetFloodControlPerUser() {
// Set a very low limit for testing.
variable_set('user_pass_reset_user_limit', 2);
// Create a user.
$account = $this->drupalCreateUser();
$this->drupalLogin($account);
$this->drupalLogout();
$edit = array('name' => $account->name);
// Try 2 requests that should not trigger flood control.
for ($i = 0; $i < 2; $i++) {
$this->drupalPost('user/password', $edit, t('E-mail new password'));
// Confirm the password reset.
$this->assertText(t('Further instructions have been sent to your e-mail address.'), 'Password reset instructions mailed message displayed.');
// Ensure that flood control was not triggered.
$this->assertNoText(t('is temporarily blocked. Try again later'), 'Flood control was not triggered by password reset.');
}
// A successful password reset should clear flood events.
$resetURL = $this->getResetURL();
$this->drupalGet($resetURL);
// Check successful login.
$this->drupalPost(NULL, NULL, t('Log in'));
$this->drupalLogout();
// Try 2 requests that should not trigger flood control.
for ($i = 0; $i < 2; $i++) {
$this->drupalPost('user/password', $edit, t('E-mail new password'));
// Confirm the password reset.
$this->assertText(t('Further instructions have been sent to your e-mail address.'), 'Password reset instructions mailed message displayed.');
// Ensure that flood control was not triggered.
$this->assertNoText(t('is temporarily blocked. Try again later'), 'Flood control was not triggered by password reset.');
}
// The next request should trigger flood control
$this->drupalPost('user/password', $edit, t('E-mail new password'));
// Confirm the password reset was blocked.
$this->assertNoText(t('Further instructions have been sent to your e-mail address.'), 'Password reset instructions mailed message not displayed for excessive password resets.');
// Ensure that flood control was triggered.
$this->assertText(t('Sorry, there have been more than 2 password reset attempts for this account. It is temporarily blocked.'), 'Flood control was triggered by excessive password resets for one user.');
}
/**
* Test IP-based flood control on password reset.
*/
function testPasswordResetFloodControlPerIp() {
// Set a very low limit for testing.
variable_set('user_pass_reset_ip_limit', 2);
// Try 2 requests that should not trigger flood control.
for ($i = 0; $i < 2; $i++) {
$name = $this->randomName();
$edit = array('name' => $name);
$this->drupalPost('user/password', $edit, t('E-mail new password'));
// Confirm the password reset was not blocked. Note that @name is used
// instead of %name as assertText() works with plain text not HTML.
$this->assertText(t('Sorry, @name is not recognized as a user name or an e-mail address.', array('@name' => $name)), 'User name not recognized message displayed.');
// Ensure that flood control was not triggered.
$this->assertNoText(t('is temporarily blocked. Try again later'), 'Flood control was not triggered by password reset.');
}
// The next request should trigger flood control
$name = $this->randomName();
$edit = array('name' => $name);
$this->drupalPost('user/password', $edit, t('E-mail new password'));
// Confirm the password reset was blocked early. Note that @name is used
// instead of %name as assertText() works with plain text not HTML.
$this->assertNoText(t('Sorry, @name is not recognized as a user name or an e-mail address.', array('@name' => $name)), 'User name not recognized message not displayed.');
// Ensure that flood control was triggered.
$this->assertText(t('Sorry, too many password reset attempts from your IP address. This IP address is temporarily blocked.'), 'Flood control was triggered by excessive password resets from one IP.');
}
/**
* Test user password reset while logged in.
*/
function testUserPasswordResetLoggedIn() {
function testUserPasswordResetLoggedIn($use_direct_login_link = FALSE) {
$another_account = $this->drupalCreateUser();
$account = $this->drupalCreateUser();
$this->drupalLogin($account);
// Make sure the test account has a valid password.
user_save($account, array('pass' => user_password()));
// Try to use the login link while logged in as a different user.
// Generate one time login link.
$reset_url = user_pass_reset_url($account);
$reset_url = $this->generateResetURL($another_account, $use_direct_login_link);
$this->drupalGet($reset_url);
$this->assertRaw(t(
'Another user (%other_user) is already logged into the site on this computer, but you tried to use a one-time link for user %resetting_user. Please <a href="!logout">logout</a> and try using the link again.',
array('%other_user' => $account->name, '%resetting_user' => $another_account->name, '!logout' => url('user/logout'))
));
$this->assertText('Reset password');
$this->drupalPost(NULL, NULL, t('Log in'));
// Test the link for a deleted user while logged in.
user_delete($another_account->uid);
$this->drupalGet($reset_url);
$this->assertText('The one-time login link you clicked is invalid.');
// Generate a one time login link for the logged-in user.
$fapi_action = $use_direct_login_link ? 'build' : 'submit';
variable_del("user_test_pass_reset_form_{$fapi_action}_{$account->uid}");
$reset_url = $this->generateResetURL($account, $use_direct_login_link);
$this->drupalGet($reset_url);
if ($use_direct_login_link) {
// The form is never fully built; user is logged out (session destroyed)
// and redirected to the same URL, then logged in again and redirected
// during form build.
$form_built = variable_get("user_test_pass_reset_form_build_{$account->uid}", FALSE);
$this->assertTrue(!$form_built, 'The password reset form was never fully built.');
}
else {
$this->assertUrl($this->getConfirmURL($reset_url), array(), 'The user is redirected to the reset password confirm form.');
$this->assertText('Reset password');
$this->drupalPost(NULL, NULL, t('Log in'));
// The form was fully processed before redirecting.
$form_submit_handled = variable_get("user_test_pass_reset_form_submit_{$account->uid}", FALSE);
$this->assertTrue($form_submit_handled, 'A custom submit handler executed.');
}
$this->assertText('You have just used your one-time login link. It is no longer necessary to use this link to log in. Please change your password.');
// The user can change the forgotten password on the page they are
// redirected to.
$pass = user_password();
$edit = array(
'pass[pass1]' => $pass,
@@ -565,6 +737,14 @@ class UserPasswordResetTestCase extends DrupalWebTestCase {
$this->assertText('The changes have been saved.');
}
/**
* Test direct login link that bypasses the password reset form.
*/
function testUserDirectLogin() {
$this->testUserPasswordReset(TRUE);
$this->testUserPasswordResetLoggedIn(TRUE);
}
/**
* Attempts login using an expired password reset link.
*/
@@ -668,7 +848,7 @@ class UserPasswordResetTestCase extends DrupalWebTestCase {
$reset_url = url("user/reset/$user1->uid/$timestamp/$reset_url_token", array('absolute' => TRUE));
$this->drupalGet($reset_url);
$this->assertText($user1->name, 'The valid password reset page shows the user name.');
$this->assertUrl($reset_url, array(), 'The user remains on the password reset login page.');
$this->assertUrl($this->getConfirmURL($reset_url), array(), 'The user is redirected to the reset password confirm form.');
$this->assertNoText('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.');
}