/ */ class P4Cms_Form extends Zend_Dojo_Form { const CSRF_TOKEN_NAME = '_csrfToken'; /** * Useful for indenting items (e.g. in select elements) */ const UTF8_NBSP = "\xc2\xa0"; protected $_storageAdapter = null; protected $_idPrefix = null; /** * Optional form csrf protection, used to enable * verification and generation of the csrf token * for authenticated user. * @var boolean */ protected $_csrfProtection = true; protected $_populatedCsrfToken = ''; /** * Zend_Session storage object. * @var Zend_Session */ protected static $_session = null; protected static $_prefixPaths = array(); protected static $_libraryPaths = array( array( 'prefix' => 'Zend_Dojo_Form_Element', 'path' => 'Zend/Dojo/Form/Element', 'type' => self::ELEMENT ), array( 'prefix' => 'Zend_Dojo_Form_Decorator', 'path' => 'Zend/Dojo/Form/Decorator', 'type' => self::DECORATOR ), array( 'prefix' => 'P4Cms_Form_Element', 'path' => 'P4Cms/Form/Element', 'type' => self::ELEMENT ), array( 'prefix' => 'P4Cms_Form_Decorator', 'path' => 'P4Cms/Form/Decorator', 'type' => self::DECORATOR ), array( 'prefix' => 'P4Cms_Validate', 'path' => 'P4Cms/Validate', 'type' => Zend_Form_Element::VALIDATE ), array( 'prefix' => 'P4Cms_Filter', 'path' => 'P4Cms/Filter', 'type' => Zend_Form_Element::FILTER ), array( 'prefix' => 'P4_Validate', 'path' => 'P4/Validate', 'type' => Zend_Form_Element::VALIDATE ) ); /** * Extend Zend_Dojo_Form's constructor to provide our own decorators. * * @param array|Zend_Config|null $options Zend provides no documentation for this param. * @return void */ public function __construct($options = null) { // combine library prefix paths with // paths from the static registry. $prefixPaths = static::$_libraryPaths + static::$_prefixPaths; // add prefix paths to form instance. foreach ($prefixPaths as $prefixPath) { extract($prefixPath); // add element and decorator paths to form. if ($type === static::ELEMENT || $type === static::DECORATOR) { $this->addPrefixPath($prefix, $path, $type); } // add decorator, validator and filter paths to elements. if ($type !== static::ELEMENT) { $this->addElementPrefixPath($prefix, $path, $type); } // add decorator paths to display groups. if ($type === static::DECORATOR) { $this->addDisplayGroupPrefixPath($prefix, $path); } } // if no storage adapter specified, use default where available if (!isset($options['storageAdapter']) && P4Cms_Record::hasDefaultAdapter() ) { $this->_storageAdapter = P4Cms_Record::getDefaultAdapter(); } parent::__construct($options); } /** * Retrieve all form element values * * Override parent to fix an issue where form structure and form->getValues() * are inconsistent if form has a sub-form with 'isArray' property set to false. * * See: http://framework.zend.com/issues/browse/ZF-12027 * * @param bool $suppressArrayNotation zend provides no description for this param. * @return array all form values organized by element/sub-form. * @todo remove when issue ZF-12027 is resolved. */ public function getValues($suppressArrayNotation = false) { $values = array(); $eBelongTo = null; if ($this->isArray()) { $eBelongTo = $this->getElementsBelongTo(); } foreach ($this->getElements() as $key => $element) { if (!$element->getIgnore()) { $merge = array(); if (($belongsTo = $element->getBelongsTo()) !== $eBelongTo) { if ('' !== (string)$belongsTo) { $key = $belongsTo . '[' . $key . ']'; } } $merge = $this->_attachToArray($element->getValue(), $key); $values = $this->_array_replace_recursive($values, $merge); } } foreach ($this->getSubForms() as $key => $subForm) { $merge = array(); if (!$subForm->isArray()) { $merge = $subForm->getValues(); } else { $merge = $this->_attachToArray( $subForm->getValues(true), $subForm->getElementsBelongTo() ); } $values = $this->_array_replace_recursive($values, $merge); } if (!$suppressArrayNotation && $this->isArray() && !$this->_getIsRendered()) { $values = $this->_attachToArray($values, $this->getElementsBelongTo()); } return $values; } /** * Retrieve error messages from elements failing validations. * * Fix for an issue where output from parent method is not consistent with the form * structure when form contains nested sub-forms with 'isArray' flag set to false. * See description for getValues() method where we fix the same issue. * * @param string $name a element or sub-form to get messages for. * @param bool $suppressArrayNotation zend provides no description for this param. * @return array list of error messages organized by element/sub-form * @todo remove when issue ZF-12027 is resolved. */ public function getMessages($name = null, $suppressArrayNotation = false) { if (null !== $name) { if (isset($this->_elements[$name])) { return $this->getElement($name)->getMessages(); } else if (isset($this->_subForms[$name])) { return $this->getSubForm($name)->getMessages(null, true); } foreach ($this->getSubForms() as $key => $subForm) { if ($subForm->isArray()) { $belongTo = $subForm->getElementsBelongTo(); if ($name == $this->_getArrayName($belongTo)) { return $subForm->getMessages(null, true); } } } } $customMessages = $this->_getErrorMessages(); if ($this->isErrors() && !empty($customMessages)) { return $customMessages; } $messages = array(); foreach ($this->getElements() as $name => $element) { $eMessages = $element->getMessages(); if (!empty($eMessages)) { $messages[$name] = $eMessages; } } foreach ($this->getSubForms() as $key => $subForm) { $merge = $subForm->getMessages(null, true); if (!empty($merge)) { if ($subForm->isArray()) { $merge = $this->_attachToArray( $merge, $subForm->getElementsBelongTo() ); } $messages = $this->_array_replace_recursive($messages, $merge); } } if (!$suppressArrayNotation && $this->isArray() && !$this->_getIsRendered()) { $messages = $this->_attachToArray($messages, $this->getElementsBelongTo()); } return $messages; } /** * Add a new element. * * This is a wrapper around the parent function that provides more palatable * error messages for end users. * * @param string|Zend_Form_Element $element The element to add. * @param string $name The name of the element. * @param array|Zend_Config $options The options for the element. * @return Zend_Form */ public function addElement($element, $name = null, $options = null) { try { parent::addElement($element, $name, $options); } catch (Exception $e) { P4Cms_Log::log( 'P4Cms_Form->addElement exception ('. get_class($e) .') - '. $e->getMessage(), P4Cms_Log::DEBUG ); if (preg_match("/^Plugin by name '(.+)' was not found in the registry;/", $e->getMessage(), $matches)) { throw new Zend_Form_Exception('Element plugin "'. $matches[1] .'" not found.'); } else { throw $e; } } return $this; } /** * Set a string to prefix element ids with. * * @param string $prefix the string to prefix element ids with. * @return P4Cms_Form_Decorator_IdPrefix the decorator instance. */ public function setIdPrefix($prefix) { $this->_idPrefix = (string) $prefix; return $this; } /** * Get the string used to prefix element ids. * * @return string the string used to prefix element ids. */ public function getIdPrefix() { return $this->_idPrefix; } /** * Add id prefixes, then render the form. * * @param Zend_View_Interface $view The Zend View Interface to render. * @return string */ public function render(Zend_View_Interface $view = null) { // prefix form element ids if id prefix is set. if ($this->getIdPrefix()) { static::prefixFormIds($this, $this->getIdPrefix()); } return parent::render($view); } /** * Add "Errors" and "CsrfForm" to the default set of decorators. * * @return void */ public function loadDefaultDecorators() { if ($this->loadDefaultDecoratorsIsDisabled()) { return; } $decorators = $this->getDecorators(); $prepend = Zend_Form_Decorator_Abstract::PREPEND; if (empty($decorators)) { $this->addDecorator('FormElements') ->addDecorator('HtmlTag', array('tag' => 'dl', 'class' => 'zend_form_dojo')) ->addDecorator('Errors', array('placement' => $prepend)) ->addDecorator('Csrf', array('placement' => $prepend)) ->addDecorator('DijitForm'); } } /** * Add a form plugin path to be used whenever a form is instantiated. * * @param string $prefix the class prefix (e.g. Foo_Form_Element) * @param string $path the path containing the classes * @param string $type the type of plugin (e.g. element, decorator * validator, filter) */ public static function registerPrefixPath($prefix, $path, $type) { static::$_prefixPaths[$prefix] = array( 'prefix' => $prefix, 'path' => $path, 'type' => strtoupper($type) ); } /** * Get any plugin prefix paths that are statically registered. * * @return array the list of registered plugin prefix paths. */ public static function getPrefixPathRegistry() { return static::$_prefixPaths; } /** * Get any library prefix paths that are statically registered. * * @return array the list of registered library prefix paths. */ public static function getLibraryPathRegistry() { return static::$_libraryPaths; } /** * Remove any registered form plugin prefix paths. */ public static function clearPrefixPathRegistry() { static::$_prefixPaths = array(); } /** * Helper method to get the validator of an element. * Calls isValid on the element to load the correct validators. * Attempts to preserve original errors and value. * * @param string $element the name of the element to get the validator for. * @param string $validator the name of the validator to get. * @return Zend_Validate_Interface the requested validator. */ public function getElementValidator($element, $validator) { // create a copy of element (to avoid polluting the element) // and call isValid to load the validators. $temp = clone $this->getElement($element); $temp->isValid(true); return $this->getElement($element) ->setValidators($temp->getValidators()) ->getValidator($validator); } /** * Get the values of the form flattened with array notation for keys. * * @return array the flattened form values. */ public function getFlattenedValues() { $filter = new P4Cms_Filter_FlattenArray; return $filter->filter($this->getValues()); } /** * Populate form * * Records CSRF token, if enabled and present, for later use, then * calls the parent populate method. * * @param P4Cms_Record|array $values the values to populate from * @return P4Cms_Form provides fluent interface */ public function populate($values) { if ($this->hasCsrfProtection() && array_key_exists(static::CSRF_TOKEN_NAME, $values)) { $this->_populatedCsrfToken = $values[static::CSRF_TOKEN_NAME]; } return $this->setDefaults($values); } /** * Set element values. * * Extended here to support setting values from a record object. * Accepting a record object allows 'enhanced' elements to look at * other aspects of the record and make decisions accordingly. * * @param P4Cms_Record|array $defaults the default values to set on elements * @return Zend_Form provides fluent interface */ public function setDefaults($defaults) { // handle record input. if ($defaults instanceof P4Cms_Record) { $record = $defaults; $defaults = $record->getValues(); // handle enhanced elements. foreach ($defaults as $field => $value) { $element = $this->getElement($field); if ($element instanceof P4Cms_Record_EnhancedElementInterface) { $element->populateFromRecord($record); unset($defaults[$field]); } } } // Zend form has a strange behavior where sub-forms will populate // from the entire values array, rather than their designated portion // of the values array if there is no matching key in defaults for // the sub-form (e.g. from $defaults instead of $defaults['subForm']) // and parent form has no element matching the key. // // This can have some unfortunate side-effects (top-level values // polluting the sub-forms) - to avoid this problem we ensure that // there is an entry in the defaults array for every sub-form. // // Example: // assuming we have a $form with element [foo] and a sub-form 'subForm' // with elements [foo] and [bar], then // $form->setDefaults(array('foo' => 'foo_value')) // will populate only [foo] element, whereas // $form->setDefaults(array('bar' => 'bar_value')) // will populate [subForm][bar] element. This fix is to prevent the // second case above (i.e. [subForm][bar] will be populated only if // defaults array contains 'subForm' => array('bar' => 'bar_value'). foreach ($this->getSubForms() as $subForm) { $key = $subForm->getElementsBelongTo(); if ($subForm->isArray() && !isset($defaults[$key])) { $defaults[$key] = array(); } } return parent::setDefaults($defaults); } /** * Set the storage adapter to use to access records. * * @param P4Cms_Record_Adapter $adapter the adapter to use for record access * @return P4Cms_Form provides fluent interface. */ public function setStorageAdapter(P4Cms_Record_Adapter $adapter) { $this->_storageAdapter = $adapter; return $this; } /** * Get the storage adapter used by this form to access records. * * @return P4Cms_Record_Adapter the adapter used by this form. */ public function getStorageAdapter() { if ($this->_storageAdapter instanceof P4Cms_Record_Adapter) { return $this->_storageAdapter; } throw new P4Cms_Form_Exception( "Cannot get storage adapter. Adapter has not been set." ); } /** * Enables or disables the csrf protection for this form; defaults to enabled. * * @param boolean $csrf Whether or not to enable csrf protection * @return P4Cms_Form provides fluid interface */ public function setCsrfProtection($csrf) { $this->_csrfProtection = (boolean)$csrf; return $this; } /** * Returns whether or not this form has csrf protection enabled. * * Protection is always turned off for anonymous users. For authenticated * users protection is on by default but can optionally be disabled. * * @return boolean whether or not csrf protection is enabled for the form */ public function hasCsrfProtection() { return $this->_csrfProtection && P4Cms_User::hasActive() && !P4Cms_User::fetchActive()->isAnonymous(); } /** * For authenticated users, returns the current session value or * generate a new value (and set on the session) if none is present. * * For anonymous users this method simply returns null as they * don't receive CSRF protection * * @return string|null csrf token for this form */ public static function getCsrfToken() { // skip starting a session for anonymous users and simply return null if (!P4Cms_User::hasActive() || P4Cms_User::fetchActive()->isAnonymous()) { return null; } $session = static::_getSession(); if (!$session->csrfToken) { $session->csrfToken = (string) new P4Cms_Uuid; // Don't let the presence of a CSRF token in the session // prevent caching of future unrelated requests. if (P4Cms_Cache::canCache('page')) { P4Cms_Cache::getCache('page')->addIgnoredSessionVariable('Forms[csrfToken]'); } } return $session->csrfToken; } /** * This method is used to retrieve a populated csrf token for use in * validation. * * @param array $data Array to search for csrf token, or empty array * @return string csrf token */ private function getPopulatedCsrfToken($data = array()) { if (!empty($this->_populatedCsrfToken)) { $csrfToken = $this->_populatedCsrfToken; } else { // a convenience so that module authors working with forms do // not have to do anything special to handle csrf validations $request = Zend_Controller_Front::getInstance()->getRequest(); // either get token or null, if the token is not set $csrfToken = $request->getParam(static::CSRF_TOKEN_NAME); } return $csrfToken; } /** * Validate the form, including csrf check * * @param array $data the data to validate. * @return boolean */ public function isValid($data) { $isValid = parent::isValid($data); // validate the CSRF token if protection is enabled if ($this->hasCsrfProtection()) { if (array_key_exists(static::CSRF_TOKEN_NAME, $data)) { $this->_populatedCsrfToken = $data[static::CSRF_TOKEN_NAME]; } if ($this->getPopulatedCsrfToken() != static::getCsrfToken()) { $isValid = false; $this->addError('Form failed security validation.'); } } return $isValid; } /** * Helper function to adjust decorators on checkbox element * to position the checkbox label to the right of the checkbox, * instead of on the left hand side. * * @param Zend_Form_Element $element the element to adjust decorators on. * @return Zend_Form_Element the updated element. */ public static function moveCheckboxLabel(Zend_Form_Element $element) { // adjust how the auto-label element is decorated // to put the label immediately after the checkbox. $decorators = array( 'Zend_Form_Decorator_ViewHelper' => null, 'P4Cms_Form_Decorator_Label' => null ); $element->setDecorators(array_merge($decorators, $element->getDecorators())); $element->getDecorator('label') ->setOption('placement', 'append') ->setOption('tag', null); return $element; } /** * Prefix the ids of all elements, fieldsets and sub-forms within a form. * * @param Zend_Form $form a specific form to prefix the ids of. * @param string $prefix the prefix to use */ public static function prefixFormIds(Zend_Form $form, $prefix) { // prefix id of form itself. static::_applyIdPrefix($form, $prefix); // prefix elements in form. foreach ($form->getElements() as $element) { static::_applyIdPrefix($element, $prefix); } // prefix display groups. foreach ($form->getDisplayGroups() as $displayGroup) { static::_applyIdPrefix($displayGroup, $prefix); } // prefix sub-forms. foreach ($form->getSubForms() as $subForm) { $subForm->setIdPrefix($prefix); } } /** * Ensure consistent presentation of sub-forms. * * @param Zend_Form $form the sub-form to normalize. * @param string $name the name of the sub-form. * @return Zend_Form the updated form. */ public static function normalizeSubForm($form, $name = null) { $name = $name ?: $form->getName(); $form->setDecorators( array( 'FormElements', array( 'decorator' => 'HtmlTag', 'options' => array('tag' => 'dl') ), 'Fieldset', array( 'decorator' => array('DdTag' => 'HtmlTag'), 'options' => array('tag' => 'dd') ), ) ); // ensure form is identified with a css class. $class = $name ? $name . '-sub-form' : ''; if (!preg_match("/(^| )$class( |$)/", $form->getAttrib('class'))) { $form->setAttrib('class', trim($class . ' ' . $form->getAttrib('class'))); } // normalized sub-forms should always put values in an array. $form->setIsArray(true); return $form; } /** * Add prefix to id attribute of given element. * * @param mixed $element the element to prefix the id of - can be a * form, fieldset or standard element. * @param string $prefix the prefix to apply */ protected static function _applyIdPrefix($element, $prefix) { // ensure prefix ends with a dash. $prefix = rtrim($prefix, '-') . '-'; // prefix if id is not blank and not already prefixed. if (strlen($element->getId()) && strpos($element->getId(), $prefix) !== 0) { $id = $prefix . $element->getId(); $element->setAttrib('id', $id); } } /** * Return the static session object, initializing if necessary. * * @return Zend_Session_Namespace */ protected static function _getSession() { if (!static::$_session instanceof Zend_Session_Namespace) { static::$_session = new Zend_Session_Namespace('Forms'); } return static::$_session; } }