Monday, April 8, 2013

Refining Access Rules

Access Rules in your various controllers is a great way to control which user is able to perform specific actions. The common layout of the access rules is something like: 

public function accessRules()
{
return array(
array('allow',  // allow all users to perform 'index' and 'view' actions
'actions'=>array('index','view'),
'users'=>array('*'),
),
array('allow', // allow authenticated user to perform 'create' and 'update' actions
'actions'=>array('create','update', 'transfer',),
'users'=>array('@'),
),
array('allow', // allow admin user to perform 'admin' and 'delete' actions
'actions'=>array('delete'),
'users'=>array('admin'),
),
array('deny',  // deny all users
'users'=>array('*'),
),
);
}
This is fine for many applications however sometimes there is a need to further restrict access to custom functions that are created in your controller when there are different types of users that have access to your web application. A typical example is that you have a superuser (Admin), another set of users (Clerical) and then you may allow clients to access your web application as well. In the Access Rules above we have only catered for authenticated users, all users and admin. A client is an authenticated user however you may only want to allow the client to be able to perform one action. One way to do this is by making use of the Identity class which is used in performing user identification.

Typically all users will have their details stored in the database and I usually assign them a role in the "user" database table. For example the field called role may be

User role
User Type Role
Admin
1
Clerical
2
Client
3

Now that each user that is set up in the database has a role we can set the users role that persists throughout the current users session by modifying the UserIdentity Class. In the UserIdentity Class we can create any characteristic we may want to make use of that will persist while the user is logged in.  In the following example I call setState() to store specific information about the user such as their "role", "id" and "username". The syntax to set this information is: $this->setState('variable_name', $record->database_field). Notice the database query for the "User" table gets the required attributes.


class UserIdentity extends CUserIdentity
{
    private $_id;
    public function authenticate()
    {
        //************Database query of User table*************************
        $record=User::model()->findByAttributes(array('username'=>$this->username));

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

        if($record===null)
            $this->errorCode=self::ERROR_USERNAME_INVALID;
        else if($record->password!==$tstvar)

            $this->errorCode=self::ERROR_PASSWORD_INVALID;
        else
        {
            $this->_id=$record->id;
            $this->setState('title', $record->real_name);
            $this->errorCode=self::ERROR_NONE;
            // Store the role in a session:
            $this->setState('role', $record->user_type_id);
            $this->setState('userid', $record->id);
            $this->setState('uname', $record->username);
        }
        return !$this->errorCode;
    }

    public function getId()
    {
        return $this->_id;
    }
}
Now it is easy to retrieve this information once it has been set by using
$currentUser = Yii::app()->user->role;

This can be very useful if you want to have a different menus for users with a different role. An admin user may be able to see a link to an audit trail however you may not want clerical staff to be able to access this information. There are many ways to exploit this attribute and further customise your web application.

However the point of this was to further refine the actions users are able to perform within the different controllers. To make use of the users role to further define what actions that particular user is able to perform take a look at the following example:

public function accessRules()
{
return array(
array('allow',  // allow all users to perform 'index' and 'view' actions
'actions'=>array('index','view'),
'users'=>array('*'),
),
array('allow', // allow authenticated user to perform 'create' and 'update' actions
'actions'=>array('create','update', 'process', 'complete', 'progress',),
'users'=>array('@'),
'expression'=>'isset($user->role) && ($user->role!=3)',
),

   array('allow', // allow authenticated client to perform 'transfer' and 'notice' actions
'actions'=>array('transfer','notice'),
'users'=>array('@'),
'expression'=>'isset($user->role) && ($user->role==3)',
),

array('allow', // allow admin user to perform 'delete' actions
'actions'=>array('delete'),
'users'=>array('@'),
'expression'=>'isset($user->role) && ($user->role==1)',
),
array('deny',  // deny all users
'users'=>array('*'),
),
);
}
In this example only admin users and clerical users can perform the actions 'create','update', 'process', 'complete', 'progress', only clients (role = 3) can perform transfer and notice, and only admin can perform the delete action.

Using the 'expression' attribute in the array for the accessRules you can create very detailed and refined rules as to what actions are allowed to be executed depending on the users defined role.

