1
23

Send a new password by email

Create site16 by copying site15.

  1. /cms
    1. ...
    2. site15
    3. site16

In this chapter, we are going to program sending a new password by email to a registered user in an attached cryptogram.

To test the result online, enter http://www.frasq.org/cms/site16 in the address bar of your navigator. Go to the identification page. Click on the link If you have forgotten your password under the input field for the password. Enter foobar as a name, check the confirmation box, type the verification code and press Send. A new password in a cryptogram has been sent by email to the address nobody@frasq.org:

Your new password is in the attached cryptogram.

If you didn't ask to change it, just ignore this message.
NOTE: Your old password is still valid.

Identify yourself with the new password to activate it.

See you soon.

frasq.org - http://www.frasq.org
--------------------------------------------------------------

Start by creating the action password by adding the file password.php in the folder actions with the following content:

  1. /cms/site16
    1. actions
      1. password.php
  1. function password($lang) {
  2.     head('title', translate('password:title', $lang));
  3.     head('description', false);
  4.     head('keywords', false);
  5.     head('robots', 'noindex, nofollow');
  6.  
  7.     $banner = build('banner', $lang);
  8.  
  9.     $remindme = build('remindme', $lang);
  10.  
  11.     $content = view('password', $lang, compact('remindme'));
  12.  
  13.     $output = layout('standard', compact('banner', 'content'));
  14.  
  15.     return $output;
  16. }

The password action returns a page with a simple banner and a form built by the remindme block.

To give access to the password action, add an alias for each language in the file includes/aliases.inc:

  1.         'mot-de-passe'          => 'password',
  1.         'password'              => 'password',

Add the title for the page for sending a password in the file includes/strings.inc:

In English in the 'en' array:

  1.         'password:title'        => 'Mot de passe',

In French in the 'fr' array:

  1.         'password:title'        => 'Password',

Add the view for the page for sending a password in the folders views/en for the English version and views/fr for the version in French:

  1. /cms/site16
    1. views
      1. en
        1. password.phtml
      2. fr
        1. password.phtml
  1. <h3>New access key</h3>
  2. <?php echo $remindme; ?>
  1. <h3>Nouvelle clé d'accès</h3>
  2. <?php echo $remindme; ?>

The form for sending a password is in a block. Start by writing the view, first in one language:

  1. /cms/site16
    1. views
      1. en
        1. password.phtml
        2. remindme.phtml
  1. <div class="form">
  2. <form action="" method="post">
  3. <input type="hidden" name="remindme_token" value="<?php echo $token; ?>" />
  4. <div class="fields">
  5. <p class="label">What is your connection name?</p>
  6. <p class="input"><input type="text" name="remindme_login" id="remindme_login" size="40" maxlength="100" title="Identifier" onkeypress="return focusonenter(event, 'remindme_code')" value="<?php echo htmlspecialchars($login, ENT_COMPAT, 'UTF-8'); ?>" /></p>
  7. <p class="info">You may also enter your email address.</p>
  8. <p class="input"><input name="remindme_confirmed" id="remindme_confirmed" type="checkbox" title="Confirmation" <?php if ($confirmed) echo 'checked="checked"'; ?> />&nbsp;I want to receive a new password</p>
  9. <?php if ($with_captcha): ?>
  10. <p class="input">
  11. <img src="<?php echo $base_path; ?>/captcha/remindme" alt="" title="Verification code" />
  12. &nbsp;:&nbsp;
  13. <input type="text" name="remindme_code" id="remindme_code" size="4" maxlength="4" title="4 letters" onkeypress="return submitonenter(event, 'remindme_submit')" value="" />
  14. </p>
  15. <?php endif; ?>
  16. <p class="submit"><button type="submit" name="remindme_submit" id="remindme_submit">Send</button></p>
  17. </div>
  18. </form>

The form has 3 input fields: identifier, confirmation and verification code. Apart from $token, 2 variables are necessary: $login and $confirmed. The parameter $with_captcha specifies if a captcha is displayed. All the field names are prefixed with remindme_. The form has only one button called remindme_submit.

