Form.php #1

  • //
  • guest/
  • perforce_software/
  • chronicle/
  • main/
  • library/
  • P4Cms/
  • Form.php
  • View
  • Commits
  • Open Download .zip Download (25 KB)
<?php
/**
 * Extends Zend_Form to provide support for an id prefix and show errors by default.
 *
 * @copyright   2011 Perforce Software. All rights reserved.
 * @license     Please see LICENSE.txt in top-level folder of this distribution.
 * @version     <release>/<patch>
 */
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;
    }
}
# Change User Description Committed
#1 16170 perforce_software Move Chronicle files to follow new path scheme for branching.
//guest/perforce_software/chronicle/library/P4Cms/Form.php
#1 8972 Matt Attaway Initial add of the Chronicle source code