References

  1. http://www.yiiframework.com/wiki/6/
  2. CBaseUserIdentity 

Thursday, March 7, 2013


Forcing a Password Change

I looked around at a few methods for forcing users to change their password. Typically you may allocate a user an initial  generated password however users will often prefer to have a password they are able to remember. This post will run through allocating a user a generated password and then forcing the user to change the password at their first login. I could not find an easy to follow method all in one place explaining the methodology so I thought I would try to bring the whole process together in one place.

First we will create an initial password for the new user and there are a number of ways to achieve this however I will show you a simple way to create an initial password. In your user controller when creating a new user the following function is simple and easy to implement. Let us assume we are creating a new user that needs a password automatically generated. Create a function in your controller.

public function createPassword()
{
$chars = "abcdefghjkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789";
$str="";
$size = strlen( $chars );
for( $i = 0; $i < 9; $i++ ) {
$str .= $chars[ rand( 0, $size - 1 ) ];
}
return $str;
}

This function will create a password 9 characters in length. I like to omit ambiguous characters such as lowercase letter 'l', uppercase 'I', and 'O' because there can be confusion if the password has a zero or a letter 'O' and he same with the number '1' and the lowercase letter 'l'. You could assign special characters to the $chars variable depending on the password complexity required. To call this function in the user controller all you need to do is use '$this->createPassword to create a random password. In this example I encrypt the password using a simple Secure Hash Algorithm,

$client_password = $this->createPassword();
$new_client = new Client;
$new_client->real_name = 'Joe';
$new_client->username = 'cleric';
$new_client->password = sha1($client_password);
$new_client->active = 1;
$new_client->save();

Now we need some way to identify the user logging in for the first time. I track every time a user logs into a web application so I have a field called "last_login" in the user table. When creating the user leave the last_login field as null so when a user logs into the web application, if the last_login field is NULL then force the password change.

When I create a web application I usually write it in such a way that a user must be logged in to access any page within the application. I will not run through how to do this however look at the article written by Larry Ullman - Forcing Login for All Pages in Yii.  The benefit to using Ullman's approach is that just a small amount of code will add authorisation to the whole web site and you can re-use the method in other applications as the implementation is very straight forward.

Now in the SiteController in the function actionLogin test to see if the last_login field is null. If that is the case then redirect the user to allow the user to change their password to something easier to remember. Do a simple query to check the last_login for the user in the actionLogin method in your SiteController.   For example:
public funcion actionLogin()
{
.........

//user login for first time

$upduser=User::model()->findByPk($userId);

if ($upduser->last_login == '') {
$this->redirect('index.php?r=user/firstlogin');
}

else // client has logged in previously
{
$upduser=User::model()->findByPk($id]);
$upduser->last_login = new CDbExpression('NOW()'); //update the users last_login
$upduser->save();
$this->redirect('index.php?r=user/welcome');
}
}

If the last_login field is null the user will get redirected to the UserController::firstlogin method. The methodology I use is adapted from a good StackOverflow discussion which you can find at Yii, best way to implement “user change of password”.

In your UserController create a function called actionFirstLogin: An example:

//function for clients to allow them to change their password on the first login
public function actionFirstlogin()
{
$model=new ChangePasswordForm();
            if (isset($_POST['ChangePasswordForm'])) {
        $model->setAttributes($_POST['ChangePasswordForm']);
        if ($model->validate()) {
            $model->saveNewPassword();
                        $this->redirect('index.php?r=client/postaldata'); //or wherever you want to redirect the user
        }
    }
     $this->render('updatefirst', array('model'=>$model));
}
Create a CFormModel rather than an a CActiveRecord called ChangePasswordForm. This class goes into the directory containing your models. The code for this is as follows remembering that the file must be called ChangePasswordForm.php.

?php
// models/ChangePasswordForm.php

class ChangePasswordForm extends CFormModel
{
    /**
     * @var string
     */
    public $currentPassword;

    /**
     * @var string
     */
    public $newPassword;

    /**
     * @var string
     */
    public $newPasswordRepeat;

