Registration Forms little tips

I’ve been putting together a little password/username/email retrieval system for those forgetful folks, and was curious as to how you’re planning to structure yours. Here’s what I’m thinking dreamed up:

  1. User tells the site they forgot their username or password (In the system I have, they log in with a username and a password, and have a contact email besides, so if they forgot their email they can just log in and see).

  2. The system sends them an email with a link containing a hashed GET parameter of non-secure values to a page (lets say their last login time or IP mixed with a salt, along with their un-hashed email so the lookup SQL doesn’t have to hash every email to find a match.).

  3. They click on the link, and this page checks the values against the database, and lets them view it if the values ‘pass’, otherwise, tells them to go fly a kite. By making it a ‘logged in’ type of scenario, we prevent most attacks unless the hacker can log in to the users contact email address. Also, without making them validate by a link, we would have to expose their secret question on a page to anybody who has their username or email (giving someone with research skills quite a bit of power).

  4. If they want to change their password, they give their username and secret question’s answer, and then are allowed to use a form to change their password. If they want the username, they give the password and answer, and the username gets emailed to them.

Or maybe I’ll just wait for you to make another one of these wonderful cut-and-paste tutorials :)

But, they probably do this because it’s more secure; a hacker needs to break in to their email in addition to their secret question’s answer and whatever other credentials you require. Using the email is just one more (difficult) nut to crack, although more inconvenient for the user.

But, for the password retrieval scenario I proposed above, you would need to actually compare the emails (you don’t want to have a button on the site that sends a change password link to anybody who knows a username, and if they forgot their username, you’d want them to supply something to identify the password).

Haha, yeah sorry about that. I’ll have to pull my information to include it about the salt. What’s a good way is to take a value of time and store it in the database then use that to salt, it’s guaranteed to be different every time and in the event you change the password or user name you can update the time variable that you used for the salt. Time is a wonderful thing :D lol.

Funny you should mention this I just completed this with a comprehensive validate function that validates the user and then an added feature that if you forgot your password you can go through the process to adjust it. Instead of sending an email to the user, I get them to input their username and email to pull from the database. This means the “hacker” has to know the user’s name and email. The next stage displays the secret question that you have to provide the answer for. After that a new field that allows you to create a new password and make sure they match (the repeating password that is). After you complete the password change, a new email is sent with the account details. So even if a hacker alters a user’s password - they get the details ;). Then later in a user profile sort of way, you just port over that last function (that changes the password) and bam you have killed 2 birds with one stone, the forgot password and the change password are both the same function ;).

I’ll post the code but it’s very messy and more over it needs to be cleaned up for efficiency but I was going to think about making it widget style. Right now I’m tampering with Jonah’s skeleton template to see what I can customize there into a widget form using my logic to improve the setup (if it needs to be).

Most of these little applications I do on my own in new site web apps to understand how to code around the framework. Then I’ll pull everything together at the end with modules and widgets. That’s why I’m checking out the skeleton application now :).

But just for you, here’s the code.

It functions between two models, a user and a validate model and then the password recovery is done through another model.

Everything is processed through one user controller. And then there are 3 views for each aspect. User and ForgotPassword are tied in with the user table and validate operates with the validate table.

Here’s the database tables I used - just so you can know what is apart of the code and test / modify how you wish:




CREATE TABLE user (

    userID INT UNSIGNED NOT NULL AUTO_INCREMENT,

    username varchar(32) NOT NULL,

    password varchar(64) NOT NULL,

    email varchar(256) NOT NULL,

    ip varchar(15) NOT NULL,

    time INT UNSIGNED NOT NULL,

    question varchar(256) NOT NULL,

    answer varchar(128) NOT NULL,

    PRIMARY KEY (userID),

    UNIQUE KEY (username)

);


CREATE TABLE validate (

    userID INT UNSIGNED NOT NULL AUTO_INCREMENT,

    username varchar(32) NOT NULL,

    password varchar(64) NOT NULL,

    email varchar(256) NOT NULL,

    ip varchar(15) NOT NULL,

    time INT UNSIGNED NOT NULL,

    session varchar(32) NOT NULL,

    question varchar(256) NOT NULL,

    answer varchar(128) NOT NULL,

    PRIMARY KEY (userID),

    UNIQUE KEY (username)

);


Now there's also a mail table that has 


subject, header, email, message


These you can get away with making all these text values, but for sure make the message and header text values.



Here’s the User model:




<?php


class User extends CActiveRecord

{

    /**

     * The followings are the available columns in table 'User':

     * @var integer $userID

     * @var string $username

     * @var string $password

     * @var string $email

     * @var string $ip

     * @var integer $time

     * @var string $question

     * @var string $answer

     */

    

    /*

     * Define a repeat password variable and the Captcha variable

     * For outside class use

     * 

     * Emailed establishes the status of emailing the user

     * Email Password takes a copy of the password before hashing

     * to send to the user. This is a private variable.

     */

    public $password2;

    public $verifyCode;

    public $emailed = "Not Applicable";

    private $emailPassword;

        


    /**

     * Returns the static model of the specified AR class.

     * @return CActiveRecord the static model class

     */

    public static function model($className=__CLASS__)

    {

        return parent::model($className);

    }


    /**

     * @return string the associated database table name

     */

    public function tableName()

    {

            return 'user';

    }


    /**

     * @return array validation rules for model attributes.

     */

    public function rules()

    {

        return array(

        

            // define the length of all attributes

            array('username','length','max'=>32),

            array('password','length','max'=>64, 'min'=>6),

            //array('password2','length','max'=>64, 'min'=>6),

            array('email','length','max'=>256),

            array('question','length','max'=>256),

            array('answer','length','max'=>128),

            array('ip','length','max'=>15),

            

            // define the parameters of the time attribute

            array('time', 'numerical', 'integerOnly'=>true),

            

            // convert username, question and answer to lower case

            array('username, question, answer', 'filter', 'filter'=>'strtolower'),

            

            // make sure email is a valid email

            array('email','email'),

            // make sure username and email are unique

            array('username, email', 'unique'),            

        );

    }

    

