Difference between #2 and #3 of Show captcha after <N> unsuccessfull attempts

unchanged
Title
Show captcha after <N> unsuccessfull attempts
unchanged
Category
How-tos
unchanged
Tags
session, Authentication, security, form
changed
Content
In this mini howto I would like to show how to add a required captcha field in
the login form, after a defined number of unsuccessfull attempts.
To do this, I will use the blog demo that you have in default Yii download
package (path/to/yii/demos/blog).

Basically, you need three things:

- in the model, you have to add captcha field as a required field in the rules()
method 

- in the controller, you have to create a different LoginForm model if number of
unsuccessfull attempts are greater than **N**<N>

- in the view, you have to show captcha field if number of unsuccessfull
attempts are greater than **N**<N>

In the LoginForm model, you can use 'scenario' to set different required fields,
so:

~~~
[php]
 public function rules()
        {
                return array(
                        // username and password are required
                        array('username, password', 'required'),
                        // rememberMe needs to be a boolean
                        array('rememberMe', 'boolean'),
                        // password needs to be authenticated
                        array('password', 'authenticate'),
                        // add these lines below                    
array('username,password,verifyCode','required','on'=>'captchaRequired'),
                        array('verifyCode', 'captcha',
'allowEmpty'=>!CCaptcha::checkRequirements()),         
                 );
        }
~~~

Moreover, add verifyCode as public property:

~~~
[php]
public $verifyCode;
~~~

In the view, add this code (show captcha field if scenario is set to
'captchaRequired', will see later):

~~~
[php]
  <?php if($model->scenario == 'captchaRequired'): ?>
        <div class="row">
                <?php echo CHtml::activeLabelEx($model,'verifyCode'); ?>
                <div>
                <?php $this->widget('CCaptcha'); ?>
                <?php echo CHtml::activeTextField($model,'verifyCode'); ?>
                </div>
                <div class="hint">Please enter the letters as
they are shown in the image above.
                <br/>Letters are not case-sensitive.</div>
        </div>
        <?php endif; ?>
~~~

Now, the controller. First, add a property to set maximum allowed attempts and a
counter that trace failed attempts time to time:

~~~
[php]
public $attempts = 5; // allowed 5 attempts
public $counter;
~~~

then, add a private function that returns true if 'captchaRequired' session
value is greater than number of failed attempts.

~~~
[php]
 private function captchaRequired()
        {           
                
                return Yii::app()->session->itemAt('captchaRequired')
>= $this->attempts;
        }
~~~

We will use this function to know if captcha is required or not. 
Now, remain to modify *actionLogin()* method:

~~~
[php]
 public function actionLogin()
        {
                $model = $this->captchaRequired()? new
LoginForm('captchaRequired') : new LoginForm;

                // if it is ajax validation request
                if(isset($_POST['ajax']) &&
$_POST['ajax']==='login-form')
                {
                        echo CActiveForm::validate($model);
                        Yii::app()->end();
                }

                // collect user input data
                if(isset($_POST['LoginForm']))
                {
                        $model->attributes=$_POST['LoginForm'];
                        // validate user input and redirect to the previous page
if valid
                        if($model->validate() && $model->login())
                               
$this->redirect(Yii::app()->user->returnUrl);
                        else
                        {
                                $this->counter =
Yii::app()->session->itemAt('captchaRequired') + 1;
                               
Yii::app()->session->add('captchaRequired',$this->counter);
                       }
                }
                // display the login form
                $this->render('login',array('model'=>$model));
        }
~~~

Note that:

- if function *captchaRequired()* returns true create LoginForm with scenario
'captchaRequired', else create LoginForm with default scenario. This is useful
because in
protected/models/LoginForm.phpprotected/models/LoginForm
we have set two different required fields depending on scenario:

~~~
[php]
 public function rules() 
        {               
                return array(
                        array('username, password', 'required'),
array('username,password,verifyCode','required','on'=>'captchaRequired'),
[... missing code...]
        }
~~~

- if validation passes redirect to a specific page, but what if validation
doesn't pass? In this case we increment the counter, then set a session named
'captchaRequired' with counter value, in this way:

~~~
[php]
if($model->validate() && $model->login())
     $this->redirect(Yii::app()->user->returnUrl);
     else
     {
     $this->counter = Yii::app()->session->itemAt('captchaRequired') +
1;
     Yii::app()->session->add('captchaRequired',$this->counter);
     }
~~~

When 'captchaRequired' session will be equal to maximum allowed attempts
(property *$attempts*) private function *captchaRequired()* will return true and
then LoginForm('captchaRequired') will be created. 
With scenario set to 'captchaRequired' captcha will be show in the view:

~~~
[php]
<?php if($model->scenario == 'captchaRequired'): ?>
// code to show captcha
<?php endif; ?>
~~~


Easy, uh? ;)

References
----------

<http://www.yiiframework.com/forum/index.php/topic/21561-captcha-custom-validation>
<http://drupal.org/node/536274>