    /**
     * Validation rules for this form.
     *
     * @return array
     */
    public function rules()
    {
        return array(
            array('currentPassword, newPassword, newPasswordRepeat', 'required'),
            array('currentPassword', 'validateCurrentPassword', 'message'=>'This is not your password.'),
            array('newPassword', 'compare', 'compareAttribute'=>'newPasswordRepeat'),
            array('newPassword', 'match', 'pattern'=>'/^[a-z0-9_\-]{8,}/i', 'message'=>'Your password be 8 characters (letters/numbers) in length.'),
        );
    }

    /**
     *
     * @return string Hashed password
     */
    protected function createPasswordHash($password)
    {                                                                                                                                                                
        return sha1($password);
    }

    /**
     *
     * @return string
     */
    protected function getUserPassword()
    {
        $curr_user = Yii::app()->db->createCommand()
                    ->select('id, password')
                    ->from('user')
                    ->where('username=:uid', array(':uid'=>Yii::app()->user->uname)) //using a session variable
                    ->queryRow();
        return $curr_user['password'];
    }

    /**
     * Saves the new password.
     */
    public function saveNewPassword()
    {
        $user=User::model()->findByAttributes(array('username'=>Yii::app()->user->uname));
        $user->password = $this->createPasswordHash($this->newPassword);
        $user->last_login = new CDbExpression('NOW()');
        $user->update();
    }

    /**
     * Validates current password.
     *
     * @return bool Is password valid
     */
    public function validateCurrentPassword()
    {
        return $this->createPasswordHash($this->currentPassword) == $this->getUserPassword();
    }
}
The pattern you use in this FormModel depends on the password complexity you require for your site - in this example it is a simple pattern - 'pattern'=>'/^[a-z0-9_\-]{8,}/i', 'message'=>'Your.............
Remember I am just using a simple SHA hash to store my passwords in this example.

You will now need to create a view that will take 3 inputs - Current Password, New Password, Repeat New Password. This view in this example is called updatefirst.php and the view is in the User view directory. The code for this view to allow the input of the old and new password will need to be modified to your needs. An example of the updatefirst.php view file code:
NOTE: Again there are some session variables in this code Yii::app()->user->title) and I am fond of the bootstrap extension hence the bootstrap widgets. Finally I am using renderPartial so you will need 2 view files


<h4>Enter new password for <?php echo Yii::app()->user->title; ?></h4>
<?php $this->widget('bootstrap.widgets.TbLabel', array(
     'type'=>'info', // 'success', 'warning', 'important', 'info' or 'inverse'
     'label'=>'You must change your password - Should be 8 characters long (mixture of letters and numbers',
));

echo '<br /><br />';
$this->renderPartial('_updateformfirst', array('model'=>$model)); ?>

=========================================================
Now the code for the _updateformfirst.php file

<?php
/* @var $this UserController */
/* @var $model User */
/* @var $form CActiveForm */
?>

<div class="wide form">

<?php $form=$this->beginWidget('CActiveForm', array(
'id'=>'user-form',
'enableAjaxValidation'=>false,
)); ?>

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

<?php echo $form->errorSummary($model); ?>

<div class="row">
<?php echo "Enter your current password <span style = 'color:red'><sup>*</sup></span>"; ?>
<?php echo $form->passwordField($model,'currentPassword',array('class'=>'span2','maxlength'=>20)); ?>
</div>

<div class="row">
<?php echo "Enter your new password <span style = 'color:red'><sup>*</sup></span>".'&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'; ?>
<?php echo $form->passwordField($model,'newPassword',array('class'=>'span2','maxlength'=>20)); ?>
</div>

<div class="row">
<?php echo "Repeat your new password <span style = 'color:red'><sup>*</sup></span>".'&nbsp;&nbsp;'; ?>
<?php echo $form->passwordField($model,'newPasswordRepeat',array('class'=>'span2','maxlength'=>20)); ?>
</div>


<div class="row buttons">
<?php echo CHtml::submitButton('Update Password'); ?>
</div>

<?php $this->endWidget(); ?>

</div><!-- form -->

Now if the user enters the old password correctly and the new password matches the repeat new password and the new password meets the password complexity defined the user will have successfully changed their password and the user will get redirected to the page you code into the UserController method actionFirstLogin.