    /**

     * @return actions to perform before validating

     */

    public function beforeValidate()

    {

        // store password for sending after registration via email

        $this->emailPassword = $this->password;

        return true;

    }

    

    /**

     * @return actions to perform before saving ie: hash password

     */

    public function beforeSave()

    {

        // hash password for database.

        $pass = md5(md5($this->password).Yii::app()->params["salt"]);

          $this->password = $pass;

          return true;

    }

    

    /**

     * @return actions to perform after saving ie: Send email with account info

     */

    public function afterSave()

    {

        /*

         * This is the Registration Email setup 

         */ 

        $subject = "Registration Details - SterlingSavvy.com";

        $message = '

        <html>

        <head>

        <title>SterlingSavvy Registration Details</title>

        <style>

        body {

            color: #8BB3DA;

        ]

        </style>

        </head>

        <body style="background-color: #161616;">

        <div style="margin-left: 10%; margin-top: 5%; background-color: #191919; color: #8BB3DA; width: 600px; overflow: none;">

        <h1>Hello '. $this->username .',</h1><br/><br/>

        

        Thank you for registering to Sterling Savvy Web Design. As a new member to our site

        you will be allowed to visit more pages and access more information. As well if you 

        were looking for our services, you can now sign up as a client to us. Please take the

        time to read our full policy, privacy and terms of service pages (located in the footer

        of every page) for our guarantee and site rules. Know that we will not share your email

        address to any other company without your given permission nor will you receive emails

        from us unless you sign up for them. The only exception to this would be web administration

        emails regarding your account.<br/><br/>

        

        Here are the detaisl of your registration, please do not lose this information and keep it safe

        from others. In the event you forget your password we have a password recovery setup

        where your secret question will be displayed so your secret answer would then be required

        to confirm the operation of a password change. If you have any troubles with this an administrator

        can assist you. Just contact Webmaster@SterlingSavvy.com.<br/><br/>

        

        <table cellspacing="0" cellpadding="0" width="600px">

        <tr><td colspan="2">Account Details</td></tr>

        <tr><td>Username:</td><td>'. $this->username .'</td></tr>

        <tr><td>Password:</td><td>'. $this->emailPassword .'</td></tr>

        <tr><td>Email:</td><td>'. $this->email .'</td></tr>

        <tr><td>Secret Question:</td><td>'. $this->question .'</td></tr>

        <tr><td>Secret Answer:</td><td>'. $this->answer .'</td></tr>

        </table><br/><br/>

        

        Best regards,<br/>

        Web Administration

        </div>

        </body>

        </html>

        ';

        

        // To send HTML mail, you can set the Content-type header. 

        $headers  = "MIME-Version: 1.0\r\n";

        $headers .= "Content-type: text/html; charset=iso-8859-1\r\n";

        

        // additional headers 

        $headers .= "To: ". $this->email ." <". $this->email .">\r\n";

        $headers .= "From: SterlingSavvy.com <do_not_reply@sterlingsavvy.com>\r\n";

        

        // Mailing process

        if(mail($this->email, $subject, $message, $headers))

        {

            $this->emailed = "Sent";

        }

        else

        {

            // Since the email didn't send, upload it to the mail database table to be sent later.

            $query = "INSERT INTO mail (email, subject, header, message) VALUES (".$this->email.",".$subject.",".$headers.",".$message.")";

            $result = mysql_query($query);

            

            if(!$result)

                $this->emailed = "Mail Save Failed";

            else 

                $this->emailed = "Not Sent";

        }

        return true;

    }


    /**

     * @return array relational rules.

     */

    public function relations()

    {

        // NOTE: you may need to adjust the relation name and the related

        // class name for the relations automatically generated below.

        // This section follows the following format:

        // 'VarName'=>array('RelationType', 'ClassName', 'ForeignKey', ...additional options)

        return array(

            'client'=>array(self::HAS_ONE, 'client', 'clientID'),

            'profile'=>array(self::HAS_ONE, 'profile', 'profileID'),

            // for RBAC roles setup when established

            // 'userRoles'=>array(self::MANY_MANY, 'className', 'ID'),        

        );

    }


    /**

     * @return array customized attribute labels (name=>label)

     */

    public function attributeLabels()

    {

        return array(

            'userID'=>'User',

            'username'=>'Username',

            'password'=>'Password',

            'password2'=>'Repeat Password',

            'email'=>'Email',

            'ip'=>'Ip',

            'time'=>'Time',

            'question'=>'Question',

            'answer'=>'Answer',

            'verifyCode'=>'Verification Code',

        );

    }

    

    /*

     * @return array of attributes that a safe for modification by user

     */

    public function safeAttributes()

    {

        return array(

            'username, password, password2, email, question, answer, verifyCode',

        );

    }

}



And this is the Validate model:




<?php


class Validate extends CActiveRecord

{

    /**

     * The followings are the available columns in table 'validate':

     * @var integer $userID

     * @var string $username

     * @var string $password

     * @var string $email

     * @var string $ip

     * @var integer $time

     * @var string $session

     * @var string $question

     * @var string $answer

     */

    

    /*

     * Define a repeat password variable and the Captcha variable and 

     * if email sent. For validation process, if session is found and valid.

     * For outside class use

     */

    public $password2;

    public $verifyCode;

    public $emailed = "Not Applicable";

    public $sessionValid = "Not Applicable";

    public $sessionLink;


    /**

     * Returns the static model of the specified AR class.

     * @return CActiveRecord the static model class

     */

    public static function model($className=__CLASS__)

    {

        return parent::model($className);

    }


    /**

     * @return string the associated database table name

     */

    public function tableName()

    {

        return 'validate';

    }


    /**

     * @return array validation rules for model attributes.

     */

    public function rules()

