2023-01-23 11:03:31 +01:00

694 lines
20 KiB
PHP

<?php
/**
* Base class for all HTML_QuickForm2 elements
*
* PHP version 5
*
* LICENSE:
*
* Copyright (c) 2006-2010, Alexey Borzov <avb@php.net>,
* Bertrand Mansion <golgote@mamasam.com>
* 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 <avb@php.net>
* @author Bertrand Mansion <golgote@mamasam.com>
* @license http://opensource.org/licenses/bsd-license.php New BSD License
* @version SVN: $Id: Node.php 300747 2010-06-25 16:16:50Z mansion $
* @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);
}
/**
* Exception classes for HTML_QuickForm2
*/
// require_once 'HTML/QuickForm2/Exception.php';
require_once dirname(__FILE__) . '/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 <avb@php.net>
* @author Bertrand Mansion <golgote@mamasam.com>
* @version Release: @package_version@
*/
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();
/**
* 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 Attribute name
* @param string 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 Element name
* @param mixed Attributes (either a string or an array)
* @param array Element data (label, options and data used for element creation)
*/
public function __construct($name = null, $attributes = null, $data = null)
{
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 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);
// Handle the 'array[]' names
if ('' === $token) {
if (empty($container)) {
$token = 0;
} else {
$keys = array_keys($container);
$token = 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 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
* @return HTML_QuickForm2_Node
*/
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 elements 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 Element's id, will be autogenerated if not given
* @return HTML_QuickForm2_Node
*/
public function setId($id = null)
{
if (is_null($id)) {
$id = self::generateId($this->getName());
} else {
self::storeId($id);
}
$this->attributes['id'] = (string)$id;
return $this;
}
/**
* Returns the element's value
*
* @return mixed
*/
abstract public function getValue();
/**
* Sets the element's value
*
* @param mixed
* @return HTML_QuickForm2_Node
*/
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 for the element (may be an array of labels)
* @return HTML_QuickForm2_Node
*/
public function setLabel($label)
{
$this->data['label'] = $label;
return $this;
}
/**
* Changes the element's frozen status
*
* @param bool 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 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 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 public function updateValue();
/**
* Adds a validation rule
*
* @param HTML_QuickForm2_Rule|string Validation rule or rule type
* @param string|int 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 Additional data for the rule
* @param int Whether to perfom validation server-side
* and/or client side. Combination of HTML_QuickForm2_Rule::RUNAT_* 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
* @todo Need some means to mark the Rules for running client-side
*/
public function addRule($rule, $messageOrRunAt = '', $options = null,
$runAt = HTML_QuickForm2_Rule::RUNAT_SERVER)
{
if ($rule instanceof HTML_QuickForm2_Rule) {
$rule->setOwner($this);
$runAt = '' == $messageOrRunAt? HTML_QuickForm2_Rule::RUNAT_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 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:
* <code>
* $first->addRule('nonempty', 'Fill in either first or second field')
* ->or_($second->createRule('nonempty'));
* </code>
*
* @param string Rule type
* @param string Message to display if validation fails
* @param mixed Additional 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;
}
/**
* 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::RUNAT_SERVER) {
$rule[0]->validate();
}
}
return !strlen($this->error);
}
/**
* Sets the error message to the element
*
* @param string
* @return HTML_QuickForm2_Node
*/
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
*
* @return string
*/
abstract public function getJavascriptValue();
/**
* Adds a filter
*
* A filter is simply a PHP callback which will be applied to the element value
* when getValue() is called. A filter is by default applied recursively :
* if the value is an array, each elements it contains will
* also be filtered, unless the recursive flag is set to false.
*
* @param callback The PHP callback used for filter
* @param array 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.
* @param bool Whether to apply the filter recursively to contained elements
* @return HTML_QuickForm2_Node The element
* @throws HTML_QuickForm2_InvalidArgumentException If callback is incorrect
*/
public function addFilter($callback, array $options = null, $recursive = true)
{
if (!is_callable($callback, false, $callbackName)) {
throw new HTML_QuickForm2_InvalidArgumentException(
'Callback Filter requires a valid callback, \'' . $callbackName .
'\' was given'
);
}
$this->filters[] = array($callback, $options, 'recursive' => $recursive);
return $this;
}
/**
* Removes all element filters
*/
public function removeFilters()
{
$this->filters = array();
}
/**
* Applies element filters on element value
* @param mixed Element value
* @return mixed Filtered value
*/
protected function applyFilters($value)
{
foreach ($this->filters as $filter) {
if (is_array($value) && !empty($filter['recursive'])) {
array_walk_recursive($value,
array('HTML_QuickForm2_Node', 'applyFilter'), $filter);
} else {
self::applyFilter($value, null, $filter);
}
}
return $value;
}
protected static function applyFilter(&$value, $key = null, $filter)
{
$callback = $filter[0];
$options = $filter[1];
if (!is_array($options)) {
$options = array();
}
array_unshift($options, $value);
$value = call_user_func_array($callback, $options);
}
}
?>