Show captcha after <N> unsuccessfull attempts

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

  • in the view, you have to show captcha field if number of unsuccessfull attempts are greater than N

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

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:

public $verifyCode;

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

<?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:

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.

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:

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.php we have set two different required fields depending on scenario:
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:
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 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

13 4
15 followers
Viewed: 34 449 times
Version: 1.1
Category: How-tos
Written by: zitter
Last updated by: zitter
Created on: Jun 12, 2012
Last updated: 11 years ago
Update Article

Revisions

View all history

Related Articles