    {

        return array(

            // define the length of all attributes

            array('username','length','max'=>32),

            array('password','length','max'=>64, 'min'=>6),

            array('password2','length','max'=>64, 'min'=>6),

            array('email','length','max'=>256),

            array('question','length','max'=>256),

            array('answer','length','max'=>128),

            array('ip','length','max'=>15),

            array('session','length','max'=>32),

            

            // define the parameters of the time attribute

            array('time', 'numerical', 'integerOnly'=>true),

            

            // convert username, question and answer to lower case

            array('username, question, answer', 'filter', 'filter'=>'strtolower'),

            

            // compare password to repeated password

            array('password', 'compare', 'compareAttribute'=>'password2'), 

            

            // make sure email is a valid email

            array('email','email'),

            // make sure username and email are unique

            array('username, email', 'unique'), 

            

            // All values are required upon registration

            array('username, password, password2, email, question, answer, verifyCode', 'required'),

            

            // verifyCode needs to be entered correctly

            array('verifyCode', 'captcha', 'allowEmpty'=>!extension_loaded('gd')),

        );

    }

    

    /*

     * Override the default function to apply changes after saving to the database

     */

    public function afterSave()

    {

        /*

         * This is the Confirmation Email setup 

         */ 

        $subject = "Account Confirmation - SterlingSavvy.com";

        $message = '

        <html>

        <head>

        <title>SterlingSavvy Account Confirmation</title>

        </head>

        <body style="background-color: #161616;">

        <div style="margin-left: 10%; margin-top: 5%; background-color: #191919; color: #8BB3DA; width: 600px; overflow: none;">

        <h1>Hello '. $this->username .',</h1><br/><br/>

        

        You are receiving this e-mail because you or someone else used your address to sign up on our site.<br/>

        To complete the sign-up process please follow 

        <a href="'. $this->sessionLink .'">this link</a>.<br/><br/>

        

        If you didn\'t sign up on our site, just ignore this message and please accept our apologies. <br/>

        Within 7 days the registration will be removed from our records and you will not receive emails or other notices from us.<br/>

        Your e-mail was submitted from IP '. $this->ip .' on '. date("r") .' (server time).<br/><br/>

        

        Best regards,<br/>

        Web Administration

        </div>

        </body>

        </html>

        ';

        // http://www.sterlingsavvy.com/site/index.php?r=user/validate&code='. $this->session .'

        // To send HTML mail, you can set the Content-type header. 

        $headers  = "MIME-Version: 1.0\r\n";

        $headers .= "Content-type: text/html; charset=iso-8859-1\r\n";

        

        // additional headers 

        $headers .= "To: ". $this->email ." <". $this->email .">\r\n";

        $headers .= "From: SterlingSavvy.com <do_not_reply@sterlingsavvy.com>\r\n";

        

        // Mailing process

        if(mail($this->email, $subject, $message, $headers))

        {    

            $this->emailed = "Sent";

        }

        else 

        {

            // Since the email didn't send, upload it to the mail database table to be sent later.

            $query = "INSERT INTO mail (email, subject, header, message) VALUES (".$this->email.",".$subject.",".$headers.",".$message.")";

            $result = mysql_query($query);

            

            if(!$result)

                $this->emailed = "Mail Save Failed";

            else 

                $this->emailed = "Not Sent";

        }

        return true;

    }


    /**

     * @return array relational rules.

     */

    public function relations()

    {

        // NOTE: you may need to adjust the relation name and the related

        // class name for the relations automatically generated below.

        return array(

        );

    }


    /**

     * @return array customized attribute labels (name=>label)

     */

    public function attributeLabels()

    {

        return array(

            'userID'=>'User',

            'username'=>'Username',

            'password'=>'Password',

            'password2'=>'Repeat Password',

            'email'=>'Email',

            'ip'=>'Ip',

            'time'=>'Time',

            'session'=>'Session',

            'question'=>'Question',

            'answer'=>'Answer',

            'verifyCode'=>'Verification Code',

        );

    }

    

    /*

     * @return array of attributes that a safe for modification by user

     */

    public function safeAttributes()

    {

        return array(

            'username, password, password2, email, question, answer, verifyCode',

        );

    }

}



A final, ForgotPassword model:




<?php


class ForgotPassword extends CActiveRecord

{

    /**

     * The followings are the available columns in table 'user':

     * @var integer $userID

     * @var string $username

     * @var string $password

     * @var string $email

     * @var string $ip

     * @var integer $time

     * @var string $question

     * @var string $answer

     */

    

    /*

     * Define a repeat password variable and the Captcha variable

     * For outside class use

     */

    public $password2;

    public $stage = "Not Applicable";

    public $emailed = "Not Applicable";

    public $saveQuestion;

    private $emailPassword;


    /**

     * Returns the static model of the specified AR class.

     * @return CActiveRecord the static model class

     */

    public static function model($className=__CLASS__)

    {

        return parent::model($className);

    }


    /**

     * @return string the associated database table name

     */

    public function tableName()

    {

        return 'user';

    }


    /**

     * @return array validation rules for model attributes.

     */

    public function rules()

    {

        return array(

            // Define all the attribute lengths

            array('username','length','max'=>32, 'on'=>'stage1'),

            array('password','length','max'=>64, 'min'=>6, 'on'=>'stage3'),

            array('password2','length','max'=>64, 'min'=>6, 'on'=>'stage3'),

            array('email','length','max'=>256, 'on'=>'stage1'),

            array('question','length','max'=>256),

            array('answer','length','max'=>128, 'on'=>'stage2'),

            array('ip','length','max'=>15),

            array('time', 'numerical', 'integerOnly'=>true),

            

            // make sure email is a valid email

            array('email','email', 'on'=>'stage1'),

            

            // Compare password to repeated password

            array('password', 'compare', 'compareAttribute'=>'password2', 'on'=>'stage3'),


            // Setting up when to have what fields. 

            array('username, email', 'required', 'on'=>'stage1'),

            array('answer', 'required', 'on'=>'stage2'),

            array('password, password2', 'required', 'on'=>'stage3'),

        );

    }

    

    /**

     * @return actions to perform before validating

     */

    public function beforeValidate()

    {

        // store password for sending after registration via email

        $this->emailPassword = $this->password;

        return true;

    }

    

    /**

     * @return actions to perform before saving ie: hash password

     */

    public function beforeSave()

