yf_get_pswd user module| File | plugins/user/modules/yf_get_pswd.class.php |
|---|---|
| Use | module('get_pswd'), URL ?object=get_pswd |
A site sets them in its config with $PROJECT_CONF['get_pswd']['NAME'] = ...;
| Property | Default | Description |
|---|---|---|
$RESET_BY_TOKENbool |
false |
Mail a single-use reset link instead of the stored password (previous behaviour: mail the password). Forced when the stored password is a hash |
$TOKEN_TTLint |
3600 |
Reset link lifetime, seconds |
$_my_site_title |
null |
|
$_sent_reset_linkbool |
false |
@conf_skip |
public function reset()
/get_pswd/reset/<token>: choose a new password.
public function reset()
{
$row = $this->_find_reset($_GET['id'] ?? '');
if ( ! $row) {
return _e(t('This password reset link is invalid or has expired. Request a new one.'));
}
$user_id = (int) $row['user_id'];
$stored = (string) db()->get_one('SELECT password FROM ' . db('user') . ' WHERE id = ' . $user_id);
$password = _class('password');
// an account that already holds a hash never goes back to the legacy form
$hash_it = $password->HASH_ON_WRITE || $password->is_hash($stored);
if ($hash_it && ! $password->column_fits('user')) {
return _e(t('Password reset is not available: contact the site administrator'));
}
$rule = $hash_it ? 'hash_password' : 'password_store';
$a = $_POST;
$a['redirect_link'] = url('/login_form');
return form($a, ['legend' => t('Choose a new password'), 'class' => 'form-vertical'])
->validate([
'__form_id__' => 'pswd_reset_form',
'password' => 'trim|required|min_length[6]|max_length[72]|' . $rule,
'pswdconf' => 'trim|required|matches[password]',
])
->on_validate_ok(function ($data, $extra, $form) use ($row) {
// single use, claimed before the write so two parallel posts cannot both succeed
db()->query('UPDATE ' . db('user_pswd_reset') . ' SET used = 1 WHERE id = ' . (int) $row['id'] . ' AND used = 0');
if (db()->affected_rows() < 1) {
$form->_validate_ok = false;
_re(t('This password reset link was already used'));
}
})
->db_update_if_ok('user', ['password'], 'id=' . $user_id, ['on_after_update' => function () use ($user_id) {
db()->query('UPDATE ' . db('user_pswd_reset') . ' SET used = 1 WHERE user_id = ' . $user_id);
// remembered browsers must not outlive the password they were issued with
_class('auth_user', 'classes/auth/')->_remember_revoke_user($user_id);
common()->message_success(t('Your password has been changed, you can log in now'));
}])
->password('password', t('New password'))
->password('pswdconf', t('Repeat the new password'))
->save();
}
public function show()
public function show()
{
conf('_login_form_displayed', true);
if ( ! empty($_POST)) {
if (empty($_POST['login']) && empty($_POST['email'])) {
_re('Login or Email required');
}
// Check if user with such login exists
if ( ! empty($_POST['login'])) {
$A = db()->query_fetch('SELECT id,name,login,password,email,nick FROM ' . db('user') . ' WHERE login="' . _es($_POST['login'] ?? null) . '"');
if (empty($A['id'])) {
_re('Login was not found', 'login');
}
if ( ! common()->_error_exists()) {
$result = $this->_send_info_to_user($A);
if ( ! $result) {
_re('Server mail error');
}
}
// Check if user with such email exists
} elseif ( ! empty($_POST['email'])) {
$Q = db()->query('SELECT id,name,login,password,email,nick FROM ' . db('user') . ' WHERE email="' . _es($_POST['email'] ?? null) . '"');
if ( ! db()->num_rows($Q)) {
_re('Email was not found', 'email');
}
// Check if errors exists and send all found accounts
if ( ! common()->_error_exists()) {
while ($A = db()->fetch_assoc($Q)) {
$result = $this->_send_info_to_user($A);
if ( ! $result) {
_re('Server mail error');
}
}
}
}
if ( ! common()->_error_exists()) {
$success_msg = $this->_sent_reset_link
? t('A link to choose a new password has been sent to your email address. It works once and expires soon.')
: t('Password has been sent to your email address. It should arrive in a couple of minutes.');
}
}
$replace = [
'form_action' => './?object=' . ($_GET['object'] ?? null),
];
$login_form = form($replace, ['legend' => 'Enter your login', 'class' => 'form-vertical'])
->validate(['login' => 'trim|required'])
->text('login', 'Enter your login')
->submit('', 'Get Password', ['class' => 'btn btn-small']);
$email_form = form($replace, ['legend' => 'Enter your email', 'class' => 'form-vertical'])
->validate(['email' => 'trim|required'])
->email('email', 'Enter your email')
->submit('', 'Get Password', ['class' => 'btn btn-small']);
return tpl()->parse(__CLASS__ . '/main', [
'error' => _e(),
'success' => ! empty($success_msg) ? $success_msg : '',
'login_form' => $login_form,
'email_form' => $email_form,
]);
}
_ hooks and helpers; not routable as page actionspublic function _find_reset($token)
Row of a valid (unused, unexpired) reset token, or false.
public function _find_reset($token)
{
$token = (string) $token;
if ( ! preg_match('~^[0-9a-f]{64}$~', $token)) {
return false;
}
$row = db()->get('SELECT * FROM ' . db('user_pswd_reset') . ' WHERE token_hash="' . _es(_class('password')->hash_token($token)) . '" AND used=0 AND expires > ' . time());
return ! empty($row['id']) ? $row : false;
}
public function _init()
public function _init()
{
$this->_my_site_title = t('Password Reminder');
}
public function _send_info_to_user($user = [])
public function _send_info_to_user($user = [])
{
if (empty($user)) {
return false;
}
if ($this->RESET_BY_TOKEN || _class('password')->is_hash($user['password'] ?? '')) {
return $this->_send_reset_link($user);
}
$html = tpl()->parse('@object/email', [
'user_name' => _display_name($user),
'password' => $user['password'],
'login' => $user['login'],
'advert_name' => SITE_ADVERT_NAME,
'home_url' => url('/'),
'login_url' => url('/login_form'),
'faq_url' => url('/faq'),
]);
return common()->send_mail([
'from_mail' => SITE_ADMIN_EMAIL,
'from_name' => SITE_ADVERT_NAME,
'to_mail' => $user['email'],
'to_name' => _display_name($user),
'subj' => t('Password Found'),
'html' => $html,
'text' => nl2br(strip_tags($html)),
// TODO: implement these inside send_mail
'on_error' => function ($params) {
common()->message_error('Server cannot send email to you, please contact support');
},
// TODO: implement these inside send_mail
'on_success' => function ($params) {
common()->message_success('Email was sent successfully');
},
]);
}
public function _send_reset_link($user = [])
Single-use token: only its sha256 is stored, the plain token goes into the mailed link.
public function _send_reset_link($user = [])
{
if (empty($user['id']) || empty($user['email'])) {
return false;
}
$password = _class('password');
$token = $password->new_token();
// one outstanding link per account; expired rows of everybody go with it
db()->query('DELETE FROM ' . db('user_pswd_reset') . ' WHERE user_id = ' . (int) $user['id'] . ' OR expires < ' . time());
$ok = db()->insert('user_pswd_reset', db()->_es_unless_builder_escapes([
'user_id' => (int) $user['id'],
'token_hash' => $password->hash_token($token),
'expires' => time() + (int) $this->TOKEN_TTL,
'used' => 0,
]));
if ( ! $ok) {
return false;
}
$this->_sent_reset_link = true;
$html = tpl()->parse('@object/email_reset', [
'user_name' => _display_name($user),
'login' => $user['login'],
'reset_url' => url('/@object/reset/' . $token),
'ttl_minutes' => (int) ceil($this->TOKEN_TTL / 60),
'advert_name' => SITE_ADVERT_NAME,
'home_url' => url('/'),
]);
return common()->send_mail([
'from_mail' => SITE_ADMIN_EMAIL,
'from_name' => SITE_ADVERT_NAME,
'to_mail' => $user['email'],
'to_name' => _display_name($user),
'subj' => t('Password reset'),
'html' => nl2br($html),
'text' => $html,
'on_error' => function ($params) {
common()->message_error('Server cannot send email to you, please contact support');
},
'on_success' => function ($params) {
common()->message_success('Email was sent successfully');
},
]);
}
public function _site_title($title)
public function _site_title($title)
{
return $this->_my_site_title;
}