The rest of the view deals with the error and the information messages. The possible errors are $missing_code, $bad_code, $missing_login, $bad_login, $missing_confirmation, $internal_error and $contact_page. If $email_sent is true, the user is notified that the message has been sent and a link to the identification page $user_page is shown.

  1. <?php
  2. if ($errors):
  3. extract($errors);
  4. ?>
  5. <div class="errors">
  6. <?php if ($missing_code): ?>
  7. <p>Enter the verification code displayed in the image.</p>
  8. <?php elseif ($bad_code): ?>
  9. <p>The verification code is incorrect.</p>
  10. <?php endif; ?>
  11. <?php if ($missing_login): ?>
  12. <p>You haven't typed your identifier.</p>
  13. <?php elseif ($bad_login): ?>
  14. <p>The identifier is not valid.</p>
  15. <?php endif; ?>
  16. <?php if ($missing_confirmation): ?>
  17. <p>Check the confirmation box.</p>
  18. <?php endif; ?>
  19. <?php if ($internal_error): ?>
  20. <p>An internal error has occurred. If you can describe the problem, please <a href="<?php echo $contact_page; ?>">contact us</a>.</p>
  21. <?php endif; ?>
  22. </div>
  23. <?php endif; ?>
  24. <?php
  25. if ($infos):
  26. extract($infos);
  27. ?>
  28. <div class="infos">
  29. <?php if ($email_sent): ?>
  30. <p>A new password has been sent to you by email.<br />
  31. To identify yourself, <a href="<?php echo $user_page; ?>">click here</a>.</p>
  32. <?php endif; ?>
  33. </div>
  34. <?php endif; ?>
  35. </div>

To validate the view, write a first version of the function remindme which is limited to displaying the form:

  1. /cms/site16
    1. blocks
      1. remindme.php
  1. require_once 'tokenid.php';
  2.  
  3. function remindme($lang, $login=false) {
  4.     $login=$confirmed=$code=$token=false;
  5.  
  6.     $missing_code=false;
  7.     $bad_code=false;
  8.  
  9.     $bad_token=false;
  10.  
  11.     $missing_login=false;
  12.     $bad_login=false;
  13.     $missing_confirmation=false;
  14.  
  15.     $email_sent=false;
  16.     $user_page=url('user', $lang);
  17.  
  18.     $internal_error=false;
  19.     $contact_page=false;
  20.  
  21.     $with_captcha=true;
  22.  
  23.     $_SESSION['remindme_token'] = $token = token_id();
  24.  
  25.     $errors = compact('missing_login', 'bad_login', 'missing_confirmation', 'missing_code', 'bad_code', 'internal_error', 'contact_page');
  26.     $infos = compact('email_sent', 'user_page');
  27.  
  28.     $output = view('remindme', $lang, compact('token', 'with_captcha', 'login', 'confirmed', 'errors', 'infos'));
  29.  
  30.     return $output;
  31. }

Enter http://localhost/cms/site16/en/user in the address bar of your navigator. Edit the remindme function. Assign values to all the input field variables. Set all the error variables to true. Check the links to the contact page and to the identification page.