    {

        // hash password for database.

        $pass = md5(md5($this->password).Yii::app()->params["salt"]);

          $this->password = $pass;

          return true;

    }

    

    /**

     * @return actions to perform after saving ie: Send email with account info

     */

    public function afterSave()

    {

        /*

         * This is the Password Change Email setup 

         */ 

        $subject = "Password Change - SterlingSavvy.com";

        $message = '

        <html>

        <head>

        <title>SterlingSavvy Password Change</title>

        <style>

        body {

            color: #8BB3DA;

        }

        </style>

        </head>

        <body style="background-color: #161616;">

        <div style="margin-left: 10%; margin-top: 5%; background-color: #191919; color: #8BB3DA; width: 600px; overflow: none;">

        <h1>Hello '. $this->username .',</h1><br/><br/>

        

        Your password change request has been complete. Below is your new password for your account.<br/>

        If you have any troubles an administrator can assist you. Just contact Webmaster@SterlingSavvy.com.<br/><br/>

        

        <table cellspacing="0" cellpadding="0" width="600px">

        <tr><td colspan="2">Account Details</td></tr>

        <tr><td>Username:</td><td>'. $this->username .'</td></tr>

        <tr><td>Password:</td><td>'. $this->emailPassword .'</td></tr>

        <tr><td>Email:</td><td>'. $this->email .'</td></tr>

        <tr><td>Secret Question:</td><td>'. $this->question .'</td></tr>

        <tr><td>Secret Answer:</td><td>'. $this->answer .'</td></tr>

        </table><br/><br/>

        

        Best regards,<br/>

        Web Administration

        </div>

        </body>

        </html>

        ';

        

        // To send HTML mail, you can set the Content-type header. 

        $headers  = "MIME-Version: 1.0\r\n";

        $headers .= "Content-type: text/html; charset=iso-8859-1\r\n";

        

        // additional headers 

        $headers .= "To: ". $this->email ." <". $this->email .">\r\n";

        $headers .= "From: SterlingSavvy.com <do_not_reply@sterlingsavvy.com>\r\n";

        

        // Mailing process

        if(mail($this->email, $subject, $message, $headers))

        {

            $this->emailed = "Sent";

        }

        else

        {

            // Since the email didn't send, upload it to the mail database table to be sent later.

            $query = "INSERT INTO mail (email, subject, header, message) VALUES (".$this->email.",".$subject.",".$headers.",".$message.")";

            $result = mysql_query($query);

            

            if(!$result)

                $this->emailed = "Mail Save Failed";

            else 

                $this->emailed = "Not Sent";

        }

        return true;

    }


    /**

     * @return array relational rules.

     */

    public function relations()

    {

        // NOTE: you may need to adjust the relation name and the related

        // class name for the relations automatically generated below.

        return array(

        );

    }


    /**

     * @return array customized attribute labels (name=>label)

     */

    public function attributeLabels()

    {

        return array(

            'userID'=>'User',

            'username'=>'Username',

            'password'=>'Password',

            'password2'=>'Repeat Password',

            'email'=>'Email',

            'ip'=>'Ip',

            'time'=>'Time',

            'question'=>'Question',

            'answer'=>'Answer',

        );

    }

    

    /*

     * @return array of attributes that a safe for modification by user

     */

    public function safeAttributes()

    {

        return array(

            'username, password, password2, email, answer',

        );

    }

}



Finally everything is tied in through this User Controller:

This is the first stage to registration, it stores to the validate table.




public function actionRegister()

    {

        $form=new Validate;

        // collect user input data

        if(isset($_POST['Validate']))

        {

            $form->attributes=$_POST['Validate']; // set all attributes with post values

            

            // Check if question has a ? at the end

            $last = $form->question[strlen($form->question)-1]; 

            if($last !== "?")

            {

                $form->question .= '?';

            }


            // This assigns the user ip, the current time and the session variable.

            // The session variable needs to be unique but unknown to the user how.

            $form->ip = $_SERVER['REMOTE_ADDR'];

            $form->time = time();

            $form->session = md5($form->time.rand(100000,999999));

            // Define the session link

            $form->sessionLink = $this->createAbsoluteUrl('user/validate', array('code'=>$form->session));

            

            // validate user input and redirect to previous page if valid

            if($form->validate())

            {    

                // save user registration

                $form->save();

            }

        }

        // display the registration form

        $this->render('register',array('form'=>$form));

    }



The email either mucks up or gets sent. If it’s sent the user gets their their session hash link - if you’re on a local machine development use POSTcast free smtp server. Easy and flexible - this will let you see the emails you send and thus the link you can copy and paste in the browser.

Once the user clicks the link, the following action happens:




public function actionValidate()

    {

        $code = $_GET['code'];

        

        $validate = new Validate;

        // find hash

        $validate = Validate::model()->find('session=?', array($code));

        

        if($validate == NULL)

        {

            $validate->sessionValid = "Not Valid";

            $this->render('validate',array('validate'=>$validate));

        }

        else

        {

            $user = new User;

            

            // Define how long until the session expires

            // 7 is the number of days.

            $valid = 3600*24*7;

            $time = time(); 

            $timeElasped = $time - $valid;

            

            if($validate->time < $timeElasped)

            {

                // Took to long to confirm registration

                $validate->sessionValid = "Not Valid";

                // Delete the record

                $validate->delete();

            }

            else

            {

                // Session is valid, transfer data to user table

                $validate->sessionValid = "Valid";

                // Set all the column values

                $user->username = $validate->username;

                $user->password = $validate->password;

                $user->email = $validate->email;

                $user->ip = $_SERVER['REMOTE_ADDR'];

                $user->time = time();

                $user->question = $validate->question;

                $user->answer = $validate->answer;

                

                if($user->validate())

                {    

                    // Save to the user table    

                    $user->save();

                    // Delete the record

                    $validate->delete();

                }

                

                if($user->emailed == "Sent")

                {

                    $this->redirect(array('site/login')); 

                }

                else

                {

                    $validate->emailed = $user->emailed;

                    $this->render('validate',array('validate'=>$validate));

                }

            } // close inner else statement                        

        } // close outer else statement        

    }



