[*]after they are initially created by the site Admin
[/list]
They shouldn’t be able to do ANYTHING on the site (except logout) until they have changed their password.
All is working well, just wondering if anyone has implemented it in a different way. Would you mind sharing?
In my parent Controller (extends from CController) I’ve overrided the beforeAction like so:
public function beforeAction($action)
{
if (!Yii::app()->user->isGuest)
{
$accessError = Yii::app()->user->validateAccess();
if ($accessError != WebUser::ERROR_NONE)
{
switch ($accessError)
{
case WebUser::ERROR_NEW_USER:
Yii::app()->user->setFlash('notice', 'You must change you password before you can proceeed.');
break;
case WebUser::ERROR_PASSWORD_EXPIRED:
Yii::app()->user->setFlash('notice', 'Your password has expired. You must change you password.');
break;
default:
break;
}
// Redirect to Change Password form.
// Do not redirect if:
// the current action is 'user/password' OR
// the current action is 'site/logout'
if (($this->id == 'user' && $action->id == 'password') ||
($this->id == 'site' && $action->id == 'logout'))
return parent::beforeAction($action);
else
$this->redirect(array('user/password'));
}
}
return parent::beforeAction($action);
}
In my WebUser (extends CWebUser) I’ve added a validateAccess method where the validation is performed.
/**
* Checks that the user is allowed to access the application and
* returns an error code indicating the result.
* @return integer the error code
*/
public function validateAccess()
{
$this->_model = $this->getModel();
$this->validateUserStatus();
if ($this->_model->status == User::STATUS_ACTIVE)
$this->validatePasswordFreshness();
return $this->accessError;
}
/**
* Checks if the user's status is New and returns an error code.
*/
private function validateUserStatus()
{
if ($this->_model->status == User::STATUS_NEW)
$this->accessError = self::ERROR_NEW_USER;
}
/**
* Checks when a user's password was last updated and returns
* an error code.
*/
private function validatePasswordFreshness()
{
$lastLoginTime = strtotime($this->_model->password_update_time);
$now = time();
$elapsedTime = abs($now - $lastLoginTime);
$elapsedDays = round($elapsedTime / 86400);
if ($elapsedDays > 30)
$this->accessError = self::ERROR_PASSWORD_EXPIRED;
}
Personally I would create a new filter as it would free up the beforeAction in parent controller and you can programmatically apply the filter in specific controller actions through each controllers’ filters method. Through filters, you can spcify their firing order too so much more powerful that beforeAction dependency.
Sure. I wrote it rather quickly; but it works well.
Controller:
/**
* Controller is the customized base controller class.
* All controller classes for this application should extend from this base class.
*/
class Controller extends CController
{
/**
* @var string the default layout for the controller view. Defaults to '//layouts/column1',
* meaning using a single column layout. See 'protected/views/layouts/column1.php'.
*/
public $layout = '//layouts/column1';
/**
* @var array context menu items. This property will be assigned to {@link CMenu::items}.
*/
public $menu = array();
/**
* @var array the breadcrumbs of the current page. The value of this property will
* be assigned to {@link CBreadcrumbs::links}. Please refer to {@link CBreadcrumbs::links}
* for more details on how to specify this property.
*/
public $breadcrumbs = array();
public function filterHttps($filterChain)
{
$filter = new HttpsFilter;
$filter->filter($filterChain);
}
/**
* Creates a list of rules to validate user's password freshness.
* @return array the array of rules to validate against
*/
public function changePasswordRules()
{
return array(
'status' => User::STATUS_ACTIVE,
'passwordExpiry' => 30,
);
}
/**
* Runs the Password filter
* @param type $filterChain
*/
public function filterChangePassword($filterChain)
{
$filter = new ChangePasswordFilter();
$filter->setRules($this->changePasswordRules());
$filter->filter($filterChain);
}
}
Filter
/**
* ChangePasswordFilterclass file.
*
* @author Matt Skelton
* @date 27-Jun-2011
*/
/**
* Determines if a user needs to change their password.
* A user must change their password if:
* User->status == active AND
* User->daysSincePasswordChange() > ChangePasswordRule->passwordExpiry
*/
class ChangePasswordFilter extends CFilter
{
private $rules = array();
/**
* Performs the pre-action filtering.
* @param CFilterChain $filterChain the filter chain that the filter is on.
* @return boolean whether the filtering process should continue and the action
* should be executed.
*/
public function preFilter($filterChain)
{
$app = Yii::app();
$user = $app->getUser();
$allowed = true;
foreach ($this->rules as $rule)
{
if ($rule->changePasswordRequired($user->getModel(), $filterChain->controller, $filterChain->action))
{
$user->setFlash('notice', 'You must change your password before you can proceed.');
$user->redirectToPasswordForm();
return false;
}
}
return true;
}
/**
* Builds the rules for the filter.
* @param array $rules the list of rules as set in the controller
*/
public function setRules($rules)
{
foreach ($rules as $name => $value)
{
$rule = new ChangePasswordRule();
switch ($name)
{
case 'status':
$rule->ruleName = $name;
$rule->$name = $value;
break;
case 'passwordExpiry':
$rule->ruleName = $name;
$rule->$name = $value;
break;
default:
break;
}
$this->rules[] = $rule;
}
}
}
Filter Rules
/**
* ChangePasswordRule class file.
*
* @author Matt Skelton
* @date 27-Jun-2011
*/
/**
* This class performs the actual validation and returns a boolean indicating if
* the user should change their password.
*/
class ChangePasswordRule extends CComponent
{
public $ruleName;
public $status;
public $passwordExpiry;
public $message;
/**
* Returns a boolean indicating if the user must change their password.
* @return boolean if the user must change their password
*/
public function changePasswordRequired($user, $controller, $action)
{
$passwordChangeRequired = false;
switch ($this->ruleName)
{
case 'status':
if ($this->checkUserStatus($user))
{
$passwordChangeRequired = true;
}
break;
case 'passwordExpiry':
if ($this->checkPasswordFreshness($user))
{
$passwordChangeRequired = true;
}
break;
default:
break;
}
return $passwordChangeRequired;
}
/**
* Checks if the User's status is new.
* @param User $user the user to check
* @return boolean a boolean indicating if the User's status is valid
*/
private function checkUserStatus(User $user)
{
$passwordChangeRequired = false;
if ($user->status == User::STATUS_NEW)
$passwordChangeRequired = true;
return $passwordChangeRequired;
}
/**
* Checks if the user's password has been changed since $this->passwordExpiry.
* @param User $user the user to check
* @return boolean a boolean indicating if the User's password is valid
*/
private function checkPasswordFreshness(User $user)
{
$passwordChangeRequired = false;
if ($user->daysSincePasswordChange() > $this->passwordExpiry)
$passwordChangeRequired = true;
return $passwordChangeRequired;
}
}
Regular controllers manage the filter by:
class UserController extends Controller
{
public $layout = '//layouts/column1';
public function init()
{
$this->layout = (Yii::app()->user->isAdmin) ? '//layouts/column2' : '//layouts/column1';
parent::init();
}
/**
* @return array action filters
*/
public function filters()
{
return array(
'accessControl', // perform access control for CRUD operations
'changePassword - password',
'https',
);
}
...
Looks like a good piece of good article! Many thanks, personally and for the community, and double plus (here and in the article) for a really good work! :]
I’ve been trying to use your code (the one in mentioned wiki article), but run into two problems:
1. Expressed as comment in article – we need implementation of WebUser->getModel() function. [Added to article]!
2. You are using this function, to get currently logged-in user date of last time password has been changed, which is obvious. But one of my controller’s actions (index) is executed for both logged-in users and guest. How can I secure your filter applied to it, so it would be executed only for non-guest users? Currently I’m getting exception after logout, because logout action (removed from filter’s allowed actions list, as used by logged-in users only) redirects to index action (which is not removed from filter’s allowed actions list as is being used by both kind of users). {Fixed myself!]
hey . i need to implement change password on first login and your example seems perfect… i am really new to yii so please help me implement it …
so i create a new controller named it controller.php , 2 models one filter.php and another filterrules.php and i copied the last code in my user controller …
is this right ??
i get the error that filter is not defined … what other changes do i have to make … to make it work … please help me with specific instructions …
I’m trying to implement this code, but i have some problem understanding filters.
I can handle with using filters inside UserController: the user logged-in can’t do anything before changing his expired password. But why in wiki article filters are declared in SiteController? It doesn’t work for me. I’m not really understandig this process.
I think i’m doing something wrong, but i need to understand