-
Notifications
You must be signed in to change notification settings - Fork 194
Expand file tree
/
Copy pathProcessForgotPassword.module
More file actions
436 lines (344 loc) · 13.4 KB
/
ProcessForgotPassword.module
File metadata and controls
436 lines (344 loc) · 13.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
<?php
/**
* ProcessWire Forgot Password
*
* Provides password reset capability for when users forget their password.
* This accompanies the ProcessLogin module.
*
* For more details about how Process modules work, please see:
* /wire/core/Process.php
*
* ProcessWire 2.x
* Copyright (C) 2012 by Ryan Cramer
* Licensed under GNU/GPL v2, see LICENSE.TXT
*
* http://www.processwire.com
* http://www.ryancramer.com
*
*/
class ProcessForgotPassword extends Process implements ConfigurableModule {
public static function getModuleInfo() {
return array(
'title' => __('Forgot Password', __FILE__), // getModuleInfo title
'summary' => __('Provides password reset/email capability for the Login process.', __FILE__), // getModuleInfo summary
'version' => 101,
'permanent' => false,
'permission' => 'page-view',
);
}
/**
* Setup default values
*
*/
public function __construct() {
// allow passwords to be reset?
$this->set('allowReset', 1);
$this->set('table', 'process_forgot_password');
$this->set('emailFrom', '');
$this->set('enableUseEmailForReset', 0);
}
/**
* Check if login posted and attempt login, otherwise render the login form
*
*/
public function ___execute() {
if($this->user->isLoggedin()) return;
$this->setFuel('processHeadline', $this->_('Reset Password')); // Reset password page headline
if(!$this->allowReset) {
$this->error($this->_("Password reset is not allowed"));
return;
}
$this->setupResetTable();
if( $this->input->post->username &&
$this->input->post->submit_forgot &&
$this->session->userResetStep === 1) {
// step 2
return $this->step2_processForm();
} else if(
$this->input->get->token &&
$this->input->get->user_id) {
// steps 3 and 4
if($this->session->userResetStep >= 2 && $this->session->userResetID === (int) $this->input->get->user_id) {
return $this->step3_processEmailClick();
} else {
$this->error($this->_("Unable to complete password reset. Please make sure you are on the same computer and in the same web browser that you originally submitted your request from."));
$this->session->redirect("./?forgot=1");
}
} else {
// step 1
return $this->step1_renderForm();
}
}
/**
* Render forgot password form
*
*/
protected function step1_renderForm() {
$form = $this->modules->get("InputfieldForm");
$form->attr('action', './?forgot=1');
$form->attr('method', 'post');
$field = $this->modules->get("InputfieldText");
$field->attr('id+name', 'username');
$field->required = true;
if ( $this->enableUseEmailForReset ) {
$field->label = $this->_("Enter your user name or email address");
}
else {
$field->label = $this->_("Enter your user name");
}
$field->description = $this->_("If you have an account in our system with a valid email address on file, an email will be sent to you after you submit this form. That email will contain a link that you may click on to reset your password.");
$form->add($field);
/*
$field = $this->modules->get("InputfieldEmail");
$field->attr('id+name', 'useremail');
$field->label = $this->_('Forgot your username?');
$field->collapsed = Inputfield::collapsedYes;
$field->description = $this->_('Enter your email address and we will send you your account name.');
$form->add($field);
*/
$submit = $this->modules->get("InputfieldSubmit");
$submit->attr('id+name', 'submit_forgot');
$form->add($submit);
$this->session->userResetStep = 1;
return $form->render();
}
/**
* Process the form submitted from step1 with username or email
*
* If it matches up to an account in the system, then send them an email.
*
*/
protected function step2_processForm() {
$user = null;
$name = $this->sanitizer->pageName($this->input->post->username);
if(strlen($name)) {
$user = $this->users->get("name=$name");
if($user && $user->id && $user->email && !$user->isUnpublished()) {
// user was found, send them an email with reset link
$this->step2_sendEmail($user);
}
elseif ( $this->enableUseEmailForReset ) {
// try also with the email address
$email = $this->sanitizer->email( $this->input->post->username );
if ( strlen( $email ) && count( $this->users->find( "email=$email" ) ) == 1 ) {
$user = $this->users->get( "email=$email" );
if ( $user && $user->id && $user->email == $email && !$user->isUnpublished() ) {
$this->step2_sendEmail( $user );
}
}
}
}
$out =
"<h2>" . $this->_("Assuming your account information was found and we have an email address on file, an email was dispatched with password reset information.") . "*</h2>" .
"<p>" . $this->_("Please check your email for this message. If you do not receive an email within the next 15 minutes please contact the site administrator to reset your password. This password reset request will expire in 60 minutes. Do NOT close this window until you have completed your password reset request.") . "</p>" .
"<p class='detail'>*" . $this->_('For security reasons, we do not reveal whether an account exists on this screen.') . "</p>";
/*
$email = $this->sanitizer->email($this->input->post->useremail);
if(strlen($email)) {
$users = $this->users->find("include=all, email=" . strtolower($this->sanitizer->selectorValue($email)));
$subject = $this->_('Account Information');
$body = $this->_('You are receiving this email because you requested your account name.') . ' ';
if(count($users) > 1) {
$body .= $this->_('Your email address appears to be associated with multiple accounts and we cannot reveal those for security reasons. Please contact the administrator for assistance.');
} else {
$user = $users->first();
if($user && strtolower($user->email) === $email) {
$body .= $this->_('Your account name is:') . ' ' . $user->name;
}
}
}
*/
return $out;
}
/**
* Send an email with password reset link to the given User account
*
*/
protected function step2_sendEmail(User $user) {
$subject = $this->_("Password Reset Information"); // Email subject
// create the unique verification token that is stored on the server and sent in the email
$token = md5(mt_rand() . $user->name . $user->id . microtime() . mt_rand());
// set some session vars we'll use for comparison
$this->session->userResetStep = 2;
$this->session->userResetID = $user->id;
$this->session->userResetName = $user->name;
$url = $this->page->httpUrl() . "?forgot=1&user_id={$user->id}&token=" . urlencode($token);
$body = $this->_("To complete your password reset, click the URL below (or paste into your browser) and follow the instructions:") . "\n\n"; // Email body part 1
$body .= $url . "\n\n";
$body .= $this->_("This URL will expire 60 minutes from time it was sent. This URL must be opened from the same computer and browser that the request was initiated from."); // Email body part 2
$emailFrom = $this->emailFrom;
if(!$emailFrom) $emailFrom = $this->wire('config')->adminEmail;
if(!$emailFrom) $emailFrom = 'processwire@' . $this->config->httpHost;
if(wireMail($user->email, $emailFrom, $subject, $body)) {
// for informational/debugging purposes
$ip = preg_replace('/[^\d.]/', '', $_SERVER['REMOTE_ADDR']);
// clear space for this reset request, since there can only be one active for any given user
$database = $this->wire('database');
$table = $database->escapeTable($this->table);
try {
$query = $database->prepare("DELETE FROM `{$table}` WHERE id=:id");
$query->bindValue(":id", (int) $user->id, PDO::PARAM_INT);
$query->execute();
$query = $database->prepare("INSERT INTO `{$table}` SET id=:id, name=:name, token=:token, ts=:ts, ip=:ip");
$query->bindValue(":id", $user->id, PDO::PARAM_INT);
$query->bindValue(":name", $user->name);
$query->bindValue(":token", $token);
$query->bindValue(":ts", time(), PDO::PARAM_INT);
$query->bindValue(":ip", $ip);
$query->execute();
} catch(Exception $e) {
// catch any errors, just to prevent anything from ever being reported to screen
$this->session->clearErrors();
$this->error("Unable to complete this step");
return;
}
}
else {
$this->logError('Error in '.__CLASS__.'::'.__FUNCTION__.' : Problems sending password reset email to '.$user->email);
}
}
/**
* User clicked URL from their email
*
* If valid, display form with new password entries.
*
* If form submitted, send to step 4.
*
*/
protected function step3_processEmailClick() {
$id = (int) $this->input->get->user_id;
$token = $this->input->get->token;
$database = $this->wire('database');
$table = $database->escapeTable($this->table);
$query = $database->prepare("SELECT name, token, ip FROM `$table` WHERE id=:id");
$query->bindValue(":id", $id);
$query->execute();
$row = $query->fetch(PDO::FETCH_ASSOC);
if($row && $id == $this->session->userResetID) {
if( $row['token'] && ($row['token'] === $token) &&
$row['name'] === $this->session->userResetName) {
// all conditions good - user may reset their password
$form = $this->step3_buildForm($id, $token);
if($this->input->post->submit_reset && $this->session->userResetStep === 3) {
$out = $this->step4_completeReset($id, $form);
} else {
$this->session->userResetStep = 3;
$out = $form->render();
}
return $out;
}
}
$this->error($this->_("Invalid reset request. Your request may have expired."));
return "<p><a href='./?forgot=1'>" . $this->_("Continue") . "</a></p>";
}
/**
* Build the form with the reset password field
*
*/
protected function step3_buildForm($id, $token) {
$form = $this->modules->get("InputfieldForm");
$form->attr('method', 'post');
$form->attr('action', "./?forgot=1&user_id=$id&token=$token");
$field = $this->modules->get("InputfieldPassword");
$field->attr('id+name', 'pass');
$field->required = true;
$field->label = $this->_("Reset Password"); // New password field label
$form->add($field);
$submit = $this->modules->get("InputfieldSubmit");
$submit->attr('id+name', 'submit_reset');
$form->add($submit);
return $form;
}
/**
* Process the submitted password reset form and reset password
*
*/
protected function step4_completeReset($id, $form) {
$form->processInput($this->input->post);
$user = $this->users->get((int) $id);
$pass = $form->get('pass')->value;
if(count($form->getErrors()) || !$user->id || !$pass) return $form->render();
$outputFormatting = $user->outputFormatting;
$user->setOutputFormatting(false);
$user->pass = $pass;
$user->save();
$user->setOutputFormatting($outputFormatting);
$this->session->message($this->_("Your password has been successfully reset. You may now login."));
$this->session->remove('userResetStep');
$this->session->remove('userResetID');
$this->session->remove('userResetName');
$database = $this->wire('database');
$table = $database->escapeTable($this->table);
$query = $database->prepare("DELETE FROM `$table` WHERE id=:id");
$query->bindValue(":id", $user->id, PDO::PARAM_INT);
$query->execute();
$this->session->redirect("./");
}
/**
* Create the process_forgot_password table if it doesn't exist
*
* Delete any entries older than 60 minutes
*
*/
protected function setupResetTable() {
// create the DB table if it's not there already
$database = $this->wire('database');
$table = $database->escapeTable($this->table);
try {
$query = $database->prepare("SHOW COLUMNS FROM `$table`");
$query->execute();
} catch(Exception $e) {
$query = false;
}
if(!$query || !$query->rowCount()) $this->install();
// delete table entries that are older than one hour
$query = $database->prepare("DELETE FROM `$table` WHERE ts<:ts");
$query->bindValue(":ts", time()-3600, PDO::PARAM_INT);
$query->execute();
}
/**
* Install this module by creating it's table
*
*/
public function ___install() {
$database = $this->wire('database');
$table = $database->escapeTable($this->table);
$engine = $this->wire('config')->dbEngine;
$sql = "CREATE TABLE `$table` ( " .
"id INT unsigned NOT NULL PRIMARY KEY, " .
"name varchar(128) NOT NULL, " .
"token char(32) NOT NULL, " .
"ts INT unsigned NOT NULL, " .
"ip varchar(15) NOT NULL, " .
"KEY token (token), " .
"KEY ts (ts), " .
"KEY ip (ip) " .
") ENGINE=$engine DEFAULT CHARSET=ascii;";
try {
$this->message("Creating table: $table", Notice::log);
$database->exec($sql);
} catch(Exception $e) {
$this->error($e->getMessage(), Notice::log);
}
}
public function ___uninstall() {
$database = $this->wire('database');
$table = $database->escapeTable($this->table);
$database->exec("DROP TABLE `$table`");
}
public static function getModuleConfigInputfields(array $data) {
$form = new InputfieldWrapper();
$f = wire('modules')->get('InputfieldEmail');
$f->attr('name', 'emailFrom');
$f->label = __('Email address to send messages from');
if(isset($data['emailFrom'])) $f->attr('value', $data['emailFrom']);
$form->add($f);
$f = wire('modules')->get( 'InputfieldCheckbox' );
$f->label = __( 'Enable users to enter optionally their email address in place of their username' );
$f->attr( 'id+name', 'enableUseEmailForReset' );
$f->attr( 'value', 0 );
$f->attr( 'checked', empty( $data['enableUseEmailForReset'] ) ? '' : 'checked' );
$form->add($f);
return $form;
}
}