That’s it, if everything goes to plan, the email in the after save function of the user table will be sent. This same email is set up to send on the password forgot too ;).

Finally this is the part you were waiting for: The forgot password. It’s set up in 3 stages, which are three different actions that all lead to the same view but for different stages - I’ll show you the view integration after.




    public function actionForgotPassword()

    {

        $form=new ForgotPassword;

        $query=new ForgotPassword;

        $form->scenario='stage1';

        

        // collect user input data

        if(isset($_POST['ForgotPassword']))

        {

            $form->attributes=$_POST['ForgotPassword']; // set all attributes with post values

            

            // validate user input and redirect

            if($form->validate())

            {

                $query = ForgotPassword::model()->find('username=?', array($form->username));

                if($query != NULL)

                {

                    if($form->email != $query->email)

                    {

                        $form->stage = "Email Not Found";

                    }

                    else

                    {

                        $url = $this->createUrl('user/validatereset', array('question'=>$query->question));

                        $this->redirect($url);

                    }

                }

                else

                {

                    $form->stage = "Username Not Found";

                }

            }

            else 

            {

                $form->stage = "Find User";

            }

        }

        else

        {

            $form->stage = "Find User";    

        }

        // display the registration form        

        $this->render('forgotPassword',array('form'=>$form));

    }

    

    public function actionValidateReset()

    {

        $form=new ForgotPassword;

        $query=new ForgotPassword;

        $form->scenario='stage2';

        

        $question = $_GET['question'];

        $form->question = $question;

        

        // collect user input data

        if(isset($_POST['ForgotPassword']))

        {

            $form->attributes=$_POST['ForgotPassword']; // set all attributes with post values

            

            // validate user input and redirect

            if($form->validate())

            {

                $query = ForgotPassword::model()->find('answer=?', array($form->answer));

                if($query != NULL)

                {

                    $url = $this->createUrl('user/resetpassword', array('row'=>$query->userID));

                    $this->redirect($url);    

                }

                else

                {

                    $form->stage = "Answer Invalid";

                }

            }        

            else 

            {

                $form->stage = "Answer Question";

            }

        }

        else

        {

            $form->stage = "Answer Question";    

        }

        // display the registration form        

        $this->render('forgotPassword',array('form'=>$form));

    }

    

    public function actionResetPassword()

    {

        $form=new ForgotPassword;

        $form->scenario='stage3';

        

        $row = $_GET['row'];

        $form->userID = $row;

        

        // collect user input data

        if(isset($_POST['ForgotPassword']))

        {

            $form->attributes=$_POST['ForgotPassword']; // set all attributes with post values

            

            if($form->validate())

            {    

                $query = ForgotPassword::model()->find('userID=?', array($form->userID));

                // This assigns the user ip, the current time

                $query->ip = $_SERVER['REMOTE_ADDR'];

                $query->time = time();

                $query->password = $form->password;

                $query->password2 = $form->password2;

                        

                if($query->validate())

                {

                    // Save the new password

                    $query->save();

                    $form->emailed = $query->emailed;

                }

            }

        }

        if($form->emailed == "Sent")

        {

            

            $this->render('forgotPassword',array('form'=>$form));

        }

        else 

        {

            $form->stage = "Reset Password";

            // display the password reset form        

            $this->render('forgotPassword',array('form'=>$form));

        }

    }



Oh before I forget, for the user controller don’t forget to override the actions function if you use captcha like so:




/**

     * Declares class-based actions.

     */

    public function actions()

    {

        return array(

            // captcha action renders the CAPTCHA image

            // this is used by the register page

            'captcha'=>array(

                'class'=>'CCaptchaAction',

                'backColor'=>0xEBF4FB,

            ),

        );

    }



So that’s all the fancy business, now for the fancy view business.

Rather than show you the whole view I’ll just put the fancy stuff in because this post is long enough with the code provided. The normal form setup you can use from the previous registration you worked through:

The register view




<?php $this->pageTitle=Yii::app()->name . ' - Register'; ?>


<h1>Register</h1>


<?php 


if($form->emailed == "Sent")

{

    ?>

    <p>Thank you for registering.<br/>

    An e-mail was sent to <?php echo($form->email); ?>

    Please check your email and confirm your membership within 7 days.

    </p>

    <?php

}

else if($form->emailed == "Not Sent")

{

    ?>

    <p>We weren't able to send you the confirmation e-mail.<br/>

    Please contact the Website Administration.

    </p>

    <?php 

}

else if($form->emailed == "Mail Saved Failed")

{

    ?>

    <p>There was an issue with our system to send your confirmation email.<br/>

    Please contact the Website Administrator to inform them of this problem.<br/>

    You will more than likely have to redo the account registration process. <br/>

    We Apologize for this inconvenience.

    </p>

    <?php 

}

else 

{

?>

<div class="yiiForm">

<?php echo CHtml::beginForm(); ?>


...


} // close the else statement at the end



The validate view




<?php $this->pageTitle=Yii::app()->name . ' - Validate'; ?>


<h1>Validation</h1>


<?php 


if($validate->emailed == "Not Sent")

{

    ?>

    <p>Thank you for confirming your registration.<br/>

    Your registration went through successfully and you may now 

    <?php echo CHtml::link('Login', array('site/login')); ?>.<br/>

    Please check your email as you should receive an email from us

    shortly containing the details to your acccount registration.

    </p>

    <?php

}

else if($validate->emailed == "Mail Saved Failed")

{

    ?>

    <p>Thank you for confirming your registration.<br/>

    While Your registration went through successfully and you may now 

    <?php echo CHtml::link('Login', array('site/login')); ?>;

    there was an issue with our system to send your account details via email.<br/>

    Please contact the Website Administrator to inform them of this problem.<br/><br/>

    The following information is the details to your account, please record this information

    and keep it safe from others.<br/><br/>

    Username: <?php echo($validate->username);?><br/>

    Password: <?php echo($validate->password);?><br/>

    Email: <?php echo($validate->email);?><br/>

    Question: <?php echo($validate->question);?><br/>

    Answer: <?php echo($validate->answer);?><br/>

    </p>

    <?php 

}

