[ticket/11327] Finish up initial version of password reset system

PHPBB3-11327
This commit is contained in:
Marc Alexander 2019-08-10 17:18:39 +02:00
parent eee18f3747
commit cefdf8bf19
No known key found for this signature in database
GPG key ID: 50E0D2423696F995
4 changed files with 111 additions and 63 deletions

View file

@ -7,6 +7,7 @@ services:
- '@dispatcher'
- '@controller.helper'
- '@language'
- '@log'
- '@passwords.manager'
- '@request'
- '@template'

View file

@ -416,7 +416,8 @@ $lang = array_merge($lang, array(
'PASS_TYPE_SYMBOL_EXPLAIN' => 'Password must be between %1$s and %2$s long, must contain letters in mixed case, must contain numbers and must contain symbols.',
'PASSWORD' => 'Password',
'PASSWORD_ACTIVATED' => 'Your new password has been activated.',
'PASSWORD_UPDATED_IF_EXISTED' => 'If your account exists, a new password was sent to your registered email address. If you do not receive an email, it may be because you are banned, your account is not activated, or you are not allowed to change your password. Contact admin if any of those reasons apply. Also, check your spam filter.',
'PASSWORD_RESET' => 'Your password has been successfully reset.',
'PASSWORD_RESET_LINK_SENT' => 'If your account exists, a password reset link was sent to your registered email address. If you do not receive an email, it may be because you are banned, your account is not activated, you have requested multiple password resets within a short time frame, or you are not allowed to change your password. Contact an admin if any of those reasons apply. Also, please check your spam filter.',
'PERMISSIONS_RESTORED' => 'Successfully restored original permissions.',
'PERMISSIONS_TRANSFERRED' => 'Successfully transferred permissions from <strong>%s</strong>, you are now able to browse the board with this users permissions.<br />Please note that admin permissions were not transferred. You are able to revert to your permission set at any time.',
'PM_DISABLED' => 'Private messaging has been disabled on this board.',
@ -464,6 +465,7 @@ $lang = array_merge($lang, array(
'REPLIED_MESSAGE' => 'Replied to message',
'REPLY_TO_ALL' => 'Reply to sender and all recipients.',
'REPORT_PM' => 'Report private message',
'RESET_PASSWORD' => 'Reset password',
'RESET_TOKEN_EXPIRED_OR_INVALID' => 'The password reset token you supplied is invalid or has expired.',
'RESIGN_SELECTED' => 'Resign selected',
'RETURN_FOLDER' => '%1$sReturn to previous folder%2$s',
@ -480,7 +482,6 @@ $lang = array_merge($lang, array(
'SAME_PASSWORD_ERROR' => 'The new password you entered is the same as your current password.',
'SEARCH_YOUR_POSTS' => 'Show your posts',
'SEND_PASSWORD' => 'Send password',
'SENT_AT' => 'Sent', // Used before dates in private messages
'SHOW_EMAIL' => 'Users can contact me by email',
'SIGNATURE_EXPLAIN' => 'This is a block of text that can be added to posts you make. There is a %d character limit.',

View file

@ -18,6 +18,7 @@ use phpbb\controller\helper;
use phpbb\db\driver\driver_interface;
use phpbb\event\dispatcher;
use phpbb\language\language;
use phpbb\log\log_interface;
use phpbb\passwords\manager;
use phpbb\request\request_interface;
use phpbb\template\template;
@ -45,6 +46,9 @@ class reset_password
/** @var language */
protected $language;
/** @var log_interface */
protected $log;
/** @var manager */
protected $passwords_manager;
@ -74,6 +78,7 @@ class reset_password
* @param dispatcher $dispatcher
* @param helper $helper
* @param language $language
* @param log_interface $log
* @param manager $passwords_manager
* @param request_interface $request
* @param template $template
@ -83,14 +88,15 @@ class reset_password
* @param $php_ext
*/
public function __construct(config $config, driver_interface $db, dispatcher $dispatcher, helper $helper,
language $language, manager $passwords_manager, request_interface $request,
template $template, user $user, $tables, $root_path, $php_ext)
language $language, log_interface $log, manager $passwords_manager,
request_interface $request, template $template, user $user, $tables, $root_path, $php_ext)
{
$this->config = $config;
$this->db = $db;
$this->dispatcher = $dispatcher;
$this->helper = $helper;
$this->language = $language;
$this->log = $log;
$this->passwords_manager = $passwords_manager;
$this->request = $request;
$this->template = $template;
@ -109,10 +115,28 @@ class reset_password
if (!$this->config['allow_password_reset'])
{
$this->helper->message($this->language->lang('UCP_PASSWORD_RESET_DISABLED', '<a href="mailto:' . htmlspecialchars($this->config['board_contact']) . '">', '</a>'));
trigger_error($this->language->lang('UCP_PASSWORD_RESET_DISABLED', '<a href="mailto:' . htmlspecialchars($this->config['board_contact']) . '">', '</a>'));
}
}
/**
* Remove reset token for specified user
*
* @param int $user_id User ID
*/
protected function remove_reset_token(int $user_id)
{
$sql_ary = [
'reset_token' => '',
'reset_token_expiration' => 0,
];
$sql = 'UPDATE ' . $this->tables['users'] . '
SET ' . $this->db->sql_build_array('UPDATE', $sql_ary) . '
WHERE user_id = ' . $user_id;
$this->db->sql_query($sql);
}
/**
* Handle password reset request
*
@ -180,7 +204,7 @@ class reset_password
}
else
{
$message = $this->language->lang('PASSWORD_UPDATED_IF_EXISTED') . '<br /><br />' . $this->language->lang('RETURN_INDEX', '<a href="' . append_sid("{$this->root_path}index.{$this->php_ext}") . '">', '</a>');
$message = $this->language->lang('PASSWORD_RESET_LINK_SENT') . '<br /><br />' . $this->language->lang('RETURN_INDEX', '<a href="' . append_sid("{$this->root_path}index.{$this->php_ext}") . '">', '</a>');
$user_row = empty($rowset) ? [] : $rowset[0];
$this->db->sql_freeresult($result);
@ -254,7 +278,7 @@ class reset_password
'S_PROFILE_ACTION' => $this->helper->route('phpbb_ucp_forgot_password_controller'),
]);
return $this->helper->render('ucp_remind.html', $this->language->lang('UCP_REMIND'));
return $this->helper->render('ucp_reset_password.html', $this->language->lang('UCP_REMIND'));
}
/**
@ -272,12 +296,12 @@ class reset_password
if (empty($reset_token))
{
$this->helper->message('NO_RESET_TOKEN');
return $this->helper->message('NO_RESET_TOKEN');
}
if (!$user_id)
{
$this->helper->message('NO_USER');
return $this->helper->message('NO_USER');
}
add_form_key('ucp_remind');
@ -314,31 +338,33 @@ class reset_password
if (empty($user_row))
{
$this->helper->message($message);
return $this->helper->message($message);
}
if (!hash_equals($reset_token, $user_row['reset_token']))
{
$this->helper->message($message);
return $this->helper->message($message);
}
if ($user_row['reset_token_expiration'] < time())
{
$this->helper->message($message);
$this->remove_reset_token($user_id);
return $this->helper->message($message);
}
$error = [];
if ($submit)
{
if (!check_form_key('ucp_remind'))
{
trigger_error('FORM_INVALID');
return $this->helper->message('FORM_INVALID');
}
$message = $this->language->lang('PASSWORD_UPDATED_IF_EXISTED') . '<br /><br />' . $this->language->lang('RETURN_INDEX', '<a href="' . append_sid("{$this->root_path}index.{$this->php_ext}") . '">', '</a>');
if ($user_row['user_type'] == USER_IGNORE || $user_row['user_type'] == USER_INACTIVE)
{
trigger_error($message);
return $this->helper->message($message);
}
// Check users permissions
@ -347,46 +373,54 @@ class reset_password
if (!$auth2->acl_get('u_chgpasswd'))
{
trigger_error($message);
return $this->helper->message($message);
}
$server_url = generate_board_url();
if (!function_exists('validate_data'))
{
include($this->root_path . 'includes/functions_user.' . $this->php_ext);
}
// Make password at least 8 characters long, make it longer if admin wants to.
// gen_rand_string() however has a limit of 12 or 13.
$user_password = gen_rand_string_friendly(max(8, mt_rand((int) $this->config['min_pass_chars'], (int) $this->config['max_pass_chars'])));
// For the activation key a random length between 6 and 10 will do.
$user_actkey = gen_rand_string(mt_rand(6, 10));
$sql = 'UPDATE ' . USERS_TABLE . "
SET user_newpasswd = '" . $this->db->sql_escape($this->passwords_manager->hash($user_password)) . "', user_actkey = '" . $this->db->sql_escape($user_actkey) . "'
WHERE user_id = " . $user_row['user_id'];
$this->db->sql_query($sql);
include_once($this->root_path . 'includes/functions_messenger.' . $this->php_ext);
$messenger = new messenger(false);
$messenger->template('user_activate_passwd', $user_row['user_lang']);
$messenger->set_addresses($user_row);
$messenger->anti_abuse_headers($this->config, $this->user);
$messenger->assign_vars([
'USERNAME' => htmlspecialchars_decode($user_row['username']),
'PASSWORD' => htmlspecialchars_decode($user_password),
'U_ACTIVATE' => "$server_url/ucp.{$this->php_ext}?mode=activate&u={$user_row['user_id']}&k=$user_actkey"
]);
$messenger->send($user_row['user_notify_type']);
trigger_error($message);
$data = [
'new_password' => $this->request->untrimmed_variable('new_password', '', true),
'password_confirm' => $this->request->untrimmed_variable('new_password_confirm', '', true),
];
$check_data = [
'new_password' => [
['string', false, $this->config['min_pass_chars'], $this->config['max_pass_chars']],
['password'],
],
'password_confirm' => ['string', true, $this->config['min_pass_chars'], $this->config['max_pass_chars']],
];
$error = array_merge($error, validate_data($data, $check_data));
if (strcmp($data['new_password'], $data['password_confirm']) !== 0)
{
$error[] = ($data['password_confirm']) ? 'NEW_PASSWORD_ERROR' : 'NEW_PASSWORD_CONFIRM_EMPTY';
}
if (empty($error))
{
$sql_ary = [
'user_password' => $this->passwords_manager->hash($data['new_password']),
'user_login_attempts' => 0,
'reset_token' => '',
'reset_token_expiration' => 0,
];
$sql = 'UPDATE ' . $this->tables['users'] . '
SET ' . $this->db->sql_build_array('UPDATE', $sql_ary) . '
WHERE user_id = ' . (int) $user_row['user_id'];
$this->db->sql_query($sql);
$this->log->add('user', $user_row['user_id'], $this->user->ip, 'LOG_USER_NEW_PASSWORD', false, [
'reportee_id' => $user_row['user_id'],
$user_row['username']
]);
meta_refresh(3, append_sid("{$this->root_path}index.{$this->php_ext}"));
trigger_error($this->language->lang('PASSWORD_RESET'));
}
}
$this->template->assign_vars([
'S_IS_PASSWORD_RESET' => true,
'ERROR' => !empty($error) ? implode('<br />', array_map([$this->language, 'lang'], $error)) : '',
'S_PROFILE_ACTION' => $this->helper->route('phpbb_ucp_reset_password_controller'),
'S_HIDDEN_FIELDS' => build_hidden_fields([
'u' => $user_id,
@ -394,6 +428,6 @@ class reset_password
]),
]);
return $this->helper->render('ucp_remind.html', $this->language->lang('UCP_REMIND'));
return $this->helper->render('ucp_reset_password.html', $this->language->lang('UCP_REMIND'));
}
}

View file

@ -1,32 +1,44 @@
<!-- INCLUDE overall_header.html -->
<form action="{S_PROFILE_ACTION}" method="post" id="remind">
<form action="{{ S_PROFILE_ACTION }}" method="post" id="remind">
<div class="panel">
<div class="inner">
<div class="content">
<h2>{L_SEND_PASSWORD}</h2>
<h2>{{ lang('RESET_PASSWORD') }}</h2>
<fieldset>
{% if S_IS_PASSWORD_RESET %}
{% if ERROR %}<p class="error">{{ ERROR }}</p>{% endif %}
<dl>
<dt><label for="new_password">{{ lang('NEW_PASSWORD') ~ lang('COLON') }}</label></dt>
<dd><input type="password" name="new_password" id="new_password" size="25" maxlength="255" title="{{ lang('CHANGE_PASSWORD') }}" autocomplete="off" /></dd>
</dl>
<dl>
<dt><label for="new_password_confirm">{{ lang('CONFIRM_PASSWORD') ~ lang('COLON') }}</label></dt>
<dd><input type="password" name="new_password_confirm" id="new_password_confirm" size="25" maxlength="255" title="{{ lang('CONFIRM_PASSWORD') }}" autocomplete="off" /></dd>
</dl>
{% else %}
{% if USERNAME_REQUIRED %}
<p class="error">{{ lang('EMAIL_NOT_UNIQUE') }}</p>
{% endif %}
<dl>
<dt><label for="email">{L_EMAIL_ADDRESS}{L_COLON}</label><br /><span>{L_EMAIL_REMIND}</span></dt>
<dd><input class="inputbox narrow" type="email" name="email" id="email" size="25" maxlength="100" value="{{ EMAIL }}" autofocus /></dd>
</dl>
{% if USERNAME_REQUIRED %}
<dl>
<dt><label for="username">{L_USERNAME}{L_COLON}</label></dt>
<dd><input class="inputbox narrow" type="text" name="username" id="username" size="25" /></dd>
</dl>
<dl>
<dt><label for="email">{{ lang('EMAIL_ADDRESS') ~ lang('COLON') }}</label><br /><span>{{ lang('EMAIL_REMIND') }}</span></dt>
<dd><input class="inputbox narrow" type="email" name="email" id="email" size="25" maxlength="100" value="{{ EMAIL }}" autofocus /></dd>
</dl>
{% if USERNAME_REQUIRED %}
<dl>
<dt><label for="username">{{ lang('USERNAME') ~ lang('COLON') }}</label></dt>
<dd><input class="inputbox narrow" type="text" name="username" id="username" size="25" /></dd>
</dl>
{% endif %}
{% endif %}
<dl>
<dt>&nbsp;</dt>
<dd>{S_HIDDEN_FIELDS}<input type="submit" name="submit" id="submit" class="button1" value="{L_SUBMIT}" tabindex="2" />&nbsp; <input type="reset" value="{L_RESET}" name="reset" class="button2" /></dd>
<dd>{{ S_HIDDEN_FIELDS }}<input type="submit" name="submit" id="submit" class="button1" value="{{ lang('SUBMIT') }}" tabindex="2" />&nbsp; <input type="reset" value="{{ lang('RESET') }}" name="reset" class="button2" /></dd>
</dl>
{S_FORM_TOKEN}
{{ S_FORM_TOKEN }}
</fieldset>
</div>