Add the view in French:

  1. /cms/site16
    1. views
      1. fr
        1. password.phtml
        2. remindme.phtml
  1. <div class="form">
  2. <form action="" method="post">
  3. <input type="hidden" name="remindme_token" value="<?php echo $token; ?>" />
  4. <div class="fields">
  5. <p class="label">Quel est votre nom de connexion ?</p>
  6. <p class="input"><input type="text" name="remindme_login" id="remindme_login" size="40" maxlength="100" title="Identifiant" onkeypress="return focusonenter(event, 'remindme_code')" value="<?php echo htmlspecialchars($login, ENT_COMPAT, 'UTF-8'); ?>" /></p>
  7. <p class="info">Vous pouvez aussi entrer votre adresse d'email.</p>
  8. <p class="input"><input name="remindme_confirmed" id="remindme_confirmed" type="checkbox" title="Confirmation" <?php if ($confirmed) echo 'checked="checked"'; ?> />&nbsp;Je veux recevoir un nouveau mot de passe</p>
  9. <?php if ($with_captcha): ?>
  10. <p class="input">
  11. <img src="<?php echo $base_path; ?>/captcha/remindme" alt="" title="Code de vérification" />
  12. &nbsp;:&nbsp;
  13. <input type="text" name="remindme_code" id="remindme_code" size="4" maxlength="4" title="4 lettres" onkeypress="return submitonenter(event, 'remindme_submit')" value="" />
  14. </p>
  15. <?php endif; ?>
  16. <p class="submit"><button type="submit" name="remindme_submit" id="remindme_submit">Envoyer</button></p>
  17. </div>
  18. </form>
  19. <?php
  20. if ($errors):
  21. extract($errors);
  22. ?>
  23. <div class="errors">
  24. <?php if ($missing_code): ?>
  25. <p>Entrez le code de vérification affiché dans l'image.</p>
  26. <?php elseif ($bad_code): ?>
  27. <p>Le code de vérification est incorrect.</p>
  28. <?php endif; ?>
  29. <?php if ($missing_login): ?>
  30. <p>Vous n'avez pas saisi votre identifiant.</p>
  31. <?php elseif ($bad_login): ?>
  32. <p>L'identifiant n'est pas valide.</p>
  33. <?php endif; ?>
  34. <?php if ($missing_confirmation): ?>
  35. <p>Cochez la case de confirmation.</p>
  36. <?php endif; ?>
  37. <?php if ($internal_error): ?>
  38. <p>Une erreur interne s'est produite. Si vous pouvez décrire le problème, merci de <a href="<?php echo $contact_page; ?>">nous contacter</a>.
  39. <?php endif; ?>
  40. </div>
  41. <?php endif; ?>
  42. <?php
  43. if ($infos):
  44. extract($infos);
  45. ?>
  46. <div class="infos">
  47. <?php if ($email_sent): ?>
  48. <p>Un nouveau mot de passe vous a été envoyé par email.<br />
  49. Pour vous identifier, <a href="<?php echo $user_page; ?>">cliquez ici</a>.</p>
  50. <?php endif; ?>
  51. </div>
  52. <?php endif; ?>
  53. </div>

Enter http://localhost/cms/site16/fr/mot-de-passe in the address bar of your navigator to validate the French version.

Once the views are tuned, complete remindme with the following code:

  1. require_once 'readarg.php';
  2. require_once 'strflat.php';
  3. require_once 'validateusername.php';
  4. require_once 'validatemail.php';
  5. require_once 'isusernameallowed.php';
  6. require_once 'ismailallowed.php';
  7. require_once 'tokenid.php';

Loads the functions readarg, strflat, validate_user_name, validate_mail, is_user_name_allowed, is_mail_allowed and token_id. Add the files isusernameallowed.php and ismailallowed.php in the folder library with the following contents:

  1. /cms/site16
    1. library
      1. isusernameallowed.php
      2. ismailallowed.php
  1. function is_user_name_allowed($name) {
  2.     static $blacklist = array('frasq');
  3.  
  4.     return !in_array($name, $blacklist);
  5. }

is_user_name_allowed returns false if $name is a name which is forbidden by the list $blacknamelist, true if $name is allowed.

  1. function is_mail_allowed($mail) {
  2.     static $blacklist = array('frasq@frasq.org', 'webmaster@frasq.org', 'keymaster@frasq.org');
  3.  
  4.     return !in_array($mail, $blacklist);
  5. }