While there are a few steps to creating this workflow for a password change it is fairly easy to implement if you follow the steps outlined. For more information about changing passwords (such as forcing password changes on a time basis e.g. every month) take a look at :

  1. Model password confirmation field
  2. Force a User to Change Their Password (ChangePasswordFilter)
  3. Change Password Action Required - Ideas?
  4. Various Yii Forum Search Results
With some patience and logic it is not difficult to add this feature into your Yii Application.

Friday, December 14, 2012

Setting Up Pagination

There are a few different ways to set up pagination with CGridView and CListView that all ultimately do the same task. It is fairly easy to do when using CDataProvider to populate CGridView and CListView. Take the following example:

public function actionIndex()
{
$dataProvider=new CActiveDataProvider('NewTask', array(
'criteria'=>array(
'condition'=>'status=0',
'order'=>'import_date DESC',
),
'pagination'=>array(
'pageSize'=>5,
),
));
$this->render('index',array(
'dataProvider'=>$dataProvider,
));
}
In this example the 'pagination' property of CDataProvider is used to set the page size to 5. This can be accomplished in the exact same manner however with different syntax.

public function actionProgress()
{
$dataProvider=new CActiveDataProvider('NewTask', array(
'criteria'=>array(
'condition'=>'status>0',
'order'=>'import_date ASC',
),
));
$dataProvider->pagination->pageSize=4;
$this->render('progress',array(
'dataProvider'=>$dataProvider,
));

}
In this example the 'pagination' property is set to a page size of 4. This is useful if you want to display all the items contained in the $dataProvider array. To display all items use the 'totalItemCount' property of CActiveDataProvider.
$dataProvider->pagination->pageSize=$dataProvider->totalItemCount;
What happens when CDataProvider is not obviously used as the data provider for a view? In the view called admin you may find that this is the case. Take the following example of the controller method called actionAdmin.

public function actionAdmin()
{
$model=new NewTask('search');
$model->unsetAttributes();  // clear any default values
if(isset($_GET['NewTask']))
$model->attributes=$_GET['NewTask'];

$this->render('admin',array(
'model'=>$model,
));
}

In this case the data is not being sent to the view with $dataProvider and you will notice in the admin view that CGridView will be similar to:

