adm_program/system/password_reset.php
<?php
/**
***********************************************************************************************
* Show form where user can request a new password and handle the request
*
* @copyright The Admidio Team
* @see https://www.admidio.org/
* @license https://www.gnu.org/licenses/gpl-2.0.html GNU General Public License v2.0 only
*
* Parameters:
*
* id : Validation id for the link if this is a valid password reset request
* user_uuid : UUID of the user who wants a reset his password
***********************************************************************************************
*/
use Admidio\Exception;
use Admidio\UserInterface\Form;
try {
require_once(__DIR__ . '/common.php');
// Initialize and check the parameters
$getResetId = admFuncVariableIsValid($_GET, 'id', 'string');
$getUserUuid = admFuncVariableIsValid($_GET, 'user_uuid', 'uuid');
// "systemmail" and "request password" must be activated
if (!$gSettingsManager->getBool('system_notifications_enabled') || !$gSettingsManager->getBool('enable_password_recovery')) {
throw new Exception('SYS_MODULE_DISABLED');
}
if ($gValidLogin) {
$gMessage->setForwardUrl(ADMIDIO_URL . '/adm_program/', 2000);
throw new Exception('SYS_RESET_PW_AREADY_LOGGED_IN');
}
if ($getUserUuid !== '') {
// user has clicked the link in his email, and now we must check if it's a valid request and then show password form
// search for user with the email address that have a valid login and membership to a role
$sql = 'SELECT usr_id, usr_pw_reset_timestamp
FROM ' . TBL_USERS . '
WHERE usr_uuid = ? -- $getUserUuid
AND usr_pw_reset_id = ? -- $getResetId
AND usr_valid = true ';
$queryParams = array(
$getUserUuid,
$getResetId
);
$userStatement = $gDb->queryPrepared($sql, $queryParams);
if ($userStatement->rowCount() === 1) {
// if the reset id was requested for more than 20 minutes -> show invalid page view
$row = $userStatement->fetch();
$timeGap = time() - strtotime($row['usr_pw_reset_timestamp']);
if ($timeGap > 20 * 60) {
throw new Exception('SYS_PASSWORD_RESET_INVALID', array('<a href="' . ADMIDIO_URL . FOLDER_SYSTEM . '/password_reset.php">' . $gL10n->get('SYS_PASSWORD_FORGOTTEN') . '</a>'));
}
} else {
throw new Exception('SYS_PASSWORD_RESET_INVALID', array('<a href="' . ADMIDIO_URL . FOLDER_SYSTEM . '/password_reset.php">' . $gL10n->get('SYS_PASSWORD_FORGOTTEN') . '</a>'));
}
$user = new User($gDb, $gProfileFields, $row['usr_id']);
$gNavigation->clear();
if (!empty($_POST['new_password'])) {
try {
// check form field input and sanitized it from malicious content
$passwordResetSetPasswordForm = $gCurrentSession->getFormObject($_POST['admidio-csrf-token']);
$formValues = $passwordResetSetPasswordForm->validate($_POST);
// check password and save new password in database
$newPassword = $formValues['new_password'];
$newPasswordConfirm = $formValues['new_password_confirm'];
// Handle form input
if (strlen($newPassword) >= PASSWORD_MIN_LENGTH) {
if (PasswordUtils::passwordStrength($newPassword, $user->getPasswordUserData()) >= $gSettingsManager->getInt('password_min_strength')) {
if ($newPassword === $newPasswordConfirm) {
$user->saveChangesWithoutRights();
$user->setPassword($newPassword);
$user->setValue('usr_pw_reset_id', '');
$user->setValue('usr_pw_reset_timestamp', '');
$user->save();
// if user has tried login several times we should reset the invalid counter,
// so he could log in with the new password immediately
$user->resetInvalidLogins();
echo json_encode(array(
'status' => 'success',
'message' => $gL10n->get('SYS_PASSWORD_RESET_SAVED'),
'url' => ADMIDIO_URL . FOLDER_SYSTEM . '/login.php'
));
exit();
} else {
throw new Exception('SYS_PASSWORDS_NOT_EQUAL');
}
} else {
throw new Exception('SYS_PASSWORD_NOT_STRONG_ENOUGH');
}
} else {
throw new Exception('SYS_PASSWORD_LENGTH');
}
} catch (Exception $e) {
echo json_encode(array('status' => 'error', 'message' => $e->getMessage()));
}
} else {
// show dialog to change password
$page = new HtmlPage('admidio-profile-photo-edit', $gL10n->get('SYS_CHANGE_PASSWORD'));
// show form
$form = new Form(
'passwordResetSetPasswordForm',
'system/password-reset.set-password.tpl',
SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_SYSTEM . '/password_reset.php', array('user_uuid' => $getUserUuid, 'id' => $getResetId)),
$page
);
$form->addInput(
'new_password',
$gL10n->get('SYS_NEW_PASSWORD'),
'',
array(
'type' => 'password',
'property' => Form::FIELD_REQUIRED,
'minLength' => PASSWORD_MIN_LENGTH,
'passwordStrength' => true,
'passwordUserData' => $user->getPasswordUserData(),
'helpTextId' => 'SYS_PASSWORD_DESCRIPTION'
)
);
$form->addInput(
'new_password_confirm',
$gL10n->get('SYS_REPEAT'),
'',
array('type' => 'password', 'property' => Form::FIELD_REQUIRED, 'minLength' => PASSWORD_MIN_LENGTH)
);
$form->addSubmitButton(
'btn_save',
$gL10n->get('SYS_SAVE'),
array('icon' => 'bi-check-lg')
);
$form->addToHtmlPage();
$gCurrentSession->addFormObject($form);
$page->show();
}
} elseif (!empty($_POST['recipient_email'])) {
// password reset form was send, and now we should create an email for the user
try {
// check form field input and sanitized it from malicious content
$passwordResetForm = $gCurrentSession->getFormObject($_POST['admidio-csrf-token']);
$formValues = $passwordResetForm->validate($_POST);
if (StringUtils::strValidCharacters($formValues['recipient_email'], 'email')) {
// search for user with the email address that have a valid login and membership to a role
$sql = 'SELECT usr_id
FROM ' . TBL_MEMBERS . '
INNER JOIN ' . TBL_ROLES . '
ON rol_id = mem_rol_id
INNER JOIN ' . TBL_CATEGORIES . '
ON cat_id = rol_cat_id
INNER JOIN ' . TBL_USERS . '
ON usr_id = mem_usr_id
INNER JOIN ' . TBL_USER_DATA . ' AS email
ON email.usd_usr_id = usr_id
AND email.usd_usf_id = ? -- $gProfileFields->getProperty(\'EMAIL\', \'usf_id\')
AND email.usd_value = ? -- $formValues[\'recipient_email\']
WHERE LENGTH(usr_login_name) > 0
AND rol_valid = true
AND usr_valid = true
AND mem_begin <= ? -- DATE_NOW
AND mem_end > ? -- DATE_NOW
GROUP BY usr_id';
$queryParams = array(
$gProfileFields->getProperty('EMAIL', 'usf_id'),
$_POST['recipient_email'],
DATE_NOW,
DATE_NOW
);
$userStatement = $gDb->queryPrepared($sql, $queryParams);
$count = $userStatement->rowCount();
} else {
// first try to find user with username. Also, an email could be a username.
$sql = 'SELECT usr_id
FROM ' . TBL_MEMBERS . '
INNER JOIN ' . TBL_ROLES . '
ON rol_id = mem_rol_id
INNER JOIN ' . TBL_CATEGORIES . '
ON cat_id = rol_cat_id
INNER JOIN ' . TBL_USERS . '
ON usr_id = mem_usr_id
WHERE usr_login_name = ? -- $formValues[\'recipient_email\']
AND rol_valid = true
AND usr_valid = true
AND mem_begin <= ? -- DATE_NOW
AND mem_end > ? -- DATE_NOW
GROUP BY usr_id';
$queryParams = array(
$formValues['recipient_email'],
DATE_NOW,
DATE_NOW
);
$userStatement = $gDb->queryPrepared($sql, $queryParams);
$count = $userStatement->rowCount();
}
// show error if more than one user found
if ($count > 1) {
throw new Exception('SYS_LOSTPW_SEVERAL_EMAIL', array($formValues['recipient_email']));
} elseif ($count === 1) {
// a valid username or email was found then send new password
$user = new User($gDb, $gProfileFields, (int)$userStatement->fetchColumn());
// create an activation id
$passwordResetId = SecurityUtils::getRandomString(50);
$user->setValue('usr_pw_reset_id', $passwordResetId);
$user->setValue('usr_pw_reset_timestamp', DATETIME_NOW);
$sysmail = new SystemMail($gDb);
$sysmail->addRecipientsByUser($user->getValue('usr_uuid'));
$sysmail->setVariable(1, SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_SYSTEM . '/password_reset.php', array('user_uuid' => $user->getValue('usr_uuid'), 'id' => $passwordResetId)));
$sysmail->sendSystemMail('SYSMAIL_PASSWORD_RESET', $user);
$user->saveChangesWithoutRights();
$user->save(false);
}
// always show a positive feedback to prevent hackers to validate an email-address or username
$gMessage->setForwardUrl(ADMIDIO_URL . FOLDER_SYSTEM . '/login.php');
if (StringUtils::strValidCharacters($_POST['recipient_email'], 'email')) {
$message = $gL10n->get('SYS_LOSTPW_SEND_EMAIL', array($_POST['recipient_email']));
} else {
$message = $gL10n->get('SYS_LOSTPW_SEND_USERNAME', array($_POST['recipient_email']));
}
echo json_encode(array(
'status' => 'success',
'message' => $message,
'url' => ADMIDIO_URL . FOLDER_SYSTEM . '/login.php'
));
exit();
} catch (Exception $e) {
if (isset($user)) {
// initialize password reset columns
$user->setValue('usr_pw_reset_id', '');
$user->setValue('usr_pw_reset_timestamp', '');
$user->saveChangesWithoutRights();
$user->save(false);
}
echo json_encode(array('status' => 'error', 'message' => $e->getMessage()));
}
} else {
// HTML_PART
$headline = $gL10n->get('SYS_PASSWORD_FORGOTTEN');
// save url to navigation stack
$gNavigation->addUrl(CURRENT_URL, $headline);
// create html page object
$page = new HtmlPage('admidio-password-reset', $headline);
$page->addHtml('<p class="lead">' . $gL10n->get('SYS_PASSWORD_FORGOTTEN_DESCRIPTION') . '</p>');
// show form
$form = new Form(
'passwordResetForm',
'system/password-reset.tpl',
ADMIDIO_URL . FOLDER_SYSTEM . '/password_reset.php',
$page
);
$form->addInput(
'recipient_email',
$gL10n->get('SYS_USERNAME_OR_EMAIL'),
'',
array('maxLength' => 254, 'property' => Form::FIELD_REQUIRED)
);
// if captchas are enabled then visitors of the website must resolve this
if (!$gValidLogin && $gSettingsManager->getBool('enable_mail_captcha')) {
$form->addCaptcha('captcha_code');
}
$form->addSubmitButton(
'btn_send',
$gL10n->get('SYS_SEND'),
array('icon' => 'bi-envelope-fill', 'class' => 'offset-sm-3')
);
$form->addToHtmlPage();
$gCurrentSession->addFormObject($form);
$page->show();
}
} catch (Exception $e) {
$gMessage->show($e->getMessage());
}