is_mail_allowed returns false if $mail is an email address which is forbidden by the list $blackmaillist, true if a mail to $mail is allowed.

  1. function remindme($lang, $login=false) {
  2.     $action='init';
  3.     if (isset($_POST['remindme_submit'])) {
  4.         $action='remindme';
  5.     }
  6.  
  7.     $login=$confirmed=$code=$token=false;
  8.  
  9.     switch($action) {
  10.         case 'remindme':
  11.             if (isset($_POST['remindme_login'])) {
  12.                 $login=strtolower(strflat(readarg($_POST['remindme_login'], true)));
  13.             }
  14.             if (isset($_POST['remindme_confirmed'])) {
  15.                 $confirmed=$_POST['remindme_confirmed'] ? true : false;
  16.             }
  17.             if (isset($_POST['remindme_code'])) {
  18.                 $code=readarg($_POST['remindme_code'], true);
  19.             }
  20.             if (isset($_POST['remindme_token'])) {
  21.                 $token=readarg($_POST['remindme_token']);
  22.             }
  23.             break;
  24.         default:
  25.             break;
  26.     }

Reads the form.

  1.     switch($action) {
  2.         case 'remindme':
  3.             if (!isset($_SESSION['remindme_token']) or $token != $_SESSION['remindme_token']) {
  4.                 $bad_token=true;
  5.                 break;
  6.             }
  7.  
  8.             if ($with_captcha) {
  9.                 if (!$code) {
  10.                     $missing_code=true;
  11.                     break;
  12.                 }
  13.                 $captcha=isset($_SESSION['captcha']) ? $_SESSION['captcha'] : false;
  14.                 if (!$captcha or $captcha != strtoupper($code)) {
  15.                     $bad_code=true;
  16.                     break;
  17.                 }
  18.             }
  19.  
  20.  
  21.             if (!$login) {
  22.                 $missing_login=true;
  23.             }
  24.             else if ((!validate_user_name($login) or !is_user_name_allowed($login)) and (!validate_mail($login) or !is_mail_allowed($login))) {
  25.                 $bad_login=true;
  26.             }
  27.             if (!$confirmed) {
  28.                 $missing_confirmation=true;
  29.             }
  30.             break;
  31.         default:
  32.             break;
  33.     }

Controls all the input data, particularly if the connection name is allowed.

  1.     switch($action) {
  2.         case 'remindme':
  3.             if ($bad_token or $missing_code or $bad_code or $missing_login or $bad_login or $missing_confirmation) {
  4.                 break;
  5.             }
  6.  
  7.             require_once 'models/user.inc';
  8.  
  9.             $user_id = user_find($login);
  10.  
  11.             if (!$user_id) {
  12.                 $bad_login=true;
  13.  
  14.                 require_once 'log.php';
  15.                 write_log('password.err', substr($login, 0, 40));
  16.  
  17.                 break;
  18.             }
  19.  
  20.             $user = user_get($user_id);
  21.  
  22.             if (!$user) {
  23.                 $internal_error=true;
  24.                 break;
  25.             }

Checks if an error has been detected, loads the model user.inc and gets the user's number with user_find. If user_find returns false, the error is logged in password.err. Otherwise, the code gets the email address for the account $user_id with user_get.

  1.             require_once 'newpassword.php';
  2.  
  3.             $newpassword=newpassword();
  4.  
  5.             if (!user_set_newpassword($user_id, $newpassword)) {
  6.                 $internal_error=true;
  7.                 break;
  8.             }

Loads the function newpassword then assigns a new password to the user's account $user_id.

  1.             require_once 'emailcrypto.php';
  2.  
  3.             global $sitename, $webmaster;
  4.  
  5.             $to=$user['user_mail'];
  6.  
  7.             $subject = translate('email:new_password_subject', $lang);
  8.             $msg = translate('email:new_password_text', $lang) . "\n\n" . translate('email:salutations', $lang);
  9.             if (!emailcrypto($msg, $newpassword, $to, $subject, $webmaster)) {
  10.                 $internal_error=true;
  11.             }
  12.             else {
  13.                 $email_sent=$to;
  14.             }
  15.  
  16.             $confirmed=false;
  17.  
  18.             break;
  19.         default:
  20.             break;
  21.     }

Loads the emailcrypto function. Prepares the email message according to the user's language and calls emailcrypto with the new password, the subject and the text of the message, the recepient and the sender's email addresses.

  1.     if ($internal_error) {
  2.         $contact_page=url('contact', $lang);
  3.     }
  4.     else if ($email_sent) {
  5.         $user_page=url('user', $lang);
  6.     }

Sets $contact_page to the URL of the contact page if $internal_error is true or $user_page to the URL of the identification if $email_sent is true.

  1.     $_SESSION['remindme_token'] = $token = token_id();
  2.  
  3.     $errors = compact('missing_login', 'bad_login', 'missing_confirmation', 'missing_code', 'bad_code', 'internal_error', 'contact_page');
  4.     $infos = compact('email_sent', 'user_page');
  5.  
  6.     $output = view('remindme', $lang, compact('token', 'with_captcha', 'login', 'confirmed', 'errors', 'infos'));
  7.  
  8.     return $output;
  9. }

