Skip to content

Commit 7259639

Browse files
committed
Implement password reset functionality
1 parent 8f541cb commit 7259639

File tree

7 files changed

+314
-46
lines changed

7 files changed

+314
-46
lines changed

src/controllers/User/ResetPassword.php

Lines changed: 200 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,222 @@
22

33
namespace BNETDocs\Controllers\User;
44

5-
use \BNETDocs\Models\User\ResetPassword as UserResetPasswordModel;
65
use \CarlBennett\MVC\Libraries\Common;
76
use \CarlBennett\MVC\Libraries\Controller;
87
use \CarlBennett\MVC\Libraries\Router;
8+
use \CarlBennett\MVC\Libraries\Template;
99
use \CarlBennett\MVC\Libraries\View;
1010

11+
use \BNETDocs\Libraries\EventTypes;
12+
use \BNETDocs\Libraries\Exceptions\UserNotFoundException;
13+
use \BNETDocs\Libraries\Logger;
14+
use \BNETDocs\Libraries\User;
15+
16+
use \BNETDocs\Models\User\ResetPassword as UserResetPasswordModel;
17+
18+
use \PHPMailer\PHPMailer\Exception;
19+
use \PHPMailer\PHPMailer\PHPMailer;
20+
21+
use \InvalidArgumentException;
22+
use \StdClass;
23+
1124
class ResetPassword extends Controller {
25+
const RET_FAILURE = 0;
26+
const RET_SUCCESS = 1;
27+
const RET_EMAIL = 2;
28+
29+
public function &run( Router &$router, View &$view, array &$args ) {
1230

13-
public function &run(Router &$router, View &$view, array &$args) {
31+
if ( $router->getRequestMethod() == 'GET' ) {
32+
$data = $router->getRequestQueryArray();
33+
} else {
34+
$data = $router->getRequestBodyArray();
35+
}
1436

1537
$model = new UserResetPasswordModel();
1638

17-
$view->render($model);
39+
$model->error = null;
40+
$model->csrf_id = isset( $data[ 'csrf_id' ]) ? $data[ 'csrf_id' ] : null;
41+
$model->csrf_token = (
42+
isset( $data[ 'csrf_token' ]) ? $data[ 'csrf_token' ] : null
43+
);
44+
$model->pw1 = isset( $data[ 'pw1' ]) ? $data[ 'pw1' ] : null;
45+
$model->pw2 = isset( $data[ 'pw2' ]) ? $data[ 'pw2' ] : null;
46+
$model->token = isset( $data[ 't' ]) ? $data[ 't' ] : null;
47+
$model->user = null;
48+
$model->username = isset( $data[ 'username' ]) ? $data[ 'username' ] : null;
49+
50+
if ( $router->getRequestMethod() == 'POST' ) {
51+
$ret = $this->doPasswordReset( $model );
52+
if ( $ret !== self::RET_EMAIL ) {
53+
Logger::logEvent(
54+
EventTypes::USER_PASSWORD_RESET,
55+
( $model->user ? $model->user->getId() : null ),
56+
getenv( 'REMOTE_ADDR' ),
57+
json_encode([
58+
'error' => $model->error,
59+
'user' => ( $model->user ? true : false ),
60+
'username' => $model->username,
61+
])
62+
);
63+
}
64+
}
65+
66+
$view->render( $model );
1867

1968
$model->_responseCode = 200;
20-
$model->_responseHeaders["Content-Type"] = $view->getMimeType();
69+
$model->_responseHeaders[ 'Content-Type' ] = $view->getMimeType();
2170
$model->_responseTTL = 0;
2271

2372
return $model;
2473

2574
}
2675

76+
protected function doPasswordReset( UserResetPasswordModel &$model ) {
77+
$model->error = 'INTERNAL_ERROR';
78+
79+
if ( empty( $model->username )) {
80+
$model->error = 'EMPTY_USERNAME';
81+
return self::RET_FAILURE;
82+
}
83+
84+
try {
85+
$model->user = new User( User::findIdByUsername( $model->username ));
86+
} catch ( UserNotFoundException $e ) {
87+
$model->user = null;
88+
} catch ( InvalidArgumentException $e ) {
89+
$model->user = null;
90+
}
91+
92+
if ( !$model->user ) {
93+
$model->error = 'USER_NOT_FOUND';
94+
return self::RET_FAILURE;
95+
}
96+
97+
if ( empty( $model->token )) {
98+
$state = new StdClass();
99+
100+
$mail = new PHPMailer( true ); // true enables exceptions
101+
$mail_config = Common::$config->email;
102+
103+
$state->mail &= $mail;
104+
$state->token = $model->user->getVerificationToken();
105+
$state->user = $model->user;
106+
107+
try {
108+
//Server settings
109+
$mail->Timeout = 10; // default is 300 per RFC2821 $ 4.5.3.2
110+
$mail->SMTPDebug = 0;
111+
$mail->isSMTP();
112+
$mail->Host = $mail_config->smtp_host;
113+
$mail->SMTPAuth = !empty($mail_config->smtp_user);
114+
$mail->Username = $mail_config->smtp_user;
115+
$mail->Password = $mail_config->smtp_password;
116+
$mail->SMTPSecure = $mail_config->smtp_tls ? 'tls' : '';
117+
$mail->Port = $mail_config->smtp_port;
118+
119+
//Recipients
120+
if (!empty($mail_config->recipient_from)) {
121+
$mail->setFrom($mail_config->recipient_from, 'BNETDocs');
122+
}
123+
124+
$mail->addAddress($model->user->getEmail(), $model->user->getName());
125+
126+
if (!empty($mail_config->recipient_reply_to)) {
127+
$mail->addReplyTo($mail_config->recipient_reply_to);
128+
}
129+
130+
// Content
131+
$mail->isHTML(true);
132+
$mail->Subject = 'Reset Password';
133+
$mail->CharSet = PHPMailer::CHARSET_UTF8;
134+
135+
ob_start();
136+
(new Template($state, 'Email/User/ResetPassword.rich'))->render();
137+
$mail->Body = ob_get_clean();
138+
139+
ob_start();
140+
(new Template($state, 'Email/User/ResetPassword.plain'))->render();
141+
$mail->AltBody = ob_get_clean();
142+
143+
$mail->send();
144+
145+
$model->error = false;
146+
147+
Logger::logEvent(
148+
EventTypes::EMAIL_SENT,
149+
$model->user->getId(),
150+
getenv( 'REMOTE_ADDR' ),
151+
json_encode([
152+
'from' => $mail->From,
153+
'to' => $mail->getToAddresses(),
154+
'reply_to' => $mail->getReplyToAddresses(),
155+
'subject' => $mail->Subject,
156+
'content_type' => $mail->ContentType,
157+
'body' => $mail->Body,
158+
'alt_body' => $mail->AltBody,
159+
])
160+
);
161+
162+
} catch (\Exception $e) {
163+
$model->error = 'EMAIL_FAILURE';
164+
}
165+
166+
return self::RET_EMAIL;
167+
}
168+
169+
if ( $model->token !== $model->user->getVerificationToken() ) {
170+
$model->error = 'INVALID_TOKEN';
171+
return self::RET_FAILURE;
172+
}
173+
174+
if ( $model->pw1 !== $model->pw2 ) {
175+
$model->error = 'PASSWORD_MISMATCH';
176+
return self::RET_FAILURE;
177+
}
178+
179+
$req = Common::$config->bnetdocs->user_register_requirements;
180+
$pwlen = strlen( $model->pw1 );
181+
182+
if ( is_numeric( $req->password_length_max )
183+
&& $pwlen > $req->password_length_max ) {
184+
$model->error = 'PASSWORD_TOO_LONG';
185+
return self::RET_FAILURE;
186+
}
187+
188+
if ( is_numeric( $req->password_length_min )
189+
&& $pwlen < $req->password_length_min ) {
190+
$model->error = 'PASSWORD_TOO_SHORT';
191+
return self::RET_FAILURE;
192+
}
193+
194+
if ( !$req->password_allow_email
195+
&& stripos( $model->pw1, $model->user->getEmail() )) {
196+
$model->error = 'PASSWORD_CONTAINS_EMAIL';
197+
return self::RET_FAILURE;
198+
}
199+
200+
if ( !$req->password_allow_username
201+
&& stripos( $model->pw1, $model->user->getUsername() )) {
202+
$model->error = 'PASSWORD_CONTAINS_USERNAME';
203+
return self::RET_FAILURE;
204+
}
205+
206+
// --
207+
$model->user->invalidateVerificationToken();
208+
// --
209+
210+
if ( $model->user->getAcl( User::OPTION_DISABLED )) {
211+
$model->error = 'USER_DISABLED';
212+
return self::RET_FAILURE;
213+
}
214+
215+
if (!$model->user->changePassword( $model->pw1 )) {
216+
$model->error = 'INTERNAL_ERROR';
217+
return self::RET_FAILURE;
218+
}
219+
220+
$model->error = false;
221+
return self::RET_SUCCESS;
222+
}
27223
}

src/models/User/ResetPassword.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ class ResetPassword extends Model {
88

99
public $csrf_id;
1010
public $csrf_token;
11-
public $email;
1211
public $error;
12+
public $token;
13+
public $user;
14+
public $username;
1315

1416
}

src/templates/Email/User/Register.plain.phtml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php
22
namespace BNETDocs\Templates\Email\User;
33
use \CarlBennett\MVC\Libraries\Common;
4-
require("./Email/header.plain.inc.phtml");
4+
require('./Email/header.plain.inc.phtml');
55
?>
66
Welcome to BNETDocs!
77

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

1414
Note: This link will only be available for 24 hours after registering. If you
1515
wait until it expires, you will need to complete a password reset.
16-
<?php require("./Email/footer.plain.inc.phtml"); ?>
16+
<?php require('./Email/footer.plain.inc.phtml'); ?>
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
<?php
22
namespace BNETDocs\Templates\Email\User;
33
use \CarlBennett\MVC\Libraries\Common;
4-
require("./Email/header.rich.inc.phtml");
4+
require('./Email/header.rich.inc.phtml');
55
$url = Common::relativeUrlToAbsolute('/user/activate?u=' . rawurlencode($this->getContext()->user_id) . '&t=' . rawurlencode($this->getContext()->token));
66
?>
77
<p style="font-family:sans-serif;font-weight:bold;">Welcome to BNETDocs!</p>
88
<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>
99
<p style="font-family:sans-serif;"><a href="<?=$url?>" rel="external"><?=filter_var($url, FILTER_SANITIZE_FULL_SPECIAL_CHARS)?></a></p>
1010
<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>
11-
<?php require("./Email/footer.rich.inc.phtml"); ?>
11+
<?php require('./Email/footer.rich.inc.phtml'); ?>

src/templates/Email/User/ResetPassword.plain.phtml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
<?php require('./Email/header.plain.inc.phtml'); ?>
1+
<?php
2+
namespace BNETDocs\Templates\Email\User;
3+
use \CarlBennett\MVC\Libraries\Common;
4+
require('./Email/header.plain.inc.phtml');
5+
?>
26
Hello <?=$this->getContext()->user->getName()?>,
37

4-
You requested your password to be reset on BNETDocs. If this was you, copy and
5-
paste the following into your web browser:
8+
Someone requested your password to be reset on BNETDocs. If this was you, copy
9+
and paste the following into your web browser:
610

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

913
If this was not you, then you can safely ignore this email; no action will be
1014
taken.

src/templates/Email/User/ResetPassword.rich.phtml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
<?php
2-
32
namespace BNETDocs\Templates\Email\User;
43

5-
$name = $this->getContext()->user->getName();
6-
$url = $this->getContext()->url;
4+
use \CarlBennett\MVC\Libraries\Common;
5+
6+
$name = filter_var($this->getContext()->user->getName(), FILTER_SANITIZE_FULL_SPECIAL_CHARS);
7+
$url = Common::relativeUrlToAbsolute('/user/resetpassword?username=' . rawurlencode($this->getContext()->user->getUsername()) . '&t=' . rawurlencode($this->getContext()->token));
78

89
?><!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>
910
<!--[if gte mso 9]><xml>
@@ -173,7 +174,7 @@ $url = $this->getContext()->url;
173174
<tbody><tr style="vertical-align: top">
174175
<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">
175176
<div style="color:#DDD;line-height:120%;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;">
176-
<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>
177+
<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>
177178
</div>
178179
</td>
179180
</tr>

0 commit comments

Comments
 (0)