Zend_Form_Element_Hash in Magento

I just managed to skip all the rather horrible and complex looking Magento form config by getting Zend_Form working in the controllers instead. (I reserve the right to retract what I said just there about Magento forms, seeing as I haven’t actually tried it that way yet!)

Anyway, as you know, CSRF (cross site request forgery) tokens are pretty important to secure up your forms, and Zend_Form_Element_Hash does a fine job of taking care of this, however the problem I got was the old “session already started” message. What I did was create my own Form_Element_Hash. It’s a copy paste job of the original Zend class, only it uses the Magento session. Here it is:

<?php /**  * Created by PhpStorm.  * User: delboy1978uk  * Date: 03/06/15  * Time: 15:30  */ class Del_Form_Element_Hash extends Zend_Form_Element_Xhtml {     /**      * Use formHidden view helper by default      * @var string      */     public $helper = 'formHidden';     /**      * Actual hash used.      *      * @var mixed      */     protected $_hash;     /**      * Salt for CSRF token      * @var string      */     protected $_salt = '54lt4ndP3pp3r';     /**      * @var Zend_Session_Namespace      */     protected $_session;     /**      * TTL for CSRF token      * @var int      */     protected $_timeout = 300;     /**      * Constructor      *      * Creates session namespace for CSRF token, and adds validator for CSRF      * token.      *      * @param array|string|Zend_Config $spec      * @param null $options      * @throws Zend_Form_Exception      */     public function __construct($spec, $options = null)     {         parent::__construct($spec, $options);         $this->setAllowEmpty(false)
            ->setRequired(true)
            ->initCsrfValidator();
    }

    /**
     * Set session object
     *
     * @param  Mage_Core_Model_Session $session
     * @return Del_Form_Element_Hash
     */
    public function setSession($session)
    {
        $this->_session = $session;
        return $this;
    }

    /**
     * Get session object
     *
     * Instantiate session object if none currently exists
     *
     * @return Mage_Core_Model_Session
     */
    public function getSession()
    {
        if (null === $this->_session)
        {
            $this->_session = Mage::getSingleton('core/session');
        }
        return $this->_session;
    }

    /**
     * Initialize CSRF validator
     *
     * Creates Session namespace, and initializes CSRF token in session.
     * Additionally, adds validator for validating CSRF token.
     *
     * @return Del_Form_Element_Hash
     */
    public function initCsrfValidator()
    {
        $session = $this->getSession();
        $key = $session->getData('csrf');
        if (isset($key))
        {
            $rightHash = $session->getData('csrf');
        }
        else
        {
            $rightHash = null;
        }

        $this->addValidator('Identical', true, array($rightHash));
        return $this;
    }

    /**
     * Salt for CSRF token
     *
     * @param  string $salt
     * @return Del_Form_Element_Hash
     */
    public function setSalt($salt)
    {
        $this->_salt = (string) $salt;
        return $this;
    }

    /**
     * Retrieve salt for CSRF token
     *
     * @return string
     */
    public function getSalt()
    {
        return $this->_salt;
    }

    /**
     * Retrieve CSRF token
     *
     * If no CSRF token currently exists, generates one.
     *
     * @return string
     */
    public function getHash()
    {
        if (null === $this->_hash) {
            $this->_generateHash();
        }
        return $this->_hash;
    }

    /**
     * Get session namespace for CSRF token
     *
     * Generates a session namespace based on salt, element name, and class.
     *
     * @return string
     */
    public function getSessionName()
    {
        return __CLASS__ . '_' . $this->getSalt() . '_' . $this->getName();
    }

    /**
     * Set timeout for CSRF session token
     *
     * @param  int $ttl
     * @return Del_Form_Element_Hash
     */
    public function setTimeout($ttl)
    {
        $this->_timeout = (int) $ttl;
        return $this;
    }

    /**
     * Get CSRF session token timeout
     *
     * @return int
     */
    public function getTimeout()
    {
        return $this->_timeout;
    }

    /**
     * Override getLabel() to always be empty
     *
     * @return null
     */
    public function getLabel()
    {
        return null;
    }

    /**
     * Initialize CSRF token in session
     *
     * @return void
     */
    public function initCsrfToken()
    {
        $session = $this->getSession();
        $session->setData('csrf',$this->getHash());
    }

    /**
     * Render CSRF token in form
     *
     * @param  Zend_View_Interface $view
     * @return string
     */
    public function render(Zend_View_Interface $view = null)
    {
        $this->initCsrfToken();
        return parent::render($view);
    }

    /**
     * Generate CSRF token
     *
     * Generates CSRF token and stores both in {@link $_hash} and element
     * value.
     *
     * @return void
     */
    protected function _generateHash()
    {
        $this->_hash = md5(
            mt_rand(1,1000000)
            .  $this->getSalt()
            .  $this->getName()
            .  mt_rand(1,1000000)
        );
        $this->setValue($this->_hash);
    }

}
Advertisements