else if($validate->sessionValid = "Not Valid")

{

    ?>

    <p>Your session code has expired.<br/><br/>

    This is because our valid time frame for confirming a registration to our site is 7 days.

    If your code has expired prematurely we sincerely apologize and would request that you

    redo the registration process.<br/> 

    (Your username and other details will still be available)<br/><br/>

    If you run into the same problem again please contact the Website Administration.<br/><br/>

    Since your session code expired you will have to do the registration process again. You can

    sign up for an account <?php echo CHtml::link('Here', array('user/register')); ?>.<br/><br/>

    Thank you for your cooperation.

    </p>

    <?php 

}

?>



There’s not much to the validate view because I’ve set it up to forward (in the user controller through the validate action) to the user login page.

Finally The forgot password view - this one is hefty




<?php $this->pageTitle=Yii::app()->name . ' - Forgot Password'; ?>


<h1>Forogt Password</h1>


<?php 


if($form->emailed == "Sent")

{

    ?>

    <p>Your Password was succesfully changed.<br/>

    An e-mail was sent to <?php echo($form->email); ?> with updated account details.

    </p>

    <?php

}

else if($form->emailed == "Not Sent")

{

    ?>

    <p>We weren't able to send you the confirmation e-mail.<br/>

    Please contact the Website Administration.

    </p>

    <?php 

}

else if($form->emailed == "Mail Saved Failed")

{

    ?>

    <p>While your Password has been successfully changed,

    there was an issue with our system to send your account details via email.<br/>

    Please contact the Website Administrator to inform them of this problem.<br/><br/>

    The following information is the details to your account, please record this information

    and keep it safe from others.<br/><br/>

    Username: <?php echo($form->username);?><br/>

    Password: <?php echo($form->emailPassword);?><br/>

    Email: <?php echo($form->email);?><br/>

    Question: <?php echo($form->question);?><br/>

    Answer: <?php echo($form->answer);?><br/>

    </p>

    <?php 

}


if($form->stage == "Username Not Found")

{

    ?>

    <p>The Username you provided was not found in our database, please try again.</p>

    <?php 

    $form->stage = "Find User";

}


if($form->stage == "Email Not Found")

{

    ?>

    <p>The Email you provided does not match the Email associated with the

     Username found in our database, please try again.</p>

    <?php 

    $form->stage = "Find User";

}


if($form->stage == "Answer Invalid")

{

    ?>

    <p>The Answer you provided was not correct, please try again.</p>

    <?php

    $form->stage = "Answer Question"; 

}


if($form->stage == "Answer Question")

{

    ?>

    <div class="yiiForm">

    <?php echo CHtml::beginForm(); ?>

    

    <?php echo CHtml::errorSummary($form); ?>

    

    <div class="simple">

    Your Question: <?php echo ($form->question); ?>

    </div>

    <div class="simple">

    <p class="hint" style="margin-left:70px;">

    Please Enter Your Secret Answer to Your Secret Question:

    </p>

    <br/>

    <?php echo CHtml::activeLabel($form,'answer', array('style'=>'width:150px;')); ?>

    <?php echo CHtml::activeTextField($form,'answer') ?>

    </div>

    <br/>

    <div class="action">

    <?php echo CHtml::submitButton('Answer'); ?>

    </div>

    <?php echo CHtml::endForm(); ?>

    </div><!-- yiiForm -->

    <?php 

}


if($form->stage == "Reset Password")

{

    ?>

    <div class="yiiForm">

    <?php echo CHtml::beginForm(); ?>

    

    <?php echo CHtml::errorSummary($form); ?>

    

    <div class="simple">

    <p class="hint" style="margin-left:70px;">

    Enter a new password for your account:

    </p>

    <br/>

    <?php echo CHtml::activeLabel($form,'password', array('style'=>'width:150px;')); ?>

    <?php echo CHtml::activePasswordField($form,'password') ?>

    </div>

    <div class="simple">

    <?php echo CHtml::activeLabel($form,'password2', array('style'=>'width:150px;')); ?>

    <?php echo CHtml::activePasswordField($form,'password2') ?>

    </div>

    <br/>

    <div class="action">

    <?php echo CHtml::submitButton('Change Password'); ?>

    </div>

    <?php echo CHtml::endForm(); ?>

    </div><!-- yiiForm -->

    <?php 

}


if($form->stage == "Find User")

{

?>

<div class="yiiForm">

<?php echo CHtml::beginForm(); ?>


<?php echo CHtml::errorSummary($form); ?>


<div class="simple">

<p class="hint" style="margin-left:70px;">

Please Enter Your Username and Email:

</p>

<br/>

<?php echo CHtml::activeLabel($form,'username', array('style'=>'width:150px;')); ?>

<?php echo CHtml::activeTextField($form,'username') ?>

</div>

<div class="simple">

<?php echo CHtml::activeLabel($form,'email', array('style'=>'width:150px;')); ?>

<?php echo CHtml::activeTextField($form,'email') ?>

</div>

<br/>

<div class="action">

<?php echo CHtml::submitButton('Find Account'); ?>

</div>

<?php echo CHtml::endForm(); ?>

</div><!-- yiiForm -->

<?php 

} // close statement



I posted the whole view because you can see how I implement the if statements and also altered the same very variable to display the proper form again when required. This is because I haven’t set up any ajax to work with it yet to make that pulled from else where on the fly.

As you can see it’s hefty and it’s messy but it works so try building around it taking what’s good and making it more efficient.

Things like fixing up the variables to true or false set ups (I used strings so I could know my logic and work flow but they should be changed for efficiency).

When transfering the validate information over to the user table that could be done with a for each loop setup with a condition that skips the session value.

The whole email setup and how it moves on and stuff needs to be better designed. But I was just trying to implement the logic and then you look at what you have and thing how you can improve it. Ultimately it helps you decide what you need to make for a widget / module.


