1<?php
2/**
3 * Base class for all HTML_QuickForm2 elements
4 *
5 * PHP version 5
6 *
7 * LICENSE:
8 *
9 * Copyright (c) 2006-2010, Alexey Borzov <avb@php.net>,
10 *                          Bertrand Mansion <golgote@mamasam.com>
11 * All rights reserved.
12 *
13 * Redistribution and use in source and binary forms, with or without
14 * modification, are permitted provided that the following conditions
15 * are met:
16 *
17 *    * Redistributions of source code must retain the above copyright
18 *      notice, this list of conditions and the following disclaimer.
19 *    * Redistributions in binary form must reproduce the above copyright
20 *      notice, this list of conditions and the following disclaimer in the
21 *      documentation and/or other materials provided with the distribution.
22 *    * The names of the authors may not be used to endorse or promote products
23 *      derived from this software without specific prior written permission.
24 *
25 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
26 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
27 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
28 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
29 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
30 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
31 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
32 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
33 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
34 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
35 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36 *
37 * @category   HTML
38 * @package    HTML_QuickForm2
39 * @author     Alexey Borzov <avb@php.net>
40 * @author     Bertrand Mansion <golgote@mamasam.com>
41 * @license    http://opensource.org/licenses/bsd-license.php New BSD License
42 * @version    SVN: $Id: Node.php 300747 2010-06-25 16:16:50Z mansion $
43 * @link       http://pear.php.net/package/HTML_QuickForm2
44 */
45
46/**
47 * HTML_Common2 - base class for HTML elements
48 */
49// require_once 'HTML/Common2.php';
50
51// By default, we generate element IDs with numeric indexes appended even for
52// elements with unique names. If you want IDs to be equal to the element
53// names by default, set this configuration option to false.
54if (null === HTML_Common2::getOption('id_force_append_index')) {
55    HTML_Common2::setOption('id_force_append_index', true);
56}
57
58/**
59 * Exception classes for HTML_QuickForm2
60 */
61// require_once 'HTML/QuickForm2/Exception.php';
62require_once dirname(__FILE__) . '/Exception.php';
63
64/**
65 * Static factory class for QuickForm2 elements
66 */
67// require_once 'HTML/QuickForm2/Factory.php';
68
69/**
70 * Base class for HTML_QuickForm2 rules
71 */
72// require_once 'HTML/QuickForm2/Rule.php';
73
74
75/**
76 * Abstract base class for all QuickForm2 Elements and Containers
77 *
78 * This class is mostly here to define the interface that should be implemented
79 * by the subclasses. It also contains static methods handling generation
80 * of unique ids for elements which do not have ids explicitly set.
81 *
82 * @category   HTML
83 * @package    HTML_QuickForm2
84 * @author     Alexey Borzov <avb@php.net>
85 * @author     Bertrand Mansion <golgote@mamasam.com>
86 * @version    Release: @package_version@
87 */
88abstract class HTML_QuickForm2_Node extends HTML_Common2
89{
90   /**
91    * Array containing the parts of element ids
92    * @var array
93    */
94    protected static $ids = array();
95
96   /**
97    * Element's "frozen" status
98    * @var boolean
99    */
100    protected $frozen = false;
101
102   /**
103    * Whether element's value should persist when element is frozen
104    * @var boolean
105    */
106    protected $persistent = false;
107
108   /**
109    * Element containing current
110    * @var HTML_QuickForm2_Container
111    */
112    protected $container = null;
113
114   /**
115    * Contains options and data used for the element creation
116    * @var  array
117    */
118    protected $data = array();
119
120   /**
121    * Validation rules for element
122    * @var  array
123    */
124    protected $rules = array();
125
126   /**
127    * An array of callback filters for element
128    * @var  array
129    */
130    protected $filters = array();
131
132   /**
133    * Error message (usually set via Rule if validation fails)
134    * @var  string
135    */
136    protected $error = null;
137
138   /**
139    * Changing 'name' and 'id' attributes requires some special handling
140    * @var array
141    */
142    protected $watchedAttributes = array('id', 'name');
143
144   /**
145    * Intercepts setting 'name' and 'id' attributes
146    *
147    * These attributes should always be present and thus trying to remove them
148    * will result in an exception. Changing their values is delegated to
149    * setName() and setId() methods, respectively
150    *
151    * @param    string  Attribute name
152    * @param    string  Attribute value, null if attribute is being removed
153    * @throws   HTML_QuickForm2_InvalidArgumentException    if trying to
154    *                                   remove a required attribute
155    */
156    protected function onAttributeChange($name, $value = null)
157    {
158        if ('name' == $name) {
159            if (null === $value) {
160                throw new HTML_QuickForm2_InvalidArgumentException(
161                    "Required attribute 'name' can not be removed"
162                );
163            } else {
164                $this->setName($value);
165            }
166        } elseif ('id' == $name) {
167            if (null === $value) {
168                throw new HTML_QuickForm2_InvalidArgumentException(
169                    "Required attribute 'id' can not be removed"
170                );
171            } else {
172                $this->setId($value);
173            }
174        }
175    }
176
177   /**
178    * Class constructor
179    *
180    * @param    string  Element name
181    * @param    mixed   Attributes (either a string or an array)
182    * @param    array   Element data (label, options and data used for element creation)
183    */
184    public function __construct($name = null, $attributes = null, $data = null)
185    {
186        parent::__construct($attributes);
187        $this->setName($name);
188        // Autogenerating the id if not set on previous steps
189        if ('' == $this->getId()) {
190            $this->setId();
191        }
192        if (!empty($data)) {
193            $this->data = array_merge($this->data, $data);
194        }
195    }
196
197
198   /**
199    * Generates an id for the element
200    *
201    * Called when an element is created without explicitly given id
202    *
203    * @param  string   Element name
204    * @return string   The generated element id
205    */
206    protected static function generateId($elementName)
207    {
208        $stop      =  !self::getOption('id_force_append_index');
209        $tokens    =  strlen($elementName)
210                      ? explode('[', str_replace(']', '', $elementName))
211                      : ($stop? array('qfauto', ''): array('qfauto'));
212        $container =& self::$ids;
213        $id        =  '';
214
215        do {
216            $token = array_shift($tokens);
217            // Handle the 'array[]' names
218            if ('' === $token) {
219                if (empty($container)) {
220                    $token = 0;
221                } else {
222                    $keys  = array_keys($container);
223                    $token = end($keys);
224                    while (isset($container[$token])) {
225                        $token++;
226                    }
227                }
228            }
229            $id .= '-' . $token;
230            if (!isset($container[$token])) {
231                $container[$token] = array();
232            // Handle duplicate names when not having mandatory indexes
233            } elseif (empty($tokens) && $stop) {
234                $tokens[] = '';
235            }
236            // Handle mandatory indexes
237            if (empty($tokens) && !$stop) {
238                $tokens[] = '';
239                $stop     = true;
240            }
241            $container =& $container[$token];
242        } while (!empty($tokens));
243
244        return substr($id, 1);
245    }
246
247
248   /**
249    * Stores the explicitly given id to prevent duplicate id generation
250    *
251    * @param    string  Element id
252    */
253    protected static function storeId($id)
254    {
255        $tokens    =  explode('-', $id);
256        $container =& self::$ids;
257
258        do {
259            $token = array_shift($tokens);
260            if (!isset($container[$token])) {
261                $container[$token] = array();
262            }
263            $container =& $container[$token];
264        } while (!empty($tokens));
265    }
266
267
268   /**
269    * Returns the element options
270    *
271    * @return   array
272    */
273    public function getData()
274    {
275        return $this->data;
276    }
277
278
279   /**
280    * Returns the element's type
281    *
282    * @return   string
283    */
284    abstract public function getType();
285
286
287   /**
288    * Returns the element's name
289    *
290    * @return   string
291    */
292    public function getName()
293    {
294        return isset($this->attributes['name'])? $this->attributes['name']: null;
295    }
296
297
298   /**
299    * Sets the element's name
300    *
301    * @param    string
302    * @return   HTML_QuickForm2_Node
303    */
304    abstract public function setName($name);
305
306
307   /**
308    * Returns the element's id
309    *
310    * @return   string
311    */
312    public function getId()
313    {
314        return isset($this->attributes['id'])? $this->attributes['id']: null;
315    }
316
317
318   /**
319    * Sets the elements id
320    *
321    * Please note that elements should always have an id in QuickForm2 and
322    * therefore it will not be possible to remove the element's id or set it to
323    * an empty value. If id is not explicitly given, it will be autogenerated.
324    *
325    * @param    string  Element's id, will be autogenerated if not given
326    * @return   HTML_QuickForm2_Node
327    */
328    public function setId($id = null)
329    {
330        if (is_null($id)) {
331            $id = self::generateId($this->getName());
332        } else {
333            self::storeId($id);
334        }
335        $this->attributes['id'] = (string)$id;
336        return $this;
337    }
338
339
340   /**
341    * Returns the element's value
342    *
343    * @return   mixed
344    */
345    abstract public function getValue();
346
347
348   /**
349    * Sets the element's value
350    *
351    * @param    mixed
352    * @return   HTML_QuickForm2_Node
353    */
354    abstract public function setValue($value);
355
356
357   /**
358    * Returns the element's label(s)
359    *
360    * @return   string|array
361    */
362    public function getLabel()
363    {
364        if (isset($this->data['label'])) {
365            return $this->data['label'];
366        }
367        return null;
368    }
369
370
371   /**
372    * Sets the element's label(s)
373    *
374    * @param    string|array    Label for the element (may be an array of labels)
375    * @return   HTML_QuickForm2_Node
376    */
377    public function setLabel($label)
378    {
379        $this->data['label'] = $label;
380        return $this;
381    }
382
383
384   /**
385    * Changes the element's frozen status
386    *
387    * @param    bool    Whether the element should be frozen or editable. If
388    *                   omitted, the method will not change the frozen status,
389    *                   just return its current value
390    * @return   bool    Old value of element's frozen status
391    */
392    public function toggleFrozen($freeze = null)
393    {
394        $old = $this->frozen;
395        if (null !== $freeze) {
396            $this->frozen = (bool)$freeze;
397        }
398        return $old;
399    }
400
401
402   /**
403    * Changes the element's persistent freeze behaviour
404    *
405    * If persistent freeze is on, the element's value will be kept (and
406    * submitted) in a hidden field when the element is frozen.
407    *
408    * @param    bool    New value for "persistent freeze". If omitted, the
409    *                   method will not set anything, just return the current
410    *                   value of the flag.
411    * @return   bool    Old value of "persistent freeze" flag
412    */
413    public function persistentFreeze($persistent = null)
414    {
415        $old = $this->persistent;
416        if (null !== $persistent) {
417            $this->persistent = (bool)$persistent;
418        }
419        return $old;
420    }
421
422
423   /**
424    * Adds the link to the element containing current
425    *
426    * @param    HTML_QuickForm2_Container  Element containing the current one,
427    *                                      null if the link should really be
428    *                                      removed (if removing from container)
429    * @throws   HTML_QuickForm2_InvalidArgumentException   If trying to set a
430    *                               child of an element as its container
431    */
432    protected function setContainer(HTML_QuickForm2_Container $container = null)
433    {
434        if (null !== $container) {
435            $check = $container;
436            do {
437                if ($this === $check) {
438                    throw new HTML_QuickForm2_InvalidArgumentException(
439                        'Cannot set an element or its child as its own container'
440                    );
441                }
442            } while ($check = $check->getContainer());
443            if (null !== $this->container && $container !== $this->container) {
444                $this->container->removeChild($this);
445            }
446        }
447        $this->container = $container;
448        if (null !== $container) {
449            $this->updateValue();
450        }
451    }
452
453
454   /**
455    * Returns the element containing current
456    *
457    * @return   HTML_QuickForm2_Container|null
458    */
459    public function getContainer()
460    {
461        return $this->container;
462    }
463
464   /**
465    * Returns the data sources for this element
466    *
467    * @return   array
468    */
469    protected function getDataSources()
470    {
471        if (empty($this->container)) {
472            return array();
473        } else {
474            return $this->container->getDataSources();
475        }
476    }
477
478   /**
479    * Called when the element needs to update its value from form's data sources
480    */
481   abstract public function updateValue();
482
483   /**
484    * Adds a validation rule
485    *
486    * @param    HTML_QuickForm2_Rule|string     Validation rule or rule type
487    * @param    string|int                      If first parameter is rule type, then
488    *               message to display if validation fails, otherwise constant showing
489    *               whether to perfom validation client-side and/or server-side
490    * @param    mixed                           Additional data for the rule
491    * @param    int                             Whether to perfom validation server-side
492    *               and/or client side. Combination of HTML_QuickForm2_Rule::RUNAT_* constants
493    * @return   HTML_QuickForm2_Rule            The added rule
494    * @throws   HTML_QuickForm2_InvalidArgumentException    if $rule is of a
495    *               wrong type or rule name isn't registered with Factory
496    * @throws   HTML_QuickForm2_NotFoundException   if class for a given rule
497    *               name cannot be found
498    * @todo     Need some means to mark the Rules for running client-side
499    */
500    public function addRule($rule, $messageOrRunAt = '', $options = null,
501                            $runAt = HTML_QuickForm2_Rule::RUNAT_SERVER)
502    {
503        if ($rule instanceof HTML_QuickForm2_Rule) {
504            $rule->setOwner($this);
505            $runAt = '' == $messageOrRunAt? HTML_QuickForm2_Rule::RUNAT_SERVER: $messageOrRunAt;
506        } elseif (is_string($rule)) {
507            $rule = HTML_QuickForm2_Factory::createRule($rule, $this, $messageOrRunAt, $options);
508        } else {
509            throw new HTML_QuickForm2_InvalidArgumentException(
510                'addRule() expects either a rule type or ' .
511                'a HTML_QuickForm2_Rule instance'
512            );
513        }
514
515        $this->rules[] = array($rule, $runAt);
516        return $rule;
517    }
518
519   /**
520    * Removes a validation rule
521    *
522    * The method will *not* throw an Exception if the rule wasn't added to the
523    * element.
524    *
525    * @param    HTML_QuickForm2_Rule    Validation rule to remove
526    * @return   HTML_QuickForm2_Rule    Removed rule
527    */
528    public function removeRule(HTML_QuickForm2_Rule $rule)
529    {
530        foreach ($this->rules as $i => $r) {
531            if ($r[0] === $rule) {
532                unset($this->rules[$i]);
533                break;
534            }
535        }
536        return $rule;
537    }
538
539   /**
540    * Creates a validation rule
541    *
542    * This method is mostly useful when when chaining several rules together
543    * via {@link HTML_QuickForm2_Rule::and_()} and {@link HTML_QuickForm2_Rule::or_()}
544    * methods:
545    * <code>
546    * $first->addRule('nonempty', 'Fill in either first or second field')
547    *     ->or_($second->createRule('nonempty'));
548    * </code>
549    *
550    * @param    string                  Rule type
551    * @param    string                  Message to display if validation fails
552    * @param    mixed                   Additional data for the rule
553    * @return   HTML_QuickForm2_Rule    The created rule
554    * @throws   HTML_QuickForm2_InvalidArgumentException If rule type is unknown
555    * @throws   HTML_QuickForm2_NotFoundException        If class for the rule
556    *           can't be found and/or loaded from file
557    */
558    public function createRule($type, $message = '', $options = null)
559    {
560        return HTML_QuickForm2_Factory::createRule($type, $this, $message, $options);
561    }
562
563
564   /**
565    * Checks whether an element is required
566    *
567    * @return   boolean
568    */
569    public function isRequired()
570    {
571        foreach ($this->rules as $rule) {
572            if ($rule[0] instanceof HTML_QuickForm2_Rule_Required) {
573                return true;
574            }
575        }
576        return false;
577    }
578
579
580   /**
581    * Performs the server-side validation
582    *
583    * @return   boolean     Whether the element is valid
584    */
585    protected function validate()
586    {
587        foreach ($this->rules as $rule) {
588            if (strlen($this->error ?? '')) {
589                break;
590            }
591            if ($rule[1] & HTML_QuickForm2_Rule::RUNAT_SERVER) {
592                $rule[0]->validate();
593            }
594        }
595        return !strlen($this->error ?? '');
596    }
597
598   /**
599    * Sets the error message to the element
600    *
601    * @param    string
602    * @return   HTML_QuickForm2_Node
603    */
604    public function setError($error = null)
605    {
606        $this->error = (string)$error;
607        return $this;
608    }
609
610   /**
611    * Returns the error message for the element
612    *
613    * @return   string
614    */
615    public function getError()
616    {
617        return $this->error;
618    }
619
620   /**
621    * Returns Javascript code for getting the element's value
622    *
623    * @return string
624    */
625    abstract public function getJavascriptValue();
626
627   /**
628    * Adds a filter
629    *
630    * A filter is simply a PHP callback which will be applied to the element value
631    * when getValue() is called. A filter is by default applied recursively :
632    * if the value is an array, each elements it contains will
633    * also be filtered, unless the recursive flag is set to false.
634    *
635    * @param    callback    The PHP callback used for filter
636    * @param    array       Optional arguments for the callback. The first parameter
637    *                       will always be the element value, then these options will
638    *                       be used as parameters for the callback.
639    * @param    bool        Whether to apply the filter recursively to contained elements
640    * @return   HTML_QuickForm2_Node    The element
641    * @throws   HTML_QuickForm2_InvalidArgumentException    If callback is incorrect
642    */
643    public function addFilter($callback, array $options = null, $recursive = true)
644    {
645        if (!is_callable($callback, false, $callbackName)) {
646            throw new HTML_QuickForm2_InvalidArgumentException(
647                'Callback Filter requires a valid callback, \'' . $callbackName .
648                '\' was given'
649            );
650        }
651        $this->filters[] = array($callback, $options, 'recursive' => $recursive);
652        return $this;
653    }
654
655   /**
656    * Removes all element filters
657    */
658    public function removeFilters()
659    {
660        $this->filters = array();
661    }
662
663   /**
664    * Applies element filters on element value
665    * @param    mixed   Element value
666    * @return   mixed   Filtered value
667    */
668    protected function applyFilters($value)
669    {
670        foreach ($this->filters as $filter) {
671            if (is_array($value) && !empty($filter['recursive'])) {
672                array_walk_recursive($value,
673                    array('HTML_QuickForm2_Node', 'applyFilter'), $filter);
674            } else {
675                self::applyFilter($value, null, $filter);
676            }
677        }
678        return $value;
679    }
680
681    protected static function applyFilter(&$value, $key, $filter)
682    {
683        $callback = $filter[0];
684        $options  = $filter[1];
685        if (!is_array($options)) {
686            $options = array();
687        }
688        array_unshift($options, $value);
689        $value = call_user_func_array($callback, $options);
690    }
691
692}
693?>
694