<?php $this->widget('zii.widgets.grid.CGridView', array(
'id'=>'export-task-grid',
'dataProvider'=>$model->search(),
'filter'=>$model,
'columns'=>array(
'id',......
The important part to notice is 'dataProvider'=>$model->search()' . To control the pagination the search function inside of the respective model needs to be changed.

public function search()
{
// Warning: Please modify the following code to remove attributes that
// should not be searched.

$criteria=new CDbCriteria;

$criteria->compare('id',$this->id);
$criteria->compare('client_id',$this->client_id,true);
$criteria->compare('user_id',$this->user_id,true);
$criteria->compare('status',$this->status);

return new CActiveDataProvider($this, array(
'criteria'=>$criteria,
'pagination'=>array(
'pageSize'=>5,
),
));
}
}
When CActiveDataProvider is returned to the admin view the pagination will be set to 5 (as in this example).
There are also a number of properties in CGridView that are worth knowing about such as 'enableSorting', 'enablePagination', 'summaryText' and 'template' just to name a view. These properties enable you to change the behaviour and appearance of CGridView. As an example of the syntax (using TbGridview):

<?php $this->widget('bootstrap.widgets.TbGridView', array(
'type'=>'striped bordered condensed',
'id'=>'new-task-grid',
'template'=>"{items}",
'dataProvider'=>$model->search(),
'enablePagination'=>true,
 'summaryText'=>'Displaying {start}-{end} of {count} results.',
 'template' => "{summary}{items}{pager}",
'filter'=>$model,
'columns'=>array(

References:

Friday, December 7, 2012

Create Ordered Drop Down List in CGridView

In CGridView it can be really useful to have a drop down to quickly filter a list. For example you may want to create a filter based on Business Name. To quickly filter the list in CGridView adding a drop down list is an excellent option.


For the drop down to be useful often time you will need to create an ordered drop down list and using Yii's Active Record makes this fairly easy to do. Firstly the code:

<?php $this->widget('zii.widgets.grid.CGridView', array(
'id'=>'client-grid',
'dataProvider'=>$model->search(),
'filter'=>$model,
'columns'=>array(
'provider_id',
array(
'name'=>'business_name',
'filter'=>CHtml::listData(Client::model()->findAll(array('condition'=>'active=1',
'order'=>'business_name ASC')),
'business_name','business_name'), //REALLY IMPORTANT
'type'=>'raw',
'value'=>'CHtml::link($data->business_name, array("client/view","id"=>$data->id))',
'htmlOptions'=>array('width'=>'100'),
),
'business_address',
'business_suburb',
'business_postcode',
                array(
'class'=>'CButtonColumn',
),
),
)); ?>

You will notice the I have used a query to return the results to the filter and ordered the Business Name. Take note of  'business_name','business_name' - If you do not use the actual attribute name in this format you will not get the drop down list returning the correct result or the filter will not work. Do not be tempted to use 'id', 'business_name' as you would typically do as this will return a valid drop down list however the filter function will not work. If you want to add a hyperlink to the filter results then ensure you use 'type'=>'raw' (see CFormatter for more information on the various 'types' available).

Sunday, December 2, 2012

Render, Redirect and Variables with Yii

The primary purpose of a View is to display data and accessing data in the View is relatively easy. The easiest way to show how to do this is by example.

In your Controller there are various methods such as actionIndex, actionView, actionCreate etc. Let's break down a simple actionIndex for a model called Client.
public function actionIndex()
{
$dataProvider=new CActiveDataProvider('Client'); //first part
$this->render('index',array(                         //second part
'dataProvider'=>$dataProvider,                  
));
}
The first part of this method is loading data into the $dataProvider array. The second part is responsible for selecting/rendering the appropriate View and pushing the data into the View.

In the second part of the above method (the code responsible for rendering the view) you will notice the first parameter is 'index' and this is the View that will be displayed in the browser. The second parameter 'dataProvider' is the data that is sent to the View called 'index'. Lets look at the CController Class Reference and specifically the method called render().
public string render(string $view, array $data=NULL, boolean $return=false)
You will see the 'render' method takes 3 parameters - the name of the View (a string) , the data passed to the view (an array) and finally a boolean value which we won't worry about. This boolean value can be null (omitted) so it is not required. How do we access the data that is passed to the view? All we need to do is use '$data->property' where property is any of fields in the 'Client' table e.g. $data->name,
$data->address.

It is also possible to send more than one variable to the view and all you need to do is separate these variables with comma's.

public function actionIndex()
{
  $myVariable = "Hello World";
$dataProvider=new CActiveDataProvider('Client');
$this->render('index',array(                      
'dataProvider'=>$dataProvider,   'myVariable'=>$myVariable,            
));
}
To access this additional variable in the 'index' view you only need to refer to the variable name.
<?php echo "This is my variable - ".$myVariable; ?>

You will notice up until now we have been using the 'render' method of the class 'CController'. There is another method you will see in your controller called 'redirect()'. The 'redirect' method is a little different to the 'render' method in terms of how you access the data you push to the view.
public void redirect(mixed $url, boolean $terminate=true, integer $statusCode=302)
Let's just focus on the url parameter -  the documentation says basically that the url parameter can be just the URL to be redirected to OR if the parameter is an array, the first element must be a route to a controller action and the rest are GET parameters in name-value pairs. This means that when using the 'redirect' method the view can be passed to the method as an array along with any variables that need to be pushed to the view. An example of the syntax of the redirect method is:
$this->redirect(array('index','id'=>$model->id, 'myVariable'=>$myVariable,));
Notice that the view called 'index' is in an array with a variable called $myVariable which is a different syntax to the 'render' method. This is the trick - you cannot access the variable called 'myVariable' in the same way as previously with the 'render' method. As the documentation says you need to use the 'GET' method to access this variable. So in the view called 'index' you can access the variable called 'myVariable' by using the following:
<?php echo "This is my variable - ".$_GET['myVariable']; ?>
This is an important difference between the 'render' and 'redirect' method in a controller and is easy to overlook. To access variables pushed to a View, there is an important difference depending on whether the controller uses the 'render' method (in which case you access the pushed variables by the variable name) or the 'redirect' method (variables pushed to the view must be access by the GET method).

This is a good example of how important the Class Reference can be if you are having difficulty with a particular method. The reference may seem a little confusing however once you become accustomed to the syntax the explanation of the parameters can often resolve problems you may experience with a particular method.

References:

  1. Render Method
  2. Redirect Method


Friday, November 30, 2012

Creating a Drop Down List using Constants

In designing databases often time we will use integers to represent data such as 'status' for example rather than actually storing "Created/In Progress/Completed". This is fine however when it comes to entering data into your view you have to know that 1 = In Progress, 0 = Created. It would be easier if you could have a drop down list. With Yii it is not difficult to achieve just that.

The first thing you need to do is create these constants in the Model. Let us assume we have a model called 'Job'. In the Job model create the constants you require.

class Job extends CActiveRecord
{
const STATUS_CREATED=0;
const STATUS_PROGRESS=1;
const STATUS_COMPLETED=2;

You now need to create a function to get the constants and again create this function in your model.

public function getStatus()
{
return array (
self::STATUS_CREATED=>'Created',
self::STATUS_PROGRESS=>'In Progress',
self::STATUS_COMPLETED=>'Completed',
);
}
All that is required now is to create a drop down list in your view. To do this you can take advantage of Yii's build in dropDownList.
<div class="wide form">
.......

<div class="row">
<?php echo $form->labelEx($model,'status'); ?>
<?php echo $form->dropDownList($model, 'status', $model->getStatus()); ?>
<?php echo $form->error($model,'status'); ?>
</div>
If you open up the form you will notice that there is a drop down list populated by Created/In Progress/Completed. When you save the form the value of 0,1 or 2 is written to the database.
You can take this a step further. Continuing with the example of a model called Job you should be able to list a specific job in your view and instead of the Status displaying a value of 0, 1 or 2 you can output the corresponding text. The way to do this is to create an additional function inside the Job model.

public function getActiveStatusText ()
{
$activeStatus=$this->getActiveStatus();
return isset($activeStatus[$this->status]) ? $activeStatus[$this->status] : "unkown status({$this->status})";
}
Then to use this in your view (e.g. CDetailView or Bootstraps TbDetailView)

<?php $this->widget('bootstrap.widgets.TbDetailView', array(
'type'=>'striped bordered condensed',
'data'=>$model,
'attributes'=>array(
'job_name',
'username',
array(
'name'=>'status',
'value'=>CHtml::encode($model->getActiveStatusText()),
),
),
)); ?>

That's all there is to it. There are other methods for creating this functionality however I find this the easiest and quickest to implement.

References:

  1. Understanding Virtual Attributes and get/set methods 
  2. Managing constants easily



Thursday, November 29, 2012

Debugging variables in Yii 

It was quite a while before I started to use this feature and now I would not be without it. As your application becomes more complex it is often necessary to see that your variables and query results are what they should be. You can even see what errors may be occurring and this is invaluable.

The first thing you need to do is configure your main configuration file. You will find this under  "Protected/config/main.php". In "main.php" you need to find a parameter called 'log' and here we use a class called "CWebLogRoute". To configure CWebLogRoute add the following
'log'=>array(
'class'=>'CLogRouter',
'routes'=>array(
array(
'class'=>'CWebLogRoute',
'levels'=>'trace',
'categories'=>'vardump',
'showInFireBug'=>true,
),
),
),
As you can see the output is written to FireBug which is an essential tool not only for Yii but for general web development. This will also work with Chrome Developer tools  - to use it in Chrome simply click anywhere on the page and select Inspect Element and go to the Console.

To write the value of a variable to the Console in Yii all you need to do is add the following to your controller or view.
$test = array(1,2,3,4);
echo Yii::trace(CVarDumper::dumpAsString($test),'vardump');
I found this particularly useful when writing code to write data to the database. You can see if your code is successful by using:

echo Yii::trace(CVarDumper::dumpAsString($model->save()),'vardump');
If the data is written successfully to your database this will return 'true' in the console. If it returns 'false' and you are not sure why then output the errors.

echo Yii::trace(CVarDumper::dumpAsString($model->errors)),'vardump');
CWebLogRoute is absolutely invaluable in debugging Yii applications.

References:

  1. http://www.yiiframework.com/doc/api/1.1/CWebLogRoute
  2. How to log and debug variables using CWebLogRoute