Skip to content

Commit

Permalink
Implement password reset functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
carlbennett committed Aug 23, 2019
1 parent 8f541cb commit 7259639
Show file tree
Hide file tree
Showing 7 changed files with 314 additions and 46 deletions.
204 changes: 200 additions & 4 deletions src/controllers/User/ResetPassword.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,222 @@

namespace BNETDocs\Controllers\User;

use \BNETDocs\Models\User\ResetPassword as UserResetPasswordModel;
use \CarlBennett\MVC\Libraries\Common;
use \CarlBennett\MVC\Libraries\Controller;
use \CarlBennett\MVC\Libraries\Router;
use \CarlBennett\MVC\Libraries\Template;
use \CarlBennett\MVC\Libraries\View;

use \BNETDocs\Libraries\EventTypes;
use \BNETDocs\Libraries\Exceptions\UserNotFoundException;
use \BNETDocs\Libraries\Logger;
use \BNETDocs\Libraries\User;

use \BNETDocs\Models\User\ResetPassword as UserResetPasswordModel;

use \PHPMailer\PHPMailer\Exception;
use \PHPMailer\PHPMailer\PHPMailer;

use \InvalidArgumentException;
use \StdClass;

class ResetPassword extends Controller {
const RET_FAILURE = 0;
const RET_SUCCESS = 1;
const RET_EMAIL = 2;

public function &run( Router &$router, View &$view, array &$args ) {

public function &run(Router &$router, View &$view, array &$args) {
if ( $router->getRequestMethod() == 'GET' ) {
$data = $router->getRequestQueryArray();
} else {
$data = $router->getRequestBodyArray();
}

$model = new UserResetPasswordModel();

$view->render($model);
$model->error = null;
$model->csrf_id = isset( $data[ 'csrf_id' ]) ? $data[ 'csrf_id' ] : null;
$model->csrf_token = (
isset( $data[ 'csrf_token' ]) ? $data[ 'csrf_token' ] : null
);
$model->pw1 = isset( $data[ 'pw1' ]) ? $data[ 'pw1' ] : null;
$model->pw2 = isset( $data[ 'pw2' ]) ? $data[ 'pw2' ] : null;
$model->token = isset( $data[ 't' ]) ? $data[ 't' ] : null;
$model->user = null;
$model->username = isset( $data[ 'username' ]) ? $data[ 'username' ] : null;

if ( $router->getRequestMethod() == 'POST' ) {
$ret = $this->doPasswordReset( $model );
if ( $ret !== self::RET_EMAIL ) {
Logger::logEvent(
EventTypes::USER_PASSWORD_RESET,
( $model->user ? $model->user->getId() : null ),
getenv( 'REMOTE_ADDR' ),
json_encode([
'error' => $model->error,
'user' => ( $model->user ? true : false ),
'username' => $model->username,
])
);
}
}

$view->render( $model );

$model->_responseCode = 200;
$model->_responseHeaders["Content-Type"] = $view->getMimeType();
$model->_responseHeaders[ 'Content-Type' ] = $view->getMimeType();
$model->_responseTTL = 0;

return $model;

}

protected function doPasswordReset( UserResetPasswordModel &$model ) {
$model->error = 'INTERNAL_ERROR';

if ( empty( $model->username )) {
$model->error = 'EMPTY_USERNAME';
return self::RET_FAILURE;
}

try {
$model->user = new User( User::findIdByUsername( $model->username ));
} catch ( UserNotFoundException $e ) {
$model->user = null;
} catch ( InvalidArgumentException $e ) {
$model->user = null;
}

if ( !$model->user ) {
$model->error = 'USER_NOT_FOUND';
return self::RET_FAILURE;
}

if ( empty( $model->token )) {
$state = new StdClass();

$mail = new PHPMailer( true ); // true enables exceptions
$mail_config = Common::$config->email;

$state->mail &= $mail;
$state->token = $model->user->getVerificationToken();
$state->user = $model->user;

try {
//Server settings
$mail->Timeout = 10; // default is 300 per RFC2821 $ 4.5.3.2
$mail->SMTPDebug = 0;
$mail->isSMTP();
$mail->Host = $mail_config->smtp_host;
$mail->SMTPAuth = !empty($mail_config->smtp_user);
$mail->Username = $mail_config->smtp_user;
$mail->Password = $mail_config->smtp_password;
$mail->SMTPSecure = $mail_config->smtp_tls ? 'tls' : '';
$mail->Port = $mail_config->smtp_port;

//Recipients
if (!empty($mail_config->recipient_from)) {
$mail->setFrom($mail_config->recipient_from, 'BNETDocs');
}

$mail->addAddress($model->user->getEmail(), $model->user->getName());

if (!empty($mail_config->recipient_reply_to)) {
$mail->addReplyTo($mail_config->recipient_reply_to);
}

// Content
$mail->isHTML(true);
$mail->Subject = 'Reset Password';
$mail->CharSet = PHPMailer::CHARSET_UTF8;

ob_start();
(new Template($state, 'Email/User/ResetPassword.rich'))->render();
$mail->Body = ob_get_clean();

ob_start();
(new Template($state, 'Email/User/ResetPassword.plain'))->render();
$mail->AltBody = ob_get_clean();

$mail->send();

$model->error = false;

Logger::logEvent(
EventTypes::EMAIL_SENT,
$model->user->getId(),
getenv( 'REMOTE_ADDR' ),
json_encode([
'from' => $mail->From,
'to' => $mail->getToAddresses(),
'reply_to' => $mail->getReplyToAddresses(),
'subject' => $mail->Subject,
'content_type' => $mail->ContentType,
'body' => $mail->Body,
'alt_body' => $mail->AltBody,
])
);

} catch (\Exception $e) {
$model->error = 'EMAIL_FAILURE';
}

return self::RET_EMAIL;
}

if ( $model->token !== $model->user->getVerificationToken() ) {
$model->error = 'INVALID_TOKEN';
return self::RET_FAILURE;
}

if ( $model->pw1 !== $model->pw2 ) {
$model->error = 'PASSWORD_MISMATCH';
return self::RET_FAILURE;
}

$req = Common::$config->bnetdocs->user_register_requirements;
$pwlen = strlen( $model->pw1 );

if ( is_numeric( $req->password_length_max )
&& $pwlen > $req->password_length_max ) {
$model->error = 'PASSWORD_TOO_LONG';
return self::RET_FAILURE;
}

if ( is_numeric( $req->password_length_min )
&& $pwlen < $req->password_length_min ) {
$model->error = 'PASSWORD_TOO_SHORT';
return self::RET_FAILURE;
}

if ( !$req->password_allow_email
&& stripos( $model->pw1, $model->user->getEmail() )) {
$model->error = 'PASSWORD_CONTAINS_EMAIL';
return self::RET_FAILURE;
}

if ( !$req->password_allow_username
&& stripos( $model->pw1, $model->user->getUsername() )) {
$model->error = 'PASSWORD_CONTAINS_USERNAME';
return self::RET_FAILURE;
}

// --
$model->user->invalidateVerificationToken();
// --

if ( $model->user->getAcl( User::OPTION_DISABLED )) {
$model->error = 'USER_DISABLED';
return self::RET_FAILURE;
}

if (!$model->user->changePassword( $model->pw1 )) {
$model->error = 'INTERNAL_ERROR';
return self::RET_FAILURE;
}

$model->error = false;
return self::RET_SUCCESS;
}
}
4 changes: 3 additions & 1 deletion src/models/User/ResetPassword.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ class ResetPassword extends Model {

public $csrf_id;
public $csrf_token;
public $email;
public $error;
public $token;
public $user;
public $username;

}
4 changes: 2 additions & 2 deletions src/templates/Email/User/Register.plain.phtml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php
namespace BNETDocs\Templates\Email\User;
use \CarlBennett\MVC\Libraries\Common;
require("./Email/header.plain.inc.phtml");
require('./Email/header.plain.inc.phtml');
?>
Welcome to BNETDocs!

Expand All @@ -13,4 +13,4 @@ or copy and paste the link below into your browser to activate your account.

Note: This link will only be available for 24 hours after registering. If you
wait until it expires, you will need to complete a password reset.
<?php require("./Email/footer.plain.inc.phtml"); ?>
<?php require('./Email/footer.plain.inc.phtml'); ?>
4 changes: 2 additions & 2 deletions src/templates/Email/User/Register.rich.phtml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<?php
namespace BNETDocs\Templates\Email\User;
use \CarlBennett\MVC\Libraries\Common;
require("./Email/header.rich.inc.phtml");
require('./Email/header.rich.inc.phtml');
$url = Common::relativeUrlToAbsolute('/user/activate?u=' . rawurlencode($this->getContext()->user_id) . '&t=' . rawurlencode($this->getContext()->token));
?>
<p style="font-family:sans-serif;font-weight:bold;">Welcome to BNETDocs!</p>
<p style="font-family:sans-serif;">Your account requires activation before being able to use this service. Click or copy and paste the link below into your browser to activate your account.</p>
<p style="font-family:sans-serif;"><a href="<?=$url?>" rel="external"><?=filter_var($url, FILTER_SANITIZE_FULL_SPECIAL_CHARS)?></a></p>
<p style="font-family:sans-serif;"><strong>Note:</strong> This link will only be available for 24 hours after registering. If you wait until it expires, you will need to complete a password reset.</p>
<?php require("./Email/footer.rich.inc.phtml"); ?>
<?php require('./Email/footer.rich.inc.phtml'); ?>
12 changes: 8 additions & 4 deletions src/templates/Email/User/ResetPassword.plain.phtml
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
<?php require('./Email/header.plain.inc.phtml'); ?>
<?php
namespace BNETDocs\Templates\Email\User;
use \CarlBennett\MVC\Libraries\Common;
require('./Email/header.plain.inc.phtml');
?>
Hello <?=$this->getContext()->user->getName()?>,

You requested your password to be reset on BNETDocs. If this was you, copy and
paste the following into your web browser:
Someone requested your password to be reset on BNETDocs. If this was you, copy
and paste the following into your web browser:

<?=$this->getContext()->url?>
<?=Common::relativeUrlToAbsolute('/user/resetpassword?username=' . rawurlencode($this->getContext()->user->getUsername()) . '&t=' . rawurlencode($this->getContext()->token))?>

If this was not you, then you can safely ignore this email; no action will be
taken.
Expand Down
9 changes: 5 additions & 4 deletions src/templates/Email/User/ResetPassword.rich.phtml
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<?php

namespace BNETDocs\Templates\Email\User;

$name = $this->getContext()->user->getName();
$url = $this->getContext()->url;
use \CarlBennett\MVC\Libraries\Common;

$name = filter_var($this->getContext()->user->getName(), FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$url = Common::relativeUrlToAbsolute('/user/resetpassword?username=' . rawurlencode($this->getContext()->user->getUsername()) . '&t=' . rawurlencode($this->getContext()->token));

?><!DOCTYPE html "-//w3c//dtd xhtml 1.0 transitional //en" "http://www.w3.org/tr/xhtml1/dtd/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head>
<!--[if gte mso 9]><xml>
Expand Down Expand Up @@ -173,7 +174,7 @@ $url = $this->getContext()->url;
<tbody><tr style="vertical-align: top">
<td style="word-break: break-word;border-collapse: collapse !important;vertical-align: top;padding-top: 10px;padding-right: 10px;padding-bottom: 10px;padding-left: 10px">
<div style="color:#DDD;line-height:120%;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;">
<div style="font-size:12px;line-height:14px;color:#DDD;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;text-align:left;"><p style="margin: 0;font-size: 12px;line-height: 14px"><span style="font-size: 14px; line-height: 16px;">Hello <?php echo $name; ?>,</span></p></div><div style="font-size:12px;line-height:14px;color:#DDD;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;text-align:left;"><p style="margin: 0;font-size: 12px;line-height: 14px"><br></p></div><div style="font-size:12px;line-height:14px;color:#DDD;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;text-align:left;"><p style="margin: 0;font-size: 12px;line-height: 14px"><span style="font-size: 14px; line-height: 16px;">You requested your password to be reset on BNETDocs. If this was you, click the following:</span></p></div><div style="font-size:12px;line-height:14px;color:#DDD;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;text-align:left;"><p style="margin: 0;font-size: 12px;line-height: 14px"><br></p></div><div style="font-size:12px;line-height:14px;color:#DDD;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;text-align:left;"><p style="margin: 0;font-size: 12px;line-height: 14px"><span style="font-size: 14px; line-height: 16px;"><?php echo $url; ?></span><br></p></div><div style="font-size:12px;line-height:14px;color:#DDD;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;text-align:left;"><p style="margin: 0;font-size: 12px;line-height: 14px"><br></p></div><div style="font-size:12px;line-height:14px;color:#DDD;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;text-align:left;"><p style="margin: 0;font-size: 12px;line-height: 14px"><span style="font-size: 14px; line-height: 16px;">If this was not you, then you can safely ignore this email; no action will be taken.</span></p></div><div style="font-size:12px;line-height:14px;color:#DDD;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;text-align:left;"><p style="margin: 0;font-size: 12px;line-height: 14px"><br></p></div><div style="font-size:12px;line-height:14px;color:#DDD;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;text-align:left;"><p style="margin: 0;font-size: 12px;line-height: 14px"><span style="font-size: 14px; line-height: 16px;">Best,</span></p></div><div style="font-size:12px;line-height:14px;color:#DDD;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;text-align:left;"><p style="margin: 0;font-size: 12px;line-height: 14px"><br></p></div><div style="font-size:12px;line-height:14px;color:#DDD;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;text-align:left;"><p style="margin: 0;font-size: 12px;line-height: 14px"><span style="font-size: 14px; line-height: 16px;">BNETDocs Staff</span></p></div>
<div style="font-size:12px;line-height:14px;color:#DDD;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;text-align:left;"><p style="margin: 0;font-size: 12px;line-height: 14px"><span style="font-size: 14px; line-height: 16px;">Hello <?php echo $name; ?>,</span></p></div><div style="font-size:12px;line-height:14px;color:#DDD;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;text-align:left;"><p style="margin: 0;font-size: 12px;line-height: 14px"><br></p></div><div style="font-size:12px;line-height:14px;color:#DDD;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;text-align:left;"><p style="margin: 0;font-size: 12px;line-height: 14px"><span style="font-size: 14px; line-height: 16px;">Someone requested your password to be reset on BNETDocs. If this was you, click the following:</span></p></div><div style="font-size:12px;line-height:14px;color:#DDD;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;text-align:left;"><p style="margin: 0;font-size: 12px;line-height: 14px"><br></p></div><div style="font-size:12px;line-height:14px;color:#DDD;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;text-align:left;"><p style="margin: 0;font-size: 12px;line-height: 14px"><span style="font-size: 14px; line-height: 16px;"><a style="color: #00DEBF;text-decoration: none;" href="<?=$url?>"><?=$url?></a></span><br></p></div><div style="font-size:12px;line-height:14px;color:#DDD;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;text-align:left;"><p style="margin: 0;font-size: 12px;line-height: 14px"><br></p></div><div style="font-size:12px;line-height:14px;color:#DDD;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;text-align:left;"><p style="margin: 0;font-size: 12px;line-height: 14px"><span style="font-size: 14px; line-height: 16px;">If this was not you, then you can safely ignore this email; no action will be taken.</span></p></div><div style="font-size:12px;line-height:14px;color:#DDD;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;text-align:left;"><p style="margin: 0;font-size: 12px;line-height: 14px"><br></p></div><div style="font-size:12px;line-height:14px;color:#DDD;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;text-align:left;"><p style="margin: 0;font-size: 12px;line-height: 14px"><span style="font-size: 14px; line-height: 16px;">Best,</span></p></div><div style="font-size:12px;line-height:14px;color:#DDD;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;text-align:left;"><p style="margin: 0;font-size: 12px;line-height: 14px"><br></p></div><div style="font-size:12px;line-height:14px;color:#DDD;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;text-align:left;"><p style="margin: 0;font-size: 12px;line-height: 14px"><span style="font-size: 14px; line-height: 16px;">BNETDocs Staff</span></p></div>
</div>
</td>
</tr>
Expand Down
Loading

0 comments on commit 7259639

Please sign in to comment.