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); } }