updated core to 7.80
This commit is contained in:
11
modules/user/tests/user_flood_test.info
Normal file
11
modules/user/tests/user_flood_test.info
Normal 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"
|
18
modules/user/tests/user_flood_test.module
Normal file
18
modules/user/tests/user_flood_test.module
Normal 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));
|
||||
}
|
||||
}
|
@@ -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"
|
||||
|
@@ -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.)
|
||||
}
|
||||
|
11
modules/user/tests/user_session_test.info
Normal file
11
modules/user/tests/user_session_test.info
Normal 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"
|
29
modules/user/tests/user_session_test.module
Normal file
29
modules/user/tests/user_session_test.module
Normal 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;
|
||||
}
|
@@ -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';
|
||||
}
|
||||
|
||||
|
@@ -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".
|
||||
*/
|
||||
|
@@ -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"
|
||||
|
@@ -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.
|
||||
*
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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.');
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user