Now with all this over and said I’m currently working on a comprehensive total guide to the Yii framework it will go from start to finish building a site from scratch. Teaching how to implement everything. I started a week ago and actually until I finish playing with some site code and exploring other stuff I put it on hold the last couple of days but I’m already up to about 4 or 5 chapters, 70+ pages typed, 25 000 words (code snippets included), the code snippets are used as you tackle each section and then at the end of each section full code layouts of models, controllers and the works are posted. There’s also full guides of screen shots for the command promt usage (for those new to it) and then as you build your site the changes you make.

I can see this puppy being about 250-300+ pages and probably at least 100 000 words when it’s finished.

It’s a full walk through going start to finish in customizing your own site with the framework. Rather than create a skeleton, it’s more geared at showing you where to edit everything and how so you can build and learn how to customize the framework to your likings. It’ll include advance stuff like Ajax and JQuery usage, the whole deal. As it stands I already go through the database layout, site planning stage and start the coding process I stop at user actions - that’s why I finished this code I showed you here to implement with the guide, but it needs to be cleaned up horrificly. You can see the user registration was cleaned up a bit since the original post in this thread.

Just a little teaser here’s the table of contents:

Table of Contents

About the Author. 2

Copyright. 2

Introduction. 5

Chapter 1 – The Framework. 7

PHP Frameworks. 7

OOP – Object Oriented Programming. 8

The MVC Model. 10

M for Model 10

V for View… 11

C for Controller 11

Yii Framework. 11

The Protected Folder 11

Commands. 12

Components. 12

Config. 12

Controllers. 12

Extensions. 12

Messages. 12

Models. 13

Runtime. 13

Views. 13

Yii Convention. 13

Chapter 2 – Your First Yii Application… 15

Planning. 15

Getting Started. 16

Yiic. 16

Building a Model. 19

Building a Controller. 21

Building a View… 27

Touching Up Our Application. 31

Chapter 3 – The Basics. 33

Building the Template. 33

The Main Config File. 33

The Site Title. 33

The Database Connection String. 34

Website Email 35

Setting up User Authentication. 35

Building the Plan. 39

What Is My Site For?. 39

What I Want 40

Breaking It Down. 41

Building the Database. 43

Where to Start?. 43

Client Table. 44

User Table. 46

The Profiles Table and Validate Table. 48

The Event Table and Message Table. 49

The News Table and Comment Table. 51

The Search Tables. 52

The RBAC Tables. 54

Making the Relationships. 55

MySQL and Our Database. 58

Chapter 4 – User Features. 63

User Registration. 63

The User Model 63

Defining Additional Attributes. 65

The Rules Function. 66

The Relations Function. 68

Overriding Default Functions. 70

I can’t tell you when it’ll be complete but know that once I get my plan in focus and learn more about the framework a lot will be added. Currently the site is set up with a calendar ideal, rbac setup, site search feature (custom so you chose which area of the site is searched - it’s meant to ignore client information) A client side - it’s a web design website (duh lol), there’s also a news feature, 1 on 1 chat with clients on their projects bla bla.

Just a teaser though lol.

Hello,

Your stuff looks awesome. Are you working on this?

http://www.yiiframework.com/forum/index.php?/topic/2819-author-yii-book-packt-publishing/

–iM

I haven’t approached them no. I’m doing it more at my own pace and in learning the framework for me sharing my knowledge. Actually I just loaded on my public development site Jonah’s skeleton that I tampered with all night. I fixed up some of the Ajax to make it dynamical content for the entire site - while also tampering with a lot of the design.

Feel free to check it out: http://www.sterlingsavvy.com/ssweb/

It looks normal, but then log in as the admin : admin account and you can see all the ID’s I set up with the dynamic content loading from the database upon editing. Makes it so a site admin can edit content on the fly. Essentially making it so that you no longer have to worry about opening files remotely and editing HTML or having to even manually do all the changes for clients - you simple give them a guide on basic HTML codings (<p>, <br/>, <ul> etc. ) and you’re set.

Hi!

When I use following line


public function rules()

{

   array('username', 'filter', 'filter'=>'strtolower'),

}



due to the first post in this topic

I got an error:

call_user_func_array() expects parameter 2 to be array, string given

It happens in framework\validators\CFilterValidator.php(47)

Ideas?

This bug is fixed in svn.

Please wait a few days until 1.0.9. gets released.


public function rules()

{

        return array(

             array('username', 'filter', 'filter'=>'strtolower'),

        );

}



That should do it, the function rules is expecting you to return an array of information, and in the array is more arrays of information.

thanks for quick reply and link to svn.

hi,

I would like to add a checkbox (to check user has read and agree to the terms of use) to the registration form.

How to validates the checkbox is checked?

I new in Yii so help me please!

thanks so much

Ok so I looked into this, here’s a link to the validation functions in Yii: http://www.yiiframework.com/doc/guide/form.model#declaring-validation-rules

Scroll down a bit you’ll see them. Obviously this doesn’t show the PHP functions that can also be applied (like strtolower, other string functions, etc).

Now based on what you said, you could always setup a rule to use the compare feature and compare it to the value 1 (being true, that it’s checked). This may or may not work, I’m not 100% sure but that’s my best guess.

At any rate it would be something simple as :




array('checkBox', 'compare', 'compareAttribute'=>1),



If that doesn’t work then you may have to build your own validation rule. Now technically you could just extend your model class and use it as a local function (building something like - checkCheckBox and in it you place the code to check the value of the checkbox), however it’s smarter to extend the Validator class. Here’s a link on extending CValidator: http://www.yiiframework.com/doc/guide/extension.create#validator

I personally think though that the compare rule should work. Let me know how it goes :)

Hi, I have followed your guide in the first post.

When I try register I get the following error messages:

I have entered everything in. Any idea why this is happening?

EDIT: Nevermind. I had my syntax for my safeAttributes wrong!

Thanks so much, it works now :)

Hi there,

It is really a helpful discussion, I am currently dealing with the same case, and this post helped me a lot.

I have only one more point to add here, CActiveRecored::beforeSave() is executed before any data save operation on the related model, that means Insert and Update. Actually we don’t need to rehash the user password each time we update his lastLoginAt -for instance-.

