Difference between #7 and #8 of
Force a User to Change Their Password (ChangePasswordFilter)

Revision #8 has been created by waterloomatt on Nov 1, 2011, 2:12:41 AM with the memo:

Added WebUser class, added Time helper class, refactored ChangePasswordRule->changePasswordRequired to use the Time helper class
« previous (#7)

Changes

Title unchanged

Force a User to Change Their Password (ChangePasswordFilter)

Category unchanged

Tutorials

Yii version unchanged

Tags unchanged

security, filters, password, user management

Content changed

[...]
{
public $passwordExpiry;

/**
* Checks if a user is required to change their password.
*
@param type $user the user whose password needs to be validated
 
     * @return boolean if the user must change their password
 
     */
 
    public function changePasswordRequired($user)
 
    {
 
        $passwordChangeRequired = false;
 
 
        if ($user->daysSincePasswordChange() > $this->passwordExpiry)
 
            $passwordChangeRequired = true;
 
 
        return $passwordChangeRequired;
 
    }
 
 
}
 
?>
 
```
 
 
 
Controller Class (Parent Controller - client controllers should extend this one). These two methods instantiate the filter and link it to all controllers that inherit from this one.
 
 
```php 
/**
 
     * Creates a rule to validate user's password freshness.
 
     * @return array the array of rules to validate against
 
     */
 
    public function changePasswordRules()
 
    {
 
        return array(
 
            'days' => 30,
 
        );
 
    }
 
 
    /**
 
     * Runs the Password filter
 
     * @param type $filterChain 
 
     */
 
    public function filterChangePassword($filterChain)
 
    {
 
        $filter = new ChangePasswordFilter();
 
        $filter->setRules($this->changePasswordRules());
 
        $filter->filter($filterChain);
 
    }
 
```
 
 
 
Client Controller Class. This is an example of my SiteController. The changePasswordFilter applies to all actions except for the ones listed after "-". 
 
 
```php 
/**
 
     * @return array action filters
 
     */
 
    public function filters()
 
    {
 
        return array(
 
            'accessControl', // perform access control for CRUD operations
 
            'changePassword - logout, login, autoLogin, password, securityCode, passwordVerify, verify, autoGeneratePassword',
 
            'https',
 
        );
 
    }
 
```
 
 
 
These are two convenience methods I use in my User model. They're used to calculate the days since the user has changed his/her password.
 
 
```php 
/**
 
     * Returns the number of days since the user changed their password.
 
     * @return integer the number of elapsed days
 
     */
 
    public function daysSincePasswordChange()
 
    {
 
        return $this->calculateInterval($this->password_update_time);
 
    }
 
 
    /**
 
     * Returns the number of days between two time intervals. If $end is null, 'now' will be used.
 
     * @param string $start the start time. Should be an English textual description. Ex: 2011-06-28 08:06:53
 
     * @param string $end the end time. Should be an English textual description. Ex: 2011-06-28 08:06:53
 
     * @return integer the number of days
 
     */
 
    public function calculateInterval($start, $end = null)
 
    {
 
        $startTime = strtotime($start);
 
        $endTime = ($end == null) ? time() : $end;
 
 
        $elapsedTime = abs($endTime - $startTime);
 
 
        return round($elapsedTime / 86400);
 
    }
 
```
 
 
 
The redirectToPasswordForm method. I include this in WebUser. It is copied from CWebUser->loginRequired() but redirects to the password form, instead of the login form.
 
 
```php 
public function redirectToPasswordForm()
 
    {
 
        if (!Yii::app()->request->getIsAjaxRequest())
 
            $this->setReturnUrl(Yii::app()->request->getUrl());
 
 
        if (($url = $this->changePasswordUrl) !== null)
 
        {
 
            if (is_array($url))
 
            {
 
                $route = isset($url[0]) ? $url[0] : $app->defaultController;
 
                $url = Yii::app()->createUrl($route, array_splice($url, 1));
 
            }
 
            Yii::app()->request->redirect($url);
 
        }
 
        else
 
            throw new CHttpException(403, Yii::t('yii', 'Change Password Required'));
 
    }
 
```

 
     * @param $user the user whose password needs to be validated
 
     * @return boolean if the user must change their password
 
     */
 
    public function changePasswordRequired(User $user)
 
    {
 
        return Time::wasWithinLast($this->passwordExpiry, $user->password_update_time) ? false : true;
 
    }
 
 
}
 
?>
 
```
 
 
 
Controller Class (Parent Controller - client controllers should extend this one). These two methods instantiate the filter and link it to all controllers that inherit from this one.
 
 
```php 
/**
 
     * Creates a rule to validate user's password freshness.
 
     * @return array the array of rules to validate against
 
     */
 
    public function changePasswordRules()
 
    {
 
        return array(
 
            'days' => 30,
 
        );
 
    }
 
 
    /**
 
     * Runs the Password filter
 
     * @param type $filterChain 
 
     */
 
    public function filterChangePassword($filterChain)
 
    {
 
        $filter = new ChangePasswordFilter();
 
        $filter->setRules($this->changePasswordRules());
 
        $filter->filter($filterChain);
 
    }
 
```
 
 
 
Client Controller Class. This is an example of my SiteController. The changePasswordFilter applies to all actions except for the ones listed after "-". 
 
 
```php 
/**
 
     * @return array action filters
 
     */
 
    public function filters()
 
    {
 
        return array(
 
            'accessControl', // perform access control for CRUD operations
 
            'changePassword - logout, login, autoLogin, password, securityCode, passwordVerify, verify, autoGeneratePassword',
 
            'https',
 
        );
 
    }
 
```
 
 
The redirectToPasswordForm method. I include this in WebUser. It is copied from CWebUser->loginRequired() but redirects to the password form, instead of the login form.
 
 
```php 
<?php
 
 
/**
 
 * WebUser class file.
 
 *
 
 * @author Matt Skelton
 
 * @date 8-Jun-2011
 
 */
 
 
/**
 
 * Provides additional properties and functionality to CWebUser.
 
 */
 
class WebUser extends CWebUser
 
{
 
    // User access error codes
 
    const ERROR_NONE = 1;
 
    const ERROR_NEW_USER = 2;
 
    const ERROR_PASSWORD_EXPIRED = 3;
 
 
    private $accessError = self::ERROR_NONE;
 
    /**
 
     * Holds a reference to the currently logged in user model.
 
     * @var User The currently logged in User Model.
 
     */
 
    private $_model;
 
    public $changePasswordUrl = array('/user/password');
 
 
    public function init()
 
    {
 
        parent::init();
 
    }
 
 
    /**
 
     * Returns the User model of the currently logged in user and null if
 
     * is user is not logged in.
 
     * 
 
     * @return User The model of the logged in user.
 
     */
 
    public function getModel()
 
    {
 
        return $this->loadUser(Yii::app()->user->id);
 
    }
 
 
    /**
 
     * Returns a boolean indicating if the currently logged in user is an Admin user.
 
     * @return boolean whether the current application user is an admin.
 
     */
 
    public function getIsAdmin()
 
    {
 
        $isAdmin = false;
 
        
 
        if (strtolower($this->loadUser(Yii::app()->user->id)->role->name) == 'admin' ||
 
            strtolower($this->loadUser(Yii::app()->user->id)->role->name) == 'super admin')
 
        {
 
            $isAdmin = true;
 
        }
 
 
        return $isAdmin;
 
    }
 
 
    /**
 
     * Retrieves a User model from the database
 
     * @param integer $id the id of the User to be retrieved
 
     * @return User the user model
 
     */
 
    protected function loadUser($id=null)
 
    {
 
        if ($this->_model === null)
 
        {
 
            if ($id !== null)
 
                $this->_model = User::model()->findByPk($id);
 
        }
 
 
        return $this->_model;
 
    }
 
 
    public function redirectToPasswordForm()
 
    {
 
        if (!Yii::app()->request->getIsAjaxRequest())
 
            $this->setReturnUrl(Yii::app()->request->getUrl());
 
 
        if (($url = $this->changePasswordUrl) !== null)
 
        {
 
            if (is_array($url))
 
            {
 
                $route = isset($url[0]) ? $url[0] : $app->defaultController;
 
                $url = Yii::app()->createUrl($route, array_splice($url, 1));
 
            }
 
            Yii::app()->request->redirect($url);
 
        }
 
        else
 
            throw new CHttpException(403, Yii::t('yii', 'Change Password Required'));
 
    }
 
}
 
?>
 
 
```
 
 
 
Time helper class used to calculate if the password has been updated within the specified time.
 
 
```php 
<?php
 
 
/**
 
 * Time class file.
 
 *
 
 * @author Matt Skelton
 
 * @date 29-Sep-2011
 
 *
 
 * Provides convenience methods for date and time functionality.
 
 */
 
class Time
 
{
 
 
    /**
 
     * Returns a nicely formatted date string for given Datetime string.
 
     *
 
     * @param string $dateString Datetime string
 
     * @param int $format Format of returned date
 
     * @return string Formatted date string
 
     */
 
    public static function nice($dateString = null, $format = 'D, M jS Y, H:i')
 
    {
 
        $date = ($dateString == null) ? time() : strtotime($dateString);
 
        return date($format, $date);
 
    }
 
 
    /**
 
     * Returns a formatted descriptive date string for given datetime string.
 
     *
 
     * If the given date is today, the returned string could be "Today, 6:54 pm".
 
     * If the given date was yesterday, the returned string could be "Yesterday, 6:54 pm".
 
     * If $dateString's year is the current year, the returned string does not
 
     * include mention of the year.
 
     *
 
     * @param string $dateString Datetime string or Unix timestamp
 
     * @return string Described, relative date string
 
     */
 
    public static function niceShort($dateString = null)
 
    {
 
        $date = ($dateString == null) ? time() : strtotime($dateString);
 
 
        $y = (self::isThisYear($date)) ? '' : ' Y';
 
 
        if (self::isToday($date))
 
        {
 
            $ret = sprintf('Today, %s', date("g:i a", $date));
 
        }
 
        elseif (self::wasYesterday($date))
 
        {
 
            $ret = sprintf('Yesterday, %s', date("g:i a", $date));
 
        }
 
        else
 
        {
 
            $ret = date("M jS{$y}, H:i", $date);
 
        }
 
 
        return $ret;
 
    }
 
 
    /**
 
     * Returns true if given date is today.
 
     *
 
     * @param string $date Unix timestamp
 
     * @return boolean True if date is today
 
     */
 
    public static function isToday($date)
 
    {
 
        return date('Y-m-d', $date) == date('Y-m-d', time());
 
    }
 
 
    /**
 
     * Returns true if given date was yesterday
 
     *
 
     * @param string $date Unix timestamp
 
     * @return boolean True if date was yesterday
 
     */
 
    public static function wasYesterday($date)
 
    {
 
        return date('Y-m-d', $date) == date('Y-m-d', strtotime('yesterday'));
 
    }
 
 
    /**
 
     * Returns true if given date is in this year
 
     *
 
     * @param string $date Unix timestamp
 
     * @return boolean True if date is in this year
 
     */
 
    public static function isThisYear($date)
 
    {
 
        return date('Y', $date) == date('Y', time());
 
    }
 
 
    /**
 
     * Returns true if given date is in this week
 
     *
 
     * @param string $date Unix timestamp
 
     * @return boolean True if date is in this week
 
     */
 
    public static function isThisWeek($date)
 
    {
 
        return date('W Y', $date) == date('W Y', time());
 
    }
 
 
    /**
 
     * Returns true if given date is in this month
 
     *
 
     * @param string $date Unix timestamp
 
     * @return boolean True if date is in this month
 
     */
 
    public static function isThisMonth($date)
 
    {
 
        return date('m Y', $date) == date('m Y', time());
 
    }
 
 
    /**
 
     * Returns either a relative date or a formatted date depending
 
     * on the difference between the current time and given datetime.
 
     * $datetime should be in a <i>strtotime</i>-parsable format, like MySQL's datetime datatype.
 
     *
 
     * Options:
 
     *  'format' => a fall back format if the relative time is longer than the duration specified by end
 
     *  'end' =>  The end of relative time telling
 
     *
 
     * Relative dates look something like this:
 
     *  3 weeks, 4 days ago
 
     *  15 seconds ago
 
     * Formatted dates look like this:
 
     *  on 02/18/2004
 
     *
 
     * The returned string includes 'ago' or 'on' and assumes you'll properly add a word
 
     * like 'Posted ' before the function output.
 
     *
 
     * @param string $dateString Datetime string
 
     * @param array $options Default format if timestamp is used in $dateString
 
     * @return string Relative time string.
 
     */
 
    public static function timeAgoInWords($dateTime, $options = array())
 
    {
 
        $now = time();
 
 
        $inSeconds = strtotime($dateTime);
 
        $backwards = ($inSeconds > $now);
 
 
        $format = 'j/n/y';
 
        $end = '+1 month';
 
 
        if (is_array($options))
 
        {
 
            if (isset($options['format']))
 
            {
 
                $format = $options['format'];
 
                unset($options['format']);
 
            }
 
            if (isset($options['end']))
 
            {
 
                $end = $options['end'];
 
                unset($options['end']);
 
            }
 
        }
 
        else
 
        {
 
            $format = $options;
 
        }
 
 
        if ($backwards)
 
        {
 
            $futureTime = $inSeconds;
 
            $pastTime = $now;
 
        }
 
        else
 
        {
 
            $futureTime = $now;
 
            $pastTime = $inSeconds;
 
        }
 
        $diff = $futureTime - $pastTime;
 
 
        // If more than a week, then take into account the length of months
 
        if ($diff >= 604800)
 
        {
 
            $current = array();
 
            $date = array();
 
 
            list($future['H'], $future['i'], $future['s'], $future['d'], $future['m'], $future['Y']) = explode('/', date('H/i/s/d/m/Y', $futureTime));
 
 
            list($past['H'], $past['i'], $past['s'], $past['d'], $past['m'], $past['Y']) = explode('/', date('H/i/s/d/m/Y', $pastTime));
 
            $years = $months = $weeks = $days = $hours = $minutes = $seconds = 0;
 
 
            if ($future['Y'] == $past['Y'] && $future['m'] == $past['m'])
 
            {
 
                $months = 0;
 
                $years = 0;
 
            }
 
            else
 
            {
 
                if ($future['Y'] == $past['Y'])
 
                {
 
                    $months = $future['m'] - $past['m'];
 
                }
 
                else
 
                {
 
                    $years = $future['Y'] - $past['Y'];
 
                    $months = $future['m'] + ((12 * $years) - $past['m']);
 
 
                    if ($months >= 12)
 
                    {
 
                        $years = floor($months / 12);
 
                        $months = $months - ($years * 12);
 
                    }
 
 
                    if ($future['m'] < $past['m'] && $future['Y'] - $past['Y'] == 1)
 
                    {
 
                        $years--;
 
                    }
 
                }
 
            }
 
 
            if ($future['d'] >= $past['d'])
 
            {
 
                $days = $future['d'] - $past['d'];
 
            }
 
            else
 
            {
 
                $daysInPastMonth = date('t', $pastTime);
 
                $daysInFutureMonth = date('t', mktime(0, 0, 0, $future['m'] - 1, 1, $future['Y']));
 
 
                if (!$backwards)
 
                {
 
                    $days = ($daysInPastMonth - $past['d']) + $future['d'];
 
                }
 
                else
 
                {
 
                    $days = ($daysInFutureMonth - $past['d']) + $future['d'];
 
                }
 
 
                if ($future['m'] != $past['m'])
 
                {
 
                    $months--;
 
                }
 
            }
 
 
            if ($months == 0 && $years >= 1 && $diff < ($years * 31536000))
 
            {
 
                $months = 11;
 
                $years--;
 
            }
 
 
            if ($months >= 12)
 
            {
 
                $years = $years + 1;
 
                $months = $months - 12;
 
            }
 
 
            if ($days >= 7)
 
            {
 
                $weeks = floor($days / 7);
 
                $days = $days - ($weeks * 7);
 
            }
 
        }
 
        else
 
        {
 
            $years = $months = $weeks = 0;
 
            $days = floor($diff / 86400);
 
 
            $diff = $diff - ($days * 86400);
 
 
            $hours = floor($diff / 3600);
 
            $diff = $diff - ($hours * 3600);
 
 
            $minutes = floor($diff / 60);
 
            $diff = $diff - ($minutes * 60);
 
            $seconds = $diff;
 
        }
 
        $relativeDate = '';
 
        $diff = $futureTime - $pastTime;
 
 
        if ($diff > abs($now - strtotime($end)))
 
        {
 
            $relativeDate = sprintf('on %s', date($format, $inSeconds));
 
        }
 
        else
 
        {
 
            if ($years > 0)
 
            {
 
                // years and months and days
 
                $relativeDate .= ($relativeDate ? ', ' : '') . $years . ' ' . ($years == 1 ? 'year' : 'years');
 
                $relativeDate .= $months > 0 ? ($relativeDate ? ', ' : '') . $months . ' ' . ($months == 1 ? 'month' : 'months') : '';
 
                $relativeDate .= $weeks > 0 ? ($relativeDate ? ', ' : '') . $weeks . ' ' . ($weeks == 1 ? 'week' : 'weeks') : '';
 
                $relativeDate .= $days > 0 ? ($relativeDate ? ', ' : '') . $days . ' ' . ($days == 1 ? 'day' : 'days') : '';
 
            }
 
            elseif (abs($months) > 0)
 
            {
 
                // months, weeks and days
 
                $relativeDate .= ($relativeDate ? ', ' : '') . $months . ' ' . ($months == 1 ? 'month' : 'months');
 
                $relativeDate .= $weeks > 0 ? ($relativeDate ? ', ' : '') . $weeks . ' ' . ($weeks == 1 ? 'week' : 'weeks') : '';
 
                $relativeDate .= $days > 0 ? ($relativeDate ? ', ' : '') . $days . ' ' . ($days == 1 ? 'day' : 'days') : '';
 
            }
 
            elseif (abs($weeks) > 0)
 
            {
 
                // weeks and days
 
                $relativeDate .= ($relativeDate ? ', ' : '') . $weeks . ' ' . ($weeks == 1 ? 'week' : 'weeks');
 
                $relativeDate .= $days > 0 ? ($relativeDate ? ', ' : '') . $days . ' ' . ($days == 1 ? 'day' : 'days') : '';
 
            }
 
            elseif (abs($days) > 0)
 
            {
 
                // days and hours
 
                $relativeDate .= ($relativeDate ? ', ' : '') . $days . ' ' . ($days == 1 ? 'day' : 'days');
 
                $relativeDate .= $hours > 0 ? ($relativeDate ? ', ' : '') . $hours . ' ' . ($hours == 1 ? 'hour' : 'hours') : '';
 
            }
 
            elseif (abs($hours) > 0)
 
            {
 
                // hours and minutes
 
                $relativeDate .= ($relativeDate ? ', ' : '') . $hours . ' ' . ($hours == 1 ? 'hour' : 'hours');
 
                $relativeDate .= $minutes > 0 ? ($relativeDate ? ', ' : '') . $minutes . ' ' . ($minutes == 1 ? 'minute' : 'minutes') : '';
 
            }
 
            elseif (abs($minutes) > 0)
 
            {
 
                // minutes only
 
                $relativeDate .= ($relativeDate ? ', ' : '') . $minutes . ' ' . ($minutes == 1 ? 'minute' : 'minutes');
 
            }
 
            else
 
            {
 
                // seconds only
 
                $relativeDate .= ($relativeDate ? ', ' : '') . $seconds . ' ' . ($seconds == 1 ? 'second' : 'seconds');
 
            }
 
 
            if (!$backwards)
 
            {
 
                $relativeDate = sprintf('%s ago', $relativeDate);
 
            }
 
        }
 
        return $relativeDate;
 
    }
 
 
    /**
 
     * Returns true if specified datetime was within the interval specified, else false.
 
     *
 
     * @param mixed $timeInterval the numeric value with space then time type. 
 
     *    Example of valid types: 6 hours, 2 days, 1 minute.
 
     * @param mixed $dateString the datestring or unix timestamp to compare
 
     * @param int $userOffset User's offset from GMT (in hours)
 
     * @return bool whether the $dateString was withing the specified $timeInterval
 
     */
 
    public static function wasWithinLast($timeInterval, $dateString, $userOffset = null)
 
    {
 
        $tmp = str_replace(' ', '', $timeInterval);
 
 
        if (is_numeric($tmp))
 
        {
 
            $timeInterval = $tmp . ' ' . __('days', true);
 
        }
 
 
        $date = self::fromString($dateString, $userOffset);
 
 
        $interval = self::fromString('-' . $timeInterval);
 
 
        if ($date >= $interval && $date <= time())
 
        {
 
            return true;
 
        }
 
 
        return false;
 
    }
 
    
 
    /**
 
     * Returns true if the specified date was in the past, else false.
 
     * 
 
     * @param mixed $date the datestring (a valid strtotime) or unix timestamp to check
 
     * @return boolean if the specified date was in the past
 
     */
 
    public static function wasInThePast($date)
 
    {
 
        return self::fromString($date) < time() ? true : false;        
 
    }
 
 
    /**
 
     * Returns a UNIX timestamp, given either a UNIX timestamp or a valid strtotime() date string.
 
     *
 
     * @param string $dateString Datetime string
 
     * @param int $userOffset User's offset from GMT (in hours)
 
     * @return string Parsed timestamp
 
     */
 
    public static function fromString($dateString, $userOffset = null)
 
    {
 
        if (empty($dateString))
 
        {
 
            return false;
 
        }
 
 
        if (is_integer($dateString) || is_numeric($dateString))
 
        {
 
            $date = intval($dateString);
 
        }
 
        else
 
        {
 
            $date = strtotime($dateString);
 
        }
 
 
        if ($userOffset !== null)
 
        {
 
            return $this->convert($date, $userOffset);
 
        }
 
 
        if ($date === -1)
 
        {
 
            return false;
 
        }
 
 
        return $date;
 
    }
 
 
}
 
?>
 
```
 


The rule setup in Controller.php, _'days' => 30,_, is obviously very simple. It can be easily extended. For example, users of different roles might have different freshness requirements - Admin users - 10 days, regular users - 30 days, super admins - never etc. You'll just need to modify _ChangePasswordFilter->setRules()_ and _ChangePasswordRule->changePasswordRequired()_ to loop over the list of rules and perform your needed logic.

Hope this helps - feedback welcome.
[...]
14 0
10 followers
Viewed: 29 521 times
Version: 1.1
Category: Tutorials
Written by: waterloomatt
Last updated by: waterloomatt
Created on: Sep 16, 2011
Last updated: 12 years ago
Update Article

Revisions

View all history