<?php
/**
 * This file contains the class Password.
 * For more information on this file and how to use the class please visit
 * http://www.hashbangcode.com/blog/password-validation-class-in-php-2015.html
 *
 * PHP Version 5.0.0
 *
 * @category Password
 * @package  Password
 * @author   Philip Norton <philipnorton42@gmail.com>
 * @license  http://www.gnu.org/copyleft/gpl.html GNU General Public License
 * @version  SVN:
 * @link     http://www.hashbangcode.com/
 */

/**
 * This class can be used to validate a password or password like string to
 * certain standards. These can be things like having more than one letter,
 * or having at least a single number.
 * The class can also be used to "score" a password to certain parameters.
 *
 * @category Password
 * @package  Password
 * @author   Philip Norton <philipnorton42@gmail.com>
 * @license  http://www.gnu.org/copyleft/gpl.html GNU General Public License
 * @version  Release: 1.0 04/09/2009
 * @link     http://www.hashbangcode.com/examples/password/Password.phps
 *
 */
class Password
{
    
/**
     *
     * @var array Holds an array of any errors encountered whilst validating
     *            the password.
     */
    
protected $errors = array();

    
/**
     *
     * @var integer The minimum number of characters that the password must be.
     */
    
protected $minLength 7;

    
/**
     *
     * @var integer The maximum number of characters that the password must be.
     */
    
protected $maxLength 15;

    
/**
     *
     * @var integer The minimum number of numbers that the password should contain.
     */
    
protected $minNumbers 1;

    
/**
     *
     * @var integer The minimum number of letters that the password should contain.
     */
    
protected $minLetters 5;

    
/**
     *
     * @var integer The minimum number of lower case letters that the password
     *              should contain.
     */
    
protected $minLowerCase 1;

    
/**
     *
     * @var integer The minimum number of upper case letters that the password
     *              should contain.
     */
    
protected $minUpperCase 1;

    
/**
     *
     * @var integer The minimum number of symbols that the password should contain.
     */
    
protected $minSymbols 1;

    
/**
     *
     * @var integer The maximum number of symbols that the password should contain.
     */
    
protected $maxSymbols 3;

    
/**
     *
     * @var array The symbols that are allowed to be in the password.
     */
    
protected $allowedSymbols = array('#''_''!');

    
/**
     *
     * @var integer The score of the password.
     */
    
protected $score 100;


    
/**
     * Constructor
     *
     * @param array $options An associative array of options.
     */
    
public function Password($options = array())
    {
        
// Pass options setting onto the setOptions() function.
        
$this->setOptions($options);
    }

    
/**
     * Validate the password to the defined parameters. If a parameters is not
     * set at runtime then a default value is used.
     *
     * @param string $password The password.
     *
     * @return boolean True if password valid, otherwise false.
     */
    
public function validatePassword($password)
    {
        
// Make sure that parameters don't overlap in such a way as to make
        // validation impossible.
        
$this->_sanitizeInputs();
        
        
$this->errors = array();

        
// Check password minimum length, return at this step.
        
if (strlen($password) < $this->minLength) {
            
$this->errors[] = 'Password must be ' $this->minLength ' characters long, current password is too short at ' strlen($password) . ' characters.';
            return 
false;
        }
        
// Check password maximum length, return at this step.
        
if (strlen($password) > $this->maxLength) {
            
$this->errors[] = 'Password must be ' $this->minLength ' characters long, current password is too long at ' strlen($password) . ' characters.';
            return 
false;
        }
        
// Check the number of numbers in the password.
        
if (strlen(preg_replace('/([^0-9]*)/'''$password)) < $this->minNumbers) {
            
$this->errors[] = 'Not enough numbers in password, a minimum of ' $this->minNumbers ' required.';
        }
        
// Check the number of letters in the password
        
if (strlen(preg_replace('/([^a-zA-Z]*)/'''$password)) < $this->minLetters) {
            
$this->errors[] = 'Not enough letters in password, a minimum of ' $this->minLetters ' required.';
        }
        
// Check the number of lower case letters in the password
        
if (strlen(preg_replace('/([^a-z]*)/'''$password)) < $this->minLowerCase && $this->minLowerCase != 0) {
            
$this->errors[] = 'Not enough lower case letters in password, a minimum of ' $this->minLowerCase ' required.';
        }
        
// Check the number of upper case letters in the password
        
if (strlen(preg_replace('/([^A-Z]*)/'''$password)) < $this->minUpperCase && $this->minUpperCase != 0) {
            
$this->errors[] = 'Not enough upper case letters in password, a minimum of ' $this->minUpperCase ' required.';
        }
        
// Check the minimum number of symbols in the password.
        
if (strlen(preg_replace('/([a-zA-Z0-9]*)/'''$password)) < $this->minSymbols && $this->maxSymbols != 0) {
            
$this->errors[] = 'Not enough symbols in password, a minimum of ' $this->minSymbols ' required.';
        }
        
// Check the maximum number of symbols in the password.
        
if (strlen(preg_replace('/([a-zA-Z0-9]*)/'''$password)) > $this->maxSymbols) {
            if (
$this->maxSymbols == 0) {
                
$this->errors[] = 'You aren not allowed any symbols in password, please remove them.';
            } else {
                
$this->errors[] = 'Too many symbols in password.';
            }
        }

        
// Check that the symbols present in the password are allowed.
        
if ($this->maxSymbols != 0) {
            
$symbols preg_replace('/([a-zA-Z0-9]*)/'''$password);
            for (
$i 0;$i strlen($symbols);++$i) {
                if (!
in_array($symbols[$i], $this->allowedSymbols)) {
                    
$this->errors[] = 'Non specified symbol ' $symbols[$i] . ' used in password, please use one of ' implode(''$this->allowedSymbols) . '.';
                }
            }
        }

        
// If any errors have been encountered then return false.
        
if (count($this->errors) > 0) {
            return 
false;
        }
        return 
true;
    }

    
/**
     * Score the password based on the level of security. This function doesn't
     * look at the parameters set up and simply scores based on best practices.
     * The function first makes sure the password is valid as there is no
     * point in scoring a password that can't be used.
     *
     * @param string $password The password to score.
     * 
     * @return mixed Returns an integer score of the password strength.
     */
    
public function scorePassword($password)
    {
        
// Make sure password is valid.
        
if (!$this->validatePassword($password)) {
            return 
false;
        }

        
// Reset initial score.
        
$this->score 100;

        
$passwordLetters preg_replace('/([^a-zA-Z]*)/'''$password);
        
$letters = array();
        for (
$i 0$i strlen($passwordLetters); ++$i) {
            
// Reduce score for duplicate letters.
            
if (in_array($passwordLetters[$i], $letters)) {
                
$this->score $this->score 5;
            }
            
// Reduce score for duplicate letters next to each other.
            
if (isset($passwordLetters[$i-1]) && $passwordLetters[$i] == $passwordLetters[$i-1]) {
                
$this->score $this->score 10;
            }
            
$letters[] = $passwordLetters[$i];
        }

        
// Reduce score for duplicate numbers.
        
$passwordNumbers preg_replace('/([^0-9]*)/'''$password);
        
$numbers = array();
        for (
$i 0$i strlen($passwordNumbers); ++$i) {
            if (
in_array($passwordNumbers[$i], $numbers)) {
                
$this->score $this->score 5;
            }
            
$numbers[] = $passwordNumbers[$i];
        }

        
// Reduce score for no symbols.
        
if (strlen(preg_replace('/([a-zA-Z0-9]*)/'''$password)) == 0) {
            
$this->score $this->score 10;
        }

        
// Reduce score for words in dictionary used in password.
        
$dictionary 'words.txt';
        if (
file_exists($dictionary)) {
            
$handle fopen($dictionary"r");

            
$words '';

            while (!
feof($handle)) {
                
$words .= fread($handle8192);
            }

            
fclose($handle);

            
$words explode("\n"$words);
            foreach (
$words as $word) {
                if (
preg_match('/.*?' trim($word) . '.*?/i'$password$match)) {
                    
$this->score $this->score 20;
                }
            }
        }
        if (
$this->score 0) {
            
$this->score 0;
        }
        
// Return the score.
        
return $this->score;
    }

    
/**
     * Use the options set up in the class to create a random password that passes
     * validation. This uses certain practices such as not using the letter o or
     * the number 0 as these can be mixed up.
     *
     * @return string The generated password.
     */
    
public function generatePassword()
    {
        
// Make sure that parameters don't overlap in such a way as to make
        // validation impossible.
        
$this->_sanitizeInputs();    
        
        
// Initialise variable.
        
$password '';

        
// Add lower case letters.
        
$lowerLetters 'aeiubdghjmnpqrstvxyz';
        if (
$this->minLowerCase != 0) {
            for (
$i 0$i <= $this->minLowerCase; ++$i) {
                
$password .= $lowerLetters[(rand() % strlen($lowerLetters))];
            }
        }

        
// Add upper case letters.
        
$upperLetters 'AEUBDGHJLMNPQRSTVWXYZ';
        if (
$this->minUpperCase != 0) {
            for (
$i 0$i <= $this->minUpperCase; ++$i) {
                
$password .= $upperLetters[(rand() % strlen($upperLetters))];
            }
        }

        
// Add numbers.
        
$numbers  '23456789';
        if (
$this->minNumbers != 0) {
            for (
$i 0$i <= $this->minNumbers; ++$i) {
                
$password .= $numbers[(rand() % strlen($numbers))];
            }
        }

        
// Add symbols using the symbols array.
        
if ($this->maxSymbols != 0) {
            
$symbols implode(''$this->allowedSymbols);
            if (
$this->minSymbols != && strlen($symbols) > 0) {
                for (
$i 0$i <= $this->minSymbols; ++$i) {
                    
$password .= $symbols[(rand() % strlen($symbols))];
                }
            }
        }

        
// If the created password isn't quite long enough then add some lowercase
        
        // letters to the password string.
        
if (strlen($password) < $this->minLength) {
            while (
strlen($password) - $this->minLength) {
                
$password .= $lowerLetters[(rand() % strlen($lowerLetters))];
            }
        }

        
// Shuffle the letters in the password.
        
$password str_shuffle($password);

        
// Return the password string.
        
return $password;
    }    
    
    
/**
     * Set multiple options for the object in one go.
     *
     * @param array $options An associative array of options.
     *
     * @return null     
     */
    
public function setOptions($options)
    {
        if (isset(
$options['maxLength'])) {
            
$this->maxLength $options['maxLength'];
        }
        if (isset(
$options['minLength'])) {
            
$this->minLength $options['minLength'];
        }
        if (isset(
$options['minNumbers'])) {
            
$this->minNumbers $options['minNumbers'];
        }
        if (isset(
$options['minLetters'])) {
            
$this->minLetters $options['minLetters'];
        }
        if (isset(
$options['minSymbols'])) {
            
$this->minSymbols $options['minSymbols'];
        }
        if (isset(
$options['maxSymbols'])) {
            
$this->maxSymbols $options['maxSymbols'];
        }
        if (isset(
$options['allowedSymbols'])) {
            if (
is_array($options['allowedSymbols'])) {
                
$this->allowedSymbols $options['allowedSymbols'];
            }
        }
        if (isset(
$options['minLowerCase'])) {
            
$this->minLowerCase $options['minLowerCase'];
        }
        if (isset(
$options['minUpperCase'])) {
            
$this->minUpperCase $options['minUpperCase'];
        }
        
// Make sure that parameters don't overlap in such a way as to make
        // validation impossible.
        
$this->_sanitizeInputs();
    }
    
    
/**
     * Get any errors produced through the last validation.
     *
     * @return array
     */
    
public function getErrors()
    {
        return 
$this->errors;
    }

    
/**
     * Get the maximum length of password allowed.
     *
     * @param integer $maxLength The maximum length of password allowed.
     *
     * @return null
     */
    
public function setMaxLength($maxLength)
    {
        
$this->maxLength $maxLength;
    }

    
/**
     * The maximum character length of the password.
     *
     * @return integer The maximum character length of the password.
     */
    
public function getMaxLength()
    {
        return 
$this->maxLength;
    }

    
/**
     * Get the minimum length of password allowed.
     *
     * @param integer $minLength The minimum length of password allowed.
     *
     * @return null
     */
    
public function setMinLength($minLength)
    {
        
$this->minLength $minLength;
    }

    
/**
     * The minimum character length of the password.
     *
     * @return integer The minimum character length of the password.
     */
    
public function getMinLength()
    {
        return 
$this->minLength;
    }

    
/**
     * Get the symbols allowed in password.
     *
     * @return array The allowed symbols array.
     */
    
public function getAllowedSymbols()
    {
        return 
$this->allowedSymbols;
    }

    
/**
     * An array of symbols that can be included in the password. If an array is
     * not passed to this function then it is not stored.
     *
     * @param array $symbols An array of symbols that can be included in the
     *                       password.
     *
     * @return null     
     */
    
public function setAllowedSymbols($symbols)
    {
        if (
is_array($symbols)) {
            
$this->allowedSymbols $symbols;
        }
    }

    
/**
     * Set the minimum number of symbols required in the password.
     *
     * @param integer $minSymbols The minimum number of symbols.
     *
     * @return null     
     */
    
public function setMinSymbols($minSymbols)
    {
        
$this->minSymbols $minSymbols;
    }

    
/**
     * Get the minimum number of symbols required in the password.
     *
     * @return integer The minimum number of symbols.
     */
    
public function getMinSymbols()
    {
        return 
$this->minSymbols;
    }

    
/**
     * Set the maximum number of symbols required in the password.
     *
     * @param integer $maxSymbols The maximum number of symbols.
     *
     * @return null     
     */
    
public function setMaxSymbols($maxSymbols)
    {
        
$this->maxSymbols $maxSymbols;
    }

    
/**
     * The maximum number of symbols allowed in the password.
     *
     * @return integer The maximum number of symbols allowed in the password.
     */
    
public function getMaxSymbols()
    {
        return 
$this->maxSymbols;
    }

    
/**
     * Make sure that parameters don't overlap in such a way as to make
     * validation impossible. For example, if the minimum number of letters
     * numbers and symbols allowed is greater than the maximum length of the
     * password then these numbers are added together and used as the new maximum
     * password length.
     *
     * @return null     
     */
    
private function _sanitizeInputs()
    {
        
$minPosLength $this->minNumbers $this->minLetters $this->minSymbols;

        if (
$minPosLength $this->minLength) {
            
$this->minLength $minPosLength;
        }

        if (
$this->minLength $this->maxLength) {
            
$this->minLength $this->maxLength;
        }
        if (
$this->minSymbols $this->maxSymbols) {
            
$this->minSymbols $this->maxSymbols;
        }
    }
}