, * Bertrand Mansion * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * The names of the authors may not be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * @category HTML * @package HTML_QuickForm2 * @author Alexey Borzov * @author Bertrand Mansion * @license http://opensource.org/licenses/bsd-license.php New BSD License * @link http://pear.php.net/package/HTML_QuickForm2 */ /** * HTML_Common2 - base class for HTML elements */ require_once 'HTML/Common2.php'; // By default, we generate element IDs with numeric indexes appended even for // elements with unique names. If you want IDs to be equal to the element // names by default, set this configuration option to false. if (null === HTML_Common2::getOption('id_force_append_index')) { HTML_Common2::setOption('id_force_append_index', true); } // set the default language for various elements' messages if (null === HTML_Common2::getOption('language')) { HTML_Common2::setOption('language', 'en'); } /** * Exception classes for HTML_QuickForm2 */ require_once 'HTML/QuickForm2/Exception.php'; /** * Static factory class for QuickForm2 elements */ require_once 'HTML/QuickForm2/Factory.php'; /** * Base class for HTML_QuickForm2 rules */ require_once 'HTML/QuickForm2/Rule.php'; /** * Abstract base class for all QuickForm2 Elements and Containers * * This class is mostly here to define the interface that should be implemented * by the subclasses. It also contains static methods handling generation * of unique ids for elements which do not have ids explicitly set. * * @category HTML * @package HTML_QuickForm2 * @author Alexey Borzov * @author Bertrand Mansion * @license http://opensource.org/licenses/bsd-license.php New BSD License * @version Release: 2.0.2 * @link http://pear.php.net/package/HTML_QuickForm2 */ abstract class HTML_QuickForm2_Node extends HTML_Common2 { /** * Array containing the parts of element ids * @var array */ protected static $ids = array(); /** * Element's "frozen" status * @var boolean */ protected $frozen = false; /** * Whether element's value should persist when element is frozen * @var boolean */ protected $persistent = false; /** * Element containing current * @var HTML_QuickForm2_Container */ protected $container = null; /** * Contains options and data used for the element creation * @var array */ protected $data = array(); /** * Validation rules for element * @var array */ protected $rules = array(); /** * An array of callback filters for element * @var array */ protected $filters = array(); /** * Recursive filter callbacks for element * * These are recursively applied for array values of element or propagated * to contained elements if the element is a Container * * @var array */ protected $recursiveFilters = array(); /** * Error message (usually set via Rule if validation fails) * @var string */ protected $error = null; /** * Changing 'name' and 'id' attributes requires some special handling * @var array */ protected $watchedAttributes = array('id', 'name'); /** * Intercepts setting 'name' and 'id' attributes * * These attributes should always be present and thus trying to remove them * will result in an exception. Changing their values is delegated to * setName() and setId() methods, respectively * * @param string $name Attribute name * @param string $value Attribute value, null if attribute is being removed * * @throws HTML_QuickForm2_InvalidArgumentException if trying to * remove a required attribute */ protected function onAttributeChange($name, $value = null) { if ('name' == $name) { if (null === $value) { throw new HTML_QuickForm2_InvalidArgumentException( "Required attribute 'name' can not be removed" ); } else { $this->setName($value); } } elseif ('id' == $name) { if (null === $value) { throw new HTML_QuickForm2_InvalidArgumentException( "Required attribute 'id' can not be removed" ); } else { $this->setId($value); } } } /** * Class constructor * * @param string $name Element name * @param string|array $attributes HTML attributes (either a string or an array) * @param array $data Element data (label, options used for element setup) */ public function __construct($name = null, $attributes = null, array $data = array()) { parent::__construct($attributes); $this->setName($name); // Autogenerating the id if not set on previous steps if ('' == $this->getId()) { $this->setId(); } if (!empty($data)) { $this->data = array_merge($this->data, $data); } } /** * Generates an id for the element * * Called when an element is created without explicitly given id * * @param string $elementName Element name * * @return string The generated element id */ protected static function generateId($elementName) { $stop = !self::getOption('id_force_append_index'); $tokens = strlen($elementName) ? explode('[', str_replace(']', '', $elementName)) : ($stop? array('qfauto', ''): array('qfauto')); $container =& self::$ids; $id = ''; do { $token = array_shift($tokens); // prevent generated ids starting with numbers if ('' == $id && is_numeric($token)) { $token = 'qf' . $token; } // Handle the 'array[]' names if ('' === $token) { if (empty($container)) { $token = 0; } else { $keys = array_filter(array_keys($container), 'is_numeric'); $token = empty($keys) ? 0 : end($keys); while (isset($container[$token])) { $token++; } } } $id .= '-' . $token; if (!isset($container[$token])) { $container[$token] = array(); // Handle duplicate names when not having mandatory indexes } elseif (empty($tokens) && $stop) { $tokens[] = ''; } // Handle mandatory indexes if (empty($tokens) && !$stop) { $tokens[] = ''; $stop = true; } $container =& $container[$token]; } while (!empty($tokens)); return substr($id, 1); } /** * Stores the explicitly given id to prevent duplicate id generation * * @param string $id Element id */ protected static function storeId($id) { $tokens = explode('-', $id); $container =& self::$ids; do { $token = array_shift($tokens); if (!isset($container[$token])) { $container[$token] = array(); } $container =& $container[$token]; } while (!empty($tokens)); } /** * Returns the element options * * @return array */ public function getData() { return $this->data; } /** * Returns the element's type * * @return string */ abstract public function getType(); /** * Returns the element's name * * @return string */ public function getName() { return isset($this->attributes['name'])? $this->attributes['name']: null; } /** * Sets the element's name * * @param string $name * * @return $this */ abstract public function setName($name); /** * Returns the element's id * * @return string */ public function getId() { return isset($this->attributes['id'])? $this->attributes['id']: null; } /** * Sets the element's id * * Please note that elements should always have an id in QuickForm2 and * therefore it will not be possible to remove the element's id or set it to * an empty value. If id is not explicitly given, it will be autogenerated. * * @param string $id Element's id, will be autogenerated if not given * * @return $this * @throws HTML_QuickForm2_InvalidArgumentException if id contains invalid * characters (i.e. spaces) */ public function setId($id = null) { if (is_null($id)) { $id = self::generateId($this->getName()); // HTML5 specification only disallows having space characters in id, // so we don't do stricter checks here } elseif (strpbrk($id, " \r\n\t\x0C")) { throw new HTML_QuickForm2_InvalidArgumentException( "The value of 'id' attribute should not contain space characters" ); } else { self::storeId($id); } $this->attributes['id'] = (string)$id; return $this; } /** * Returns the element's value without filters applied * * @return mixed */ abstract public function getRawValue(); /** * Returns the element's value, possibly with filters applied * * @return mixed */ public function getValue() { $value = $this->getRawValue(); return is_null($value)? null: $this->applyFilters($value); } /** * Sets the element's value * * @param mixed $value * * @return $this */ abstract public function setValue($value); /** * Returns the element's label(s) * * @return string|array */ public function getLabel() { if (isset($this->data['label'])) { return $this->data['label']; } return null; } /** * Sets the element's label(s) * * @param string|array $label Label for the element (may be an array of labels) * * @return $this */ public function setLabel($label) { $this->data['label'] = $label; return $this; } /** * Changes the element's frozen status * * @param bool $freeze Whether the element should be frozen or editable. If * omitted, the method will not change the frozen status, * just return its current value * * @return bool Old value of element's frozen status */ public function toggleFrozen($freeze = null) { $old = $this->frozen; if (null !== $freeze) { $this->frozen = (bool)$freeze; } return $old; } /** * Changes the element's persistent freeze behaviour * * If persistent freeze is on, the element's value will be kept (and * submitted) in a hidden field when the element is frozen. * * @param bool $persistent New value for "persistent freeze". If omitted, the * method will not set anything, just return the current * value of the flag. * * @return bool Old value of "persistent freeze" flag */ public function persistentFreeze($persistent = null) { $old = $this->persistent; if (null !== $persistent) { $this->persistent = (bool)$persistent; } return $old; } /** * Adds the link to the element containing current * * @param HTML_QuickForm2_Container $container Element containing * the current one, null if the link should * really be removed (if removing from container) * * @throws HTML_QuickForm2_InvalidArgumentException If trying to set a * child of an element as its container */ protected function setContainer(HTML_QuickForm2_Container $container = null) { if (null !== $container) { $check = $container; do { if ($this === $check) { throw new HTML_QuickForm2_InvalidArgumentException( 'Cannot set an element or its child as its own container' ); } } while ($check = $check->getContainer()); if (null !== $this->container && $container !== $this->container) { $this->container->removeChild($this); } } $this->container = $container; if (null !== $container) { $this->updateValue(); } } /** * Returns the element containing current * * @return HTML_QuickForm2_Container|null */ public function getContainer() { return $this->container; } /** * Returns the data sources for this element * * @return array */ protected function getDataSources() { if (empty($this->container)) { return array(); } else { return $this->container->getDataSources(); } } /** * Called when the element needs to update its value from form's data sources */ abstract protected function updateValue(); /** * Adds a validation rule * * @param HTML_QuickForm2_Rule|string $rule Validation rule or rule type * @param string|int $messageOrRunAt If first parameter is rule type, * then message to display if validation fails, otherwise constant showing * whether to perfom validation client-side and/or server-side * @param mixed $options Configuration data for the rule * @param int $runAt Whether to perfom validation * server-side and/or client side. Combination of * HTML_QuickForm2_Rule::SERVER and HTML_QuickForm2_Rule::CLIENT constants * * @return HTML_QuickForm2_Rule The added rule * @throws HTML_QuickForm2_InvalidArgumentException if $rule is of a * wrong type or rule name isn't registered with Factory * @throws HTML_QuickForm2_NotFoundException if class for a given rule * name cannot be found */ public function addRule( $rule, $messageOrRunAt = '', $options = null, $runAt = HTML_QuickForm2_Rule::SERVER ) { if ($rule instanceof HTML_QuickForm2_Rule) { $rule->setOwner($this); $runAt = '' == $messageOrRunAt? HTML_QuickForm2_Rule::SERVER: $messageOrRunAt; } elseif (is_string($rule)) { $rule = HTML_QuickForm2_Factory::createRule($rule, $this, $messageOrRunAt, $options); } else { throw new HTML_QuickForm2_InvalidArgumentException( 'addRule() expects either a rule type or ' . 'a HTML_QuickForm2_Rule instance' ); } $this->rules[] = array($rule, $runAt); return $rule; } /** * Removes a validation rule * * The method will *not* throw an Exception if the rule wasn't added to the * element. * * @param HTML_QuickForm2_Rule $rule Validation rule to remove * * @return HTML_QuickForm2_Rule Removed rule */ public function removeRule(HTML_QuickForm2_Rule $rule) { foreach ($this->rules as $i => $r) { if ($r[0] === $rule) { unset($this->rules[$i]); break; } } return $rule; } /** * Creates a validation rule * * This method is mostly useful when when chaining several rules together * via {@link HTML_QuickForm2_Rule::and_()} and {@link HTML_QuickForm2_Rule::or_()} * methods: * * $first->addRule('nonempty', 'Fill in either first or second field') * ->or_($second->createRule('nonempty')); * * * @param string $type Rule type * @param string $message Message to display if validation fails * @param mixed $options Configuration data for the rule * * @return HTML_QuickForm2_Rule The created rule * @throws HTML_QuickForm2_InvalidArgumentException If rule type is unknown * @throws HTML_QuickForm2_NotFoundException If class for the rule * can't be found and/or loaded from file */ public function createRule($type, $message = '', $options = null) { return HTML_QuickForm2_Factory::createRule($type, $this, $message, $options); } /** * Checks whether an element is required * * @return boolean */ public function isRequired() { foreach ($this->rules as $rule) { if ($rule[0] instanceof HTML_QuickForm2_Rule_Required) { return true; } } return false; } /** * Adds element's client-side validation rules to a builder object * * @param HTML_QuickForm2_JavascriptBuilder $builder */ protected function renderClientRules(HTML_QuickForm2_JavascriptBuilder $builder) { if ($this->toggleFrozen()) { return; } $onblur = HTML_QuickForm2_Rule::ONBLUR_CLIENT ^ HTML_QuickForm2_Rule::CLIENT; foreach ($this->rules as $rule) { if ($rule[1] & HTML_QuickForm2_Rule::CLIENT) { $builder->addRule($rule[0], $rule[1] & $onblur); } } } /** * Performs the server-side validation * * @return boolean Whether the element is valid */ protected function validate() { foreach ($this->rules as $rule) { if (strlen($this->error)) { break; } if ($rule[1] & HTML_QuickForm2_Rule::SERVER) { $rule[0]->validate(); } } return !strlen($this->error); } /** * Sets the error message to the element * * @param string $error * * @return $this */ public function setError($error = null) { $this->error = (string)$error; return $this; } /** * Returns the error message for the element * * @return string */ public function getError() { return $this->error; } /** * Returns Javascript code for getting the element's value * * @param bool $inContainer Whether it should return a parameter for * qf.form.getContainerValue() * * @return string */ abstract public function getJavascriptValue($inContainer = false); /** * Returns IDs of form fields that should trigger "live" Javascript validation * * Rules added to this element with parameter HTML_QuickForm2_Rule::ONBLUR_CLIENT * will be run by after these form elements change or lose focus * * @return array */ abstract public function getJavascriptTriggers(); /** * Adds a filter * * A filter is simply a PHP callback which will be applied to the element value * when getValue() is called. * * @param callback $callback The PHP callback used for filter * @param array $options Optional arguments for the callback. The first parameter * will always be the element value, then these options will * be used as parameters for the callback. * * @return $this The element * @throws HTML_QuickForm2_InvalidArgumentException If callback is incorrect */ public function addFilter($callback, array $options = array()) { if (!is_callable($callback, false, $callbackName)) { throw new HTML_QuickForm2_InvalidArgumentException( "Filter should be a valid callback, '{$callbackName}' was given" ); } $this->filters[] = array($callback, $options); return $this; } /** * Adds a recursive filter * * A filter is simply a PHP callback which will be applied to the element value * when getValue() is called. If the element value is an array, for example with * selects of type 'multiple', the filter is applied to all values recursively. * A filter on a container will not be applied on a container value but * propagated to all contained elements instead. * * If the element is not a container and its value is not an array the behaviour * will be identical to filters added via addFilter(). * * @param callback $callback The PHP callback used for filter * @param array $options Optional arguments for the callback. The first parameter * will always be the element value, then these options will * be used as parameters for the callback. * * @return $this The element * @throws HTML_QuickForm2_InvalidArgumentException If callback is incorrect */ public function addRecursiveFilter($callback, array $options = array()) { if (!is_callable($callback, false, $callbackName)) { throw new HTML_QuickForm2_InvalidArgumentException( "Filter should be a valid callback, '{$callbackName}' was given" ); } $this->recursiveFilters[] = array($callback, $options); return $this; } /** * Helper function for applying filter callback to a value * * @param mixed &$value Value being filtered * @param mixed $key Array key (not used, present to be able to use this * method as a callback to array_walk_recursive()) * @param array $filter Array containing callback and additional callback * parameters */ protected static function applyFilter(&$value, $key, $filter) { list($callback, $options) = $filter; array_unshift($options, $value); $value = call_user_func_array($callback, $options); } /** * Applies non-recursive filters on element value * * @param mixed $value Element value * * @return mixed Filtered value */ protected function applyFilters($value) { foreach ($this->filters as $filter) { self::applyFilter($value, null, $filter); } return $value; } /** * Renders the element using the given renderer * * @param HTML_QuickForm2_Renderer $renderer * @return HTML_QuickForm2_Renderer */ abstract public function render(HTML_QuickForm2_Renderer $renderer); } ?>