Using Zend_Form in Magento

As we all know, Magento is built using some of the Zend Framework library. However, that doesn’t mean you can go straight into a controller and start programming like you would ZF. However, preparing your Zend_Form in order to work in Magento is pretty straigtforward.

Magento’s controllers do not have a view! So your form will fail to render! To fix this, we override the __toString() method:

/**
 *  Overridden because Magento doesnt have the zend view
 */
public function __toString()
{
    return  $this->render(new Zend_View());
}

Now your form will work in your magento controller.  If you are new to Magento like I am, you’ll also need to know how to instantiate the form, send the form to the view, and how to display it once we are in the view! In the controller:

public function indexAction()
{
    $this->loadLayout();
 
 // new My_Form_Class(); also works here, but this is how Magento does it, from the xml config.
 $this->form = Mage::getModel('madskull_feedback/form'); 

 if($this->getRequest()->isPost())
 {
   $data = $this->getRequest()->getPost();
   if($this->form->isValid($data))
   {
     // get sanitised data
     $data = $this->form->getValues();
     $this->sendFeedbackEmail($data);
     $this->_redirect('*/*/thanks');
   }
   else
   {
     $this->form->populate($data);
   }
  }

  $this->getLayout()->getBlock('madskull_feedback.form')->setData('form', $this->form);
  $this->renderLayout();
}

On the .phtml file, it’s a piece of cake:

echo  $this->getData('form');

There you go! Full Zend_Form goodness! Have fun!

Customising Zend_Form Views

Sometimes, you’ll want the same form in more than one place in your application, however occasionally, that means your forms look terrible! However you can specify a different view script to use (I put mine in a forms subdirectory):

$this->addDecorators(array(
    array('ViewScript', array('viewScript' => 'view_folder/different-looking-form.phtml'))
));

And the bit that confdused me for a while, accessing the form in that view is strangle called $this->element:

<?= $this->element; ?>

Of course, as you may know, you don’t have to render the form like that, which is kind of the point of this:

<?php
$form = $this->element;
$supply = $form->getSupply();
$fueltype = $form->getFueltype();

?>

<form id = "<?=$form->getAttrib('id') ?>"
    name = "<?=$form->getName() ?>"
    method = "<?=$form->getMethod(); ?>"
    enctype = "<?=$form->getEnctype(); ?>"
    >
    <dl class="zend_form">

        <dt><label>Supply / Meter</label></dt>
        <dd><?=$supply->getMeterNumber() ?></dd>

        <dt><label>Fuel Type</label></dt>
        <dd><?=$fueltype->getFueltype() ?></dd>

        <dt><label>Description</label></dt>
        <dd><?=$supply->getDescription() ?></dd>

    <?php
    foreach ($form->getElements() as $element)
    {
        echo $element;
    }
    ?>
    </dl>
</form>

Sorted! No need to encase your form in more divs and mess around with CSS! 🙂

Zend_Validate custom Form Error Messages

Zend_Form is awesome. Zend_Validate and Zend_Filter are awesome. The Error decorators are also awesome, but sometimes a little too awesome.

I mean, when a user leaves an email field blank, and I had a NotEmpty and EmailAddress validator, I get two error messages!

  • Value is required and can't be empty
  • '' is no valid email address in the basic format local-part@hostname

Really I’d rather it just said:

  • Please enter a valid email address

Because some users, lets face it, are idiots! Nice idiots, but idiots all the same lol!

To stop this sort of thing happening, you can say in your form element:

$email = new Zend_Form_Element_Text('email');
 $email->setRequired(true)
 ->addFilter('StripTags')
 ->addFilter('StringTrim')
 ->addValidator('NotEmpty',true)
 ->addValidator('EmailAddress')
 ->setLabel('Email')
 ->setErrorMessages(array('Please enter a real email address'));

The key thing here is the true value in the NotEmpty validator. It breaks the chain of validators upon failure, stopping subsequent validators from checking and adding its error message too.
Without the true, but with the custom error message, you would get:

  • Please enter a valid email address
  • Please enter a valid email address

This should help save headaches!