__('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', ''); } /** * 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; $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); } } $out = "
" . $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.") . "
" . "*" . $this->_('For security reasons, we do not reveal whether an account exists on this screen.') . "
"; /* $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; } } } /** * 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 ""; } /** * 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); return $form; } }