In a recent project I had the need to verify a users current password on a form used to update that users login credentials - this is the example custom validator code I will be showing here.
Under the application directory create a subdirectory called validate, and within this a new file called MatchPassword.php.
Inside this file put the following code:
<?php class Application_Validate_MatchPassword extends Zend_Validate_Abstract { const NOT_MATCH = 'notMatch'; protected $_messageTemplates = array( self::NOT_MATCH => "Incorrect password entered" ); public function isValid($value) { $authadapter = new Zend_Auth_Adapter_DbTable(Zend_Db_Table::getDefaultAdapter()); $authadapter->setTableName('users') ->setIdentityColumn('username') ->setCredentialColumn('password') ->setCredentialTreatment('MD5(CONCAT(salt,?))'); $authadapter->setIdentity($_POST['username']); $authadapter->setCredential($value); $auth = Zend_Auth::getInstance(); $result = $auth->authenticate($authadapter); if ($result->isValid()) { return true; } $this->_error(self::NOT_MATCH); return false; } }First we declare the class name and it must extend Zend_Validate_Abstract. The class name can be anything, but Application_Validate_MatchPassword is logical because of the file location and the function of the custom validator.
Next we declare the variable to contain the failed validation error message and assign the desired text to it within the $_messageTemplates array.
Custom validators only support two methods, isValid() and getMessages(), for this validator we will only be using isValid(), but if you are interested to read more about the two methods with example custom validator code, then refer to the Zend manual here.
To get the validator to run, you just need to call the isValid() method on the form object from within whatever controller you use to instantiate the form, for instance:
$form = new Application_Form_SomeForm(); $request = $this->getRequest(); if ($form->isValid($request->getPost())) { // the form passed validation so take some action here }You don't need any logic for if the form fails validation as the error messages will automatically be displayed next to the form element the validator has been added to.
So within the isValid() method above you can see we make use of Zend_Auth to allow us to verify the password given against the actual current user password. As we are validating against a password held in a database, we also use the Zend_Auth_Adapter_DbTable class.
Firstly we instantiate Zend_Auth_Adapter_DbTable passing it the default database connection details pulled from our projects application.ini file:
$authadapter = new Zend_Auth_Adapter_DbTable(Zend_Db_Table::getDefaultAdapter());Next we tell it which database table we want to validate against (in this case users), followed by which column contains the username, and then password data:
$authadapter->setTableName('users') ->setIdentityColumn('username') ->setCredentialColumn('password')Finally we give it some details about how the password data is stored. If this line is emitted, then it would mean you would have to be storing passwords in plain text in your database (a bad idea!). In this case when the password is stored, a random salt value is created which is concatenated with the plain text password, and an MD5 hash is then created out of the result. So we need to tell Zend_Auth_Adapter_DbTable that this is what has happened so that it can perform the same operation when the custom validator runs.
The ? represents the password entered into the form element the custom validator is added to, and salt represents the contents of the salt column in the database table we are using for the record we are validating against. The contents of this column is the random salt value which is concatenated with the plain text password, and was also saved when the record was created:
->setCredentialTreatment('MD5(CONCAT(salt,?))');Finally we tell Zend_Auth_Adapter_DbTable what data we want to authenticate with. The form this custom validator is added to also has an element username, and in the table users, the column username is primary key, so we pass the the post data username to the setIdentity() method. The variable $value (as defined in the isValid() function declaration) is the contents of the form field to which this custom validator is applied when the form gets submitted. As we have already told Zend_Auth_Adapter_DbTable that we want to authenticate against the column password when we call the setCredentialColumn() method, we need to tell it what data we need to validate against the contents of this column. To do this we pass the setCredential() method the $value variable.
$authadapter->setIdentity($_POST['username']); $authadapter->setCredential($value);Now that Zend_Auth_Adapter_DbTable has all the details it needs, we can instantiate an instance of Zend_Auth:
$auth = Zend_Auth::getInstance();And then store the results of an authentication against the populated Zend_Auth_Adapter_DbTable instance:
$result = $auth->authenticate($authadapter);If the password authentication succeeds, then we return true to indicate validation has passed:
if ($result->isValid()) { return true; }Otherwise we set the element error message to NOT_MATCH as defined at the top of the class, and return false to indicate validation has not passed (in which case that error message will be displayed next to the relevant form element):
$this->_error(self::NOT_MATCH); return false;Now we have the custom validator written and in the right place, we just need to add it to our form. This is actually very easy, we need to declare the presence of an additional path to search for form validators under at the top of the form class and within the init() function:
<?php class Application_Form_SomeForm extends Zend_Form { public function init() { $this->addElementPrefixPath('Application_Validate', APPLICATION_PATH . '/validate/', 'validate'); // other form code } }And then within the relevant form element, add in the validator:
class Application_Form_SomeForm extends Zend_Form { public function init() { $this->addElementPrefixPath('Application_Validate', APPLICATION_PATH . '/validate/', 'validate'); // other form code $this->addElement('password', 'currentpassword', array( 'label' => 'Enter current user password:', 'required' => true, 'validators' => array( 'MatchPassword' ) )); // other form code } }Now when you call the isValid() method on the form object instantiated from your controller, the custom validator will run against the element that declares it. Remember you also need a username element in your form for it to work.
IMPORTANT:
Because Zend_Auth is a singleton there can only be one instance of the class. So if you are using it anywhere else in your site (to for instance login users), then this method will interfere with the stored user credentials in the object. In this case, you should use the following code instead for MatchPassword.php:
<?php class Application_Validate_MatchPassword extends Zend_Validate_Abstract { const NOT_MATCH = 'notMatch'; protected $_messageTemplates = array( self::NOT_MATCH => "Incorrect password entered" ); public function isValid($value) { $mapper = new Application_Model_UsersMapper(); $user = new Application_Model_Users(); $mapper->find($_POST['username'], $user); if ($user->getPassword() === md5($user->getSalt() . $value)) { return true; } $this->_error(self::NOT_MATCH); return false; } }This performs exactly the same operation as before, but won't interfere with your Zend_Auth credentials. You may have noticed that this method refers to two model classes, Application_Model_UsersMapper and Application_Model_Users. You will need these for the validation class to work correctly so create application/models/Users.php and application/models/UsersMapper.php (you can of course call these something else). In Users.php put the following code:
<?php class Application_Model_Users { protected $_username; protected $_password; protected $_email; protected $_active; protected $_lastaccess; protected $_salt; public function __construct(array $options = null) { if (is_array($options)) { $this->setOptions($options); } } public function __set($name, $value) { $method = 'set' . $name; if (('mapper' == $name) || !method_exists($this, $method)) { throw new Exception('Invalid users property'); } $this->$method($value); } public function __get($name) { $method = 'get' . $name; if (('mapper' == $name) || !method_exists($this, $method)) { throw new Exception('Invalid users property'); } return $this->$method(); } public function setOptions(array $options) { $methods = get_class_methods($this); foreach ($options as $key => $value) { $method = 'set' . ucfirst($key); if (in_array($method, $methods)) { $this->$method($value); } } return $this; } public function setUsername($text) { $this->_username = (string) $text; return $this; } public function getUsername() { return $this->_username; } public function setPassword($text) { $this->_password = (string) $text; return $this; } public function getPassword() { return $this->_password; } public function setEmail($email) { $this->_email = (string) $email; return $this; } public function getEmail() { return $this->_email; } public function setActive($active) { $this->_active = (int) $active; return $this; } public function getActive() { return $this->_active; } public function setLastaccess($lastaccess) { $this->_lastaccess = (string) $lastaccess; return $this; } public function getLastaccess() { return $this->_lastaccess; } public function setSalt($salt) { $this->_salt = (string) $salt; return $this; } public function getSalt() { return $this->_salt; } }I'm not going to go into much detail about this class, but when instantiated it sets up an object you can populate with information from the database via the mapper you are about to create. You should create a protected variable relating to each columnn in the database table that holds user credentials. In my case, I have the columns, username, password, email, active, lastaccess and salt with username being primary key. For each of the variables you define you also need to create a get and set method as above. Finally, if you pass the class an array when you instantiate it, the object will be populated with those values, so you might pass something like the following:
$data = array('username' => 'yourusername', 'password' => 'yourpassword', 'email' => 'youremail', 'active' => 'youractive')So, now we need to mapper class to retrieve the database information, inside UsersMapper.php put the following code:
<?php class Application_Model_UsersMapper { protected $_dbTable; public function getDbTable() { if (null === $this->_dbTable) { $this->setDbTable('Application_Model_DbTable_Users'); } return $this->_dbTable; } public function find($username, Application_Model_Users $user) { $result = $this->getDbTable()->find($username); if (0 == count($result)) { return; } $row = $result->current(); $user->setUsername($row->username) ->setPassword($row->password) ->setSalt($row->salt) ->setEmail($row->email) ->setActive($row->active) ->setLastaccess($row->lastaccess); } }This class retrieves an entry from the database using the find() method of Zend_Db_Table_Abstract and populates the Application_Model_Users object you pass it with any returned data. There is one more class you need for this to work, being Application_Model_DbTable_Users thats referred to in the mapper. Create the file application/models/DbTable/Users.php and put the following content inside:
<?php class Application_Model_DbTable_Users extends Zend_Db_Table_Abstract { protected $_name = 'users'; protected $_primary = 'username'; }This class is just used to define the name of the database table to use, and the column defined as primary key.
So overall a bit more complex than using Zend_Auth, but does allow you to create the validator without interfering with stored user credentials in the object.
Thank you, best zend form validator so far.
ReplyDelete