The rest of the code prepares all the parameters of the view, including the token which is memorized in the session variable $_SESSION['remindme_token'], generates it and returns content.

Define the subject and the text of the email by adding the parameters email:salutations, email:new_password_subject and email:new_password_text in the file includes/strings.inc, in English and in French:

  1.         'email:salutations'             => 'See you soon.',
  2.         'email:new_password_subject'    => 'frasq.org : Your new password.',
  3.         'email:new_password_text'       => "Your new password is in the attached cryptogram.\n\nIf you didn't ask to change it, just ignore this message.\nNOTE: Your old password is still valid.\n\nIdentify yourself with the new password to activate it.",
  1.         'email:salutations'             => 'À bientôt.',
  2.         'email:new_password_subject'    => 'frasq.org : Votre mot de passe.',
  3.         'email:new_password_text'       => "Votre nouveau mot de passe est dans le cryptogramme en pièce jointe.\n\nSi vous n'avez pas demandé à en changer, veuillez ignorer ce message.\nNOTE : Votre ancien mot de passe est toujours valide.\n\nConnectez-vous avec le nouveau mot de passe pour l'activer.",

Add the files newpassword.php and emailcrypto.php in the folder library with the following contents:

  1. /cms/site16
    1. library
      1. newpassword.php
      2. emailcrypto.php
  1. require_once 'strrand.php';
  2.  
  3. function newpassword($len=6) {
  4.     $charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
  5.  
  6.     return strrand($charset, $len);
  7. }

newpassword returns 6 random characters in capital letters or digits.

The signature, the mailer and the return address of the email are global variables defined in the file includes/config.inc:

  1. global $sitename, $webmaster, $mailer;
  2.  
  3. $sitename = 'frasq.org';
  4. $webmaster = 'nobody@frasq.org';
  5. $mailer = 'Frasq';
  6.  
  7. global $signature;
  8.  
  9. $signature='frasq.org - http://www.frasq.org';
  1. require_once 'strtag.php';
  2.  
  3. function emailcrypto($text, $tag, $to, $subject, $sender) {
  4.     global $signature, $mailer, $webmaster;

emailcrypto sends the message $text with an attached cryptogram of $tag to $to with the subject $subject from $sender.

  1.     $img=strtag($tag);
  2.  
  3.     ob_start();
  4.     imagepng($img);
  5.     imagedestroy($img);
  6.     $imgdata=ob_get_contents();
  7.     ob_end_clean();
  8.  
  9.     $sep=md5(uniqid('sep'));
  10.     $data=chunk_split(base64_encode($imgdata));

Builds a cryptogram from the string of characters $tag with strtag then converts the obtained image in PNG. The PNG is then encoded in base64.

  1.     $headers = <<<_SEP_
  2. From: $sender
  3. Return-Path: $webmaster
  4. Content-Type: multipart/mixed; boundary="$sep"
  5. X-Mailer: $mailer
  6. _SEP_;

Generates the MIME header of the email with the fields From:, Return-Path:, Content-Type: and X-Mailer: with the parameter $sender and the glocal variables $webmaster and $mailer. The field Content-Type: indicates that the message is in 2 parts, the text of email and the attached cryptogram, separated by $sep.

  1.     $body = '';
  2.  
  3.     if ($text) {
  4.         $body .= <<<_SEP_
  5. --$sep
  6. Content-Type: text/plain; charset=utf-8
  7.  
  8. $text
  9.  
  10. $signature
  11.  
  12. _SEP_;
  13.     }
  14.  
  15.     $body .= <<<_SEP_
  16. --$sep
  17. Content-Type: image/png; name="crypto.png"
  18. Content-Transfer-Encoding: base64
  19. Content-Disposition: inline; filename="crypto.png"
  20.  
  21. $data
  22. --$sep--
  23. _SEP_;
  24.  
  25.     return @mail($to, $subject, $body, $headers);
  26. }