I think we can handle this case using the following beforSave function


	public function beforeSave(){

		if($this->isNewRecord || isset($this->password)){

			$this->password = sha1($this->password);

		}

		return true;

	}

The second operand in the or expression is to handle the case if the user wants to update his password.

phiras,

If the record is not new, $this->password will always be set, since that is a required field, so it gets populated from database for sure.

Well, and here is the problem. the password hash digest is stored inside the DB, Without my code, each update on a record will rehash that digest again, which will make the user unable to login using his password.

Great thread. Just one thing I would like to add that usually, in my login systems, we use two salts. One defined for the whole application (as you did with the param), another defined for each user in his table row. So in the end the password can be something like sha1($user->password).$Yii::app()->params["salt"].$user->salt). Of course, you have to define a function to generate the salt when the user registers.

And finally, as many pointed out, make sure your login form is sending data over https.

Hi!

I have followed the thread and the tutorial making my own registration form. I had the same problems as above, but all are now solved and it works as it should. Except one little thing! My $confirmPassword attribute is declared as required in the validation rules, but in the form the red star (*) indicating required is not there how can I fix that?

My model:


<?php


class Users extends CActiveRecord

{

	/**

	 * The followings are the available columns in table 'Users':

	 * @var integer $UsersID

	 * @var string $Email

	 * @var string $Firstname

	 * @var string $Lastname

	 * @var string $Password

	 * @var string $CreatedDate

	 */


	/**

	 * This variable is added here for the purpose of working with the signup-form and other password related forms.

	 * @var unknown_type

	 */

	public $confirmPassword;


	/**

	 * Returns the static model of the specified AR class.

	 * @return CActiveRecord the static model class

	 */

	public static function model($className=__CLASS__)

	{

		return parent::model($className);

	}


	/**

	 * @return string the associated database table name

	 */

	public function tableName()

	{

		return 'Users';

	}


	/**

	 * @return array validation rules for model attributes.

	 */

	public function rules()

	{

		return array(

		array('Email','length','max'=>128),

		array('Email','email'),

		array('Email', 'unique'),

		//array('Email', 'filter', 'strtolower'),

		array('Firstname','length', 'min'=>2, 'max'=>32),

		array('Lastname','length', 'min'=>3, 'max'=>32),

		array('Password','length','min'=> 6, 'max'=>64),

		array('Password','compare', 'compareAttribute'=>'confirmPassword'),

		array('Email, Firstname, Lastname, Password, confirmPassword, CreatedDate', 'required'),

		);

	}


	/**

	 * @return array relational rules.

	 */

	public function relations()

	{

		// NOTE: you may need to adjust the relation name and the related

		// class name for the relations automatically generated below.

		return array(

			'circles' => array(self::HAS_MANY, 'GiftCircle', 'UsersID'),

			'memberof' => array(self::HAS_MANY, 'GiftCircleMembers', 'UsersID'),

			'wishlists' => array(self::HAS_MANY, 'WishList', 'UsersID'),

			'reservations' => array(self::HAS_MANY, 'WishListItemReservation', 'UsersID'),

		);

	}


	/**

	 * @return array customized attribute labels (name=>label)

	 */

	public function attributeLabels()

	{

		return array(

			'UsersID' => 'Users',

			'Email' => 'Email',

			'Firstname' => 'Firstname',

			'Lastname' => 'Lastname',

			'Password' => 'Password',

			'confirmPassword' => 'Password again',

			'CreatedDate' => 'Created Date',

		);

	}

	

	public function safeAttributes(){

		return array(

			'Email,Firstname,Lastname,Password,confirmPassword',

		);

	}


	public function beforeSave()

	{

		$pass = md5($this->Password);

		$this->Password = $pass;

		return true;

	}


	public function beforeValidate(){

		$this->CreatedDate = date('Y-m-d H:i:s');

		return true;

	}

}



And my form:


<div class="yiiForm">


<p>

Fields with <span class="required">*</span> are required.

</p>


<?php echo CHtml::beginForm(); ?>


<?php echo CHtml::errorSummary($model); ?>


<div class="simple">

<?php echo CHtml::activeLabelEx($model,'Email'); ?>

<?php echo CHtml::activeTextField($model,'Email',array('size'=>60,'maxlength'=>128)); ?>

</div>

<div class="simple">

<?php echo CHtml::activeLabelEx($model,'Firstname'); ?>

<?php echo CHtml::activeTextField($model,'Firstname',array('size'=>32,'maxlength'=>32)); ?>

</div>

<div class="simple">

<?php echo CHtml::activeLabelEx($model,'Lastname'); ?>

<?php echo CHtml::activeTextField($model,'Lastname',array('size'=>32,'maxlength'=>32)); ?>

</div>

<div class="simple">

<?php echo CHtml::activeLabelEx($model,'Password'); ?>

<?php echo CHtml::activePasswordField($model,'Password',array('size'=>60,'maxlength'=>64)); ?>

</div>

<div class="simple">

<?php echo CHtml::activeLabelEx($model,'Password agian'); ?>

<?php echo CHtml::activePasswordField($model,'confirmPassword',array('size'=>60,'maxlength'=>64)); ?>

</div>


<div class="action">

<?php echo CHtml::submitButton('Save'); ?>

</div>


<?php echo CHtml::endForm(); ?>


</div><!-- yiiForm -->

Notice this line in your view file compared to the rest of them. CHtml::activeLabelEx is effectively a label for a form in html. As such the 2nd passing attribute to the function is referencing the "for" attribute of the label. This means that it tries to identify what field this label is for.

Since in your model class you have the safeAttribute and rules and definition of everything for confirmPassword, it works however come time for the identification of required information highlighting on the view format, it does not appear because it’s trying to reference to a field Password again but no such field exists.

Simple fix:




<div class="simple">

<?php echo CHtml::activeLabelEx($model,'confirmPassword'); ?>

<?php echo CHtml::activePasswordField($model,'confirmPassword',array('size'=>60,'maxlength'=>64)); ?>

</div>




Hope that helps.

Thanx man! :)