Builds the body of the email, first the text followed by the attached image, then sends the email with the PHP function mail.

Add a column to the user table to memorize to new password:

mysql> ALTER TABLE user ADD newpassword VARCHAR( 32 ) NULL AFTER password;

Edit the file user.inc in the folder models/user.inc and the function user_set_newpassword:

  1. function user_set_newpassword($user_id, $password) {
  2.     if (!is_numeric($user_id)) {
  3.         return false;
  4.     }
  5.  
  6.     $sqlnewpassword=$password ? '\'' . md5($password) . '\'' : NULL;
  7.  
  8.     $tabuser=db_prefix_table('user');
  9.  
  10.     $sql="UPDATE $tabuser SET newpassword=$sqlnewpassword WHERE user_id=$user_id LIMIT 1";
  11.  
  12.     $r = db_update($sql);
  13.  
  14.     return $r;
  15. }

Modify the function user_get to return the field user_password:

  1.     $sql="SELECT name AS user_name, password AS user_password, newpassword AS user_newpassword, mail AS user_mail, UNIX_TIMESTAMP(created) AS user_created, UNIX_TIMESTAMP(access) AS user_access, locale AS user_locale, active AS user_active, banned AS user_banned FROM $tabuser WHERE user_id=$user_id LIMIT 1";

Modify the function user_login to take into account the possibilty of a new password:

  1.     if ($user_newpassword) {
  2.         if ($password == $user_newpassword) {
  3.             // use the new one
  4.             $sql="UPDATE $tabuser SET access=FROM_UNIXTIME($now), password='$user_newpassword', newpassword=NULL WHERE user_id=$user_id LIMIT 1";
  5.         }
  6.         else {
  7.             // keep the old one
  8.             $sql="UPDATE $tabuser SET access=FROM_UNIXTIME($now), newpassword=NULL WHERE user_id=$user_id LIMIT 1";
  9.         }
  10.     }
  11.     else {
  12.         // just keep track
  13.         $sql="UPDATE $tabuser SET access=FROM_UNIXTIME($now) WHERE user_id=$user_id LIMIT 1";
  14.     }
  15.  
  16.     db_update($sql);

If a new password has been defined and if the given password is the new password, user_login replaces the old password by the new one, otherwise, the old password is kept. The new password is always erased. In all cases, user_login writes down the date and the time of the connection.

Add a link to the page for sending a new password in the view of the identification page:

  1. <p class="info link">If you have forgotten your password, <a href="<?php echo $password_page; ?>">click here</a>.</p>
  1. <p class="info link">Si vous avez oublié votre mot de passe, <a href="<?php echo $password_page; ?>">cliquez ici</a>.</p>

Assign the URL of the page for sending a password to the variable $password_page of the views in the login function:

  1.     $password_page=url('password', $lang);
  2.  
  3.     $output = view('login', $lang, compact('token', 'with_captcha', 'password_page', 'login', 'errors'));

Make sure the email address of the user foobar is foobar@localnet.net. Enter http://localhost/cms/site16 in the address bar of your navigator. Go to the identification page. Click on the link If you have forgotten your password. Enter foobar as a name, check the confirmation box, type the verification code and press Send. Read foobar's email. Try to identify yourself with the new password. Disconnect. Ask again for a new password. Identify yourself with the old password which is still valid.

Comments

To add a comment, click here.