1<?php
2/**
3 * Base class for all HTML_QuickForm2 elements
4 *
5 * PHP version 5
6 *
7 * LICENSE
8 *
9 * This source file is subject to BSD 3-Clause License that is bundled
10 * with this package in the file LICENSE and available at the URL
11 * https://raw.githubusercontent.com/pear/HTML_QuickForm2/trunk/docs/LICENSE
12 *
13 * @category  HTML
14 * @package   HTML_QuickForm2
15 * @author    Alexey Borzov <avb@php.net>
16 * @author    Bertrand Mansion <golgote@mamasam.com>
17 * @copyright 2006-2021 Alexey Borzov <avb@php.net>, Bertrand Mansion <golgote@mamasam.com>
18 * @license   https://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License
19 * @link      https://pear.php.net/package/HTML_QuickForm2
20 */
21
22/**
23 * HTML_Common2 - base class for HTML elements
24 */
25require_once 'HTML/Common2.php';
26
27/**
28 * Exception classes for HTML_QuickForm2
29 */
30require_once 'HTML/QuickForm2/Exception.php';
31
32/**
33 * Static factory class for QuickForm2 elements
34 */
35require_once 'HTML/QuickForm2/Factory.php';
36
37/**
38 * Base class for HTML_QuickForm2 rules
39 */
40require_once 'HTML/QuickForm2/Rule.php';
41
42
43/**
44 * Abstract base class for all QuickForm2 Elements and Containers
45 *
46 * This class is mostly here to define the interface that should be implemented
47 * by the subclasses. It also contains static methods handling generation
48 * of unique ids for elements which do not have ids explicitly set.
49 *
50 * @category HTML
51 * @package  HTML_QuickForm2
52 * @author   Alexey Borzov <avb@php.net>
53 * @author   Bertrand Mansion <golgote@mamasam.com>
54 * @license  https://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License
55 * @version  Release: 2.2.2
56 * @link     https://pear.php.net/package/HTML_QuickForm2
57 */
58abstract class HTML_QuickForm2_Node extends HTML_Common2
59{
60    /**
61     * Name of option containing default language for various elements' messages
62     */
63    const OPTION_LANGUAGE = 'language';
64
65    /**
66     * Name of option that toggles always appending a numeric index to generated id values
67     *
68     * By default, we generate element IDs with numeric indexes appended even for
69     * elements with unique names. If you want IDs to be equal to the element
70     * names by default, set this configuration option to false.
71     */
72    const OPTION_ID_FORCE_APPEND_INDEX = 'id_force_append_index';
73
74    /**
75     * Name of option containing a value for "nonce" attribute of generated &lt;script&gt; tags
76     */
77    const OPTION_NONCE = 'nonce';
78
79   /**
80    * Array containing the parts of element ids
81    * @var array
82    */
83    protected static $ids = [];
84
85   /**
86    * Element's "frozen" status
87    * @var boolean
88    */
89    protected $frozen = false;
90
91   /**
92    * Whether element's value should persist when element is frozen
93    * @var boolean
94    */
95    protected $persistent = false;
96
97   /**
98    * Element containing current
99    * @var HTML_QuickForm2_Container
100    */
101    protected $container = null;
102
103   /**
104    * Contains options and data used for the element creation
105    * @var  array
106    */
107    protected $data = [];
108
109   /**
110    * Validation rules for element
111    * @var  array
112    */
113    protected $rules = [];
114
115   /**
116    * An array of callback filters for element
117    * @var  array
118    */
119    protected $filters = [];
120
121   /**
122    * Recursive filter callbacks for element
123    *
124    * These are recursively applied for array values of element or propagated
125    * to contained elements if the element is a Container
126    *
127    * @var  array
128    */
129    protected $recursiveFilters = [];
130
131   /**
132    * Error message (usually set via Rule if validation fails)
133    * @var  string
134    */
135    protected $error = null;
136
137   /**
138    * Changing 'name' and 'id' attributes requires some special handling
139    * @var array
140    */
141    protected $watchedAttributes = ['id', 'name'];
142
143   /**
144    * Intercepts setting 'name' and 'id' attributes
145    *
146    * These attributes should always be present and thus trying to remove them
147    * will result in an exception. Changing their values is delegated to
148    * setName() and setId() methods, respectively
149    *
150    * @param string $name  Attribute name
151    * @param string $value Attribute value, null if attribute is being removed
152    *
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       $name       Element name
181    * @param string|array $attributes HTML attributes (either a string or an array)
182    * @param array        $data       Element data (label, options used for element setup)
183    */
184    public function __construct($name = null, $attributes = null, array $data = [])
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 $elementName Element name
204    *
205    * @return string The generated element id
206    */
207    protected static function generateId($elementName)
208    {
209        $stop      =  !self::getOption(self::OPTION_ID_FORCE_APPEND_INDEX);
210        $tokens    =  strlen($elementName)
211                      ? explode('[', str_replace(']', '', $elementName))
212                      : ($stop? ['qfauto', ''] : ['qfauto']);
213        $container =& self::$ids;
214        $id        =  '';
215
216        do {
217            $token = array_shift($tokens);
218            // prevent generated ids starting with numbers
219            if ('' == $id && is_numeric($token)) {
220                $token = 'qf' . $token;
221            }
222            // Handle the 'array[]' names
223            if ('' === $token) {
224                if (empty($container)) {
225                    $token = 0;
226                } else {
227                    $keys  = array_filter(array_keys($container), 'is_numeric');
228                    $token = empty($keys) ? 0 : end($keys);
229                    while (isset($container[$token])) {
230                        $token++;
231                    }
232                }
233            }
234            $id .= '-' . $token;
235            if (!isset($container[$token])) {
236                $container[$token] = [];
237            // Handle duplicate names when not having mandatory indexes
238            } elseif (empty($tokens) && $stop) {
239                $tokens[] = '';
240            }
241            // Handle mandatory indexes
242            if (empty($tokens) && !$stop) {
243                $tokens[] = '';
244                $stop     = true;
245            }
246            $container =& $container[$token];
247        } while (!empty($tokens));
248
249        return substr($id, 1);
250    }
251
252
253   /**
254    * Stores the explicitly given id to prevent duplicate id generation
255    *
256    * @param string $id Element id
257    */
258    protected static function storeId($id)
259    {
260        $tokens    =  explode('-', $id);
261        $container =& self::$ids;
262
263        do {
264            $token = array_shift($tokens);
265            if (!isset($container[$token])) {
266                $container[$token] = [];
267            }
268            $container =& $container[$token];
269        } while (!empty($tokens));
270    }
271
272
273   /**
274    * Returns the element options
275    *
276    * @return   array
277    */
278    public function getData()
279    {
280        return $this->data;
281    }
282
283
284   /**
285    * Returns the element's type
286    *
287    * @return   string
288    */
289    abstract public function getType();
290
291
292   /**
293    * Returns the element's name
294    *
295    * @return   string
296    */
297    public function getName()
298    {
299        return isset($this->attributes['name'])? $this->attributes['name']: null;
300    }
301
302
303   /**
304    * Sets the element's name
305    *
306    * @param string $name
307    *
308    * @return $this
309    */
310    abstract public function setName($name);
311
312
313   /**
314    * Returns the element's id
315    *
316    * @return   string
317    */
318    public function getId()
319    {
320        return isset($this->attributes['id'])? $this->attributes['id']: null;
321    }
322
323
324   /**
325    * Sets the element's id
326    *
327    * Please note that elements should always have an id in QuickForm2 and
328    * therefore it will not be possible to remove the element's id or set it to
329    * an empty value. If id is not explicitly given, it will be autogenerated.
330    *
331    * @param string $id Element's id, will be autogenerated if not given
332    *
333    * @return   $this
334    * @throws   HTML_QuickForm2_InvalidArgumentException if id contains invalid
335    *           characters (i.e. spaces)
336    */
337    public function setId($id = null)
338    {
339        if (is_null($id)) {
340            $id = self::generateId($this->getName());
341        // HTML5 specification only disallows having space characters in id,
342        // so we don't do stricter checks here
343        } elseif (strpbrk($id, " \r\n\t\x0C")) {
344            throw new HTML_QuickForm2_InvalidArgumentException(
345                "The value of 'id' attribute should not contain space characters"
346            );
347        } else {
348            self::storeId($id);
349        }
350        $this->attributes['id'] = (string)$id;
351        return $this;
352    }
353
354
355   /**
356    * Returns the element's value without filters applied
357    *
358    * @return   mixed
359    */
360    abstract public function getRawValue();
361
362   /**
363    * Returns the element's value, possibly with filters applied
364    *
365    * @return mixed
366    */
367    public function getValue()
368    {
369        $value = $this->getRawValue();
370        return is_null($value)? null: $this->applyFilters($value);
371    }
372
373   /**
374    * Sets the element's value
375    *
376    * @param mixed $value
377    *
378    * @return $this
379    */
380    abstract public function setValue($value);
381
382
383   /**
384    * Returns the element's label(s)
385    *
386    * @return   string|array
387    */
388    public function getLabel()
389    {
390        if (isset($this->data['label'])) {
391            return $this->data['label'];
392        }
393        return null;
394    }
395
396
397   /**
398    * Sets the element's label(s)
399    *
400    * @param string|array $label Label for the element (may be an array of labels)
401    *
402    * @return $this
403    */
404    public function setLabel($label)
405    {
406        $this->data['label'] = $label;
407        return $this;
408    }
409
410
411   /**
412    * Changes the element's frozen status
413    *
414    * @param bool $freeze Whether the element should be frozen or editable. If
415    *                     omitted, the method will not change the frozen status,
416    *                     just return its current value
417    *
418    * @return   bool    Old value of element's frozen status
419    */
420    public function toggleFrozen($freeze = null)
421    {
422        $old = $this->frozen;
423        if (null !== $freeze) {
424            $this->frozen = (bool)$freeze;
425        }
426        return $old;
427    }
428
429
430   /**
431    * Changes the element's persistent freeze behaviour
432    *
433    * If persistent freeze is on, the element's value will be kept (and
434    * submitted) in a hidden field when the element is frozen.
435    *
436    * @param bool $persistent New value for "persistent freeze". If omitted, the
437    *                         method will not set anything, just return the current
438    *                         value of the flag.
439    *
440    * @return   bool    Old value of "persistent freeze" flag
441    */
442    public function persistentFreeze($persistent = null)
443    {
444        $old = $this->persistent;
445        if (null !== $persistent) {
446            $this->persistent = (bool)$persistent;
447        }
448        return $old;
449    }
450
451
452   /**
453    * Adds the link to the element containing current
454    *
455    * @param HTML_QuickForm2_Container $container Element containing
456    *                           the current one, null if the link should
457    *                           really be removed (if removing from container)
458    *
459    * @throws   HTML_QuickForm2_InvalidArgumentException   If trying to set a
460    *                               child of an element as its container
461    */
462    protected function setContainer(HTML_QuickForm2_Container $container = null)
463    {
464        if (null !== $container) {
465            $check = $container;
466            do {
467                if ($this === $check) {
468                    throw new HTML_QuickForm2_InvalidArgumentException(
469                        'Cannot set an element or its child as its own container'
470                    );
471                }
472            } while ($check = $check->getContainer());
473            if (null !== $this->container && $container !== $this->container) {
474                $this->container->removeChild($this);
475            }
476        }
477        $this->container = $container;
478        if (null !== $container) {
479            $this->updateValue();
480        }
481    }
482
483
484   /**
485    * Returns the element containing current
486    *
487    * @return   HTML_QuickForm2_Container|null
488    */
489    public function getContainer()
490    {
491        return $this->container;
492    }
493
494   /**
495    * Returns the data sources for this element
496    *
497    * @return   array
498    */
499    protected function getDataSources()
500    {
501        if (empty($this->container)) {
502            return [];
503        } else {
504            return $this->container->getDataSources();
505        }
506    }
507
508   /**
509    * Called when the element needs to update its value from form's data sources
510    */
511    abstract protected function updateValue();
512
513   /**
514    * Adds a validation rule
515    *
516    * @param HTML_QuickForm2_Rule|string $rule           Validation rule or rule type
517    * @param string|int                  $messageOrRunAt If first parameter is rule type,
518    *            then message to display if validation fails, otherwise constant showing
519    *            whether to perfom validation client-side and/or server-side
520    * @param mixed                       $options        Configuration data for the rule
521    * @param int                         $runAt          Whether to perfom validation
522    *               server-side and/or client side. Combination of
523    *               HTML_QuickForm2_Rule::SERVER and HTML_QuickForm2_Rule::CLIENT constants
524    *
525    * @return   HTML_QuickForm2_Rule            The added rule
526    * @throws   HTML_QuickForm2_InvalidArgumentException    if $rule is of a
527    *               wrong type or rule name isn't registered with Factory
528    * @throws   HTML_QuickForm2_NotFoundException   if class for a given rule
529    *               name cannot be found
530    */
531    public function addRule(
532        $rule, $messageOrRunAt = '', $options = null,
533        $runAt = HTML_QuickForm2_Rule::SERVER
534    ) {
535        if ($rule instanceof HTML_QuickForm2_Rule) {
536            $rule->setOwner($this);
537            $runAt = '' == $messageOrRunAt? HTML_QuickForm2_Rule::SERVER: $messageOrRunAt;
538        } elseif (is_string($rule)) {
539            $rule = HTML_QuickForm2_Factory::createRule($rule, $this, $messageOrRunAt, $options);
540        } else {
541            throw new HTML_QuickForm2_InvalidArgumentException(
542                'addRule() expects either a rule type or ' .
543                'a HTML_QuickForm2_Rule instance'
544            );
545        }
546
547        $this->rules[] = [$rule, $runAt];
548        return $rule;
549    }
550
551   /**
552    * Removes a validation rule
553    *
554    * The method will *not* throw an Exception if the rule wasn't added to the
555    * element.
556    *
557    * @param HTML_QuickForm2_Rule $rule Validation rule to remove
558    *
559    * @return   HTML_QuickForm2_Rule    Removed rule
560    */
561    public function removeRule(HTML_QuickForm2_Rule $rule)
562    {
563        foreach ($this->rules as $i => $r) {
564            if ($r[0] === $rule) {
565                unset($this->rules[$i]);
566                break;
567            }
568        }
569        return $rule;
570    }
571
572   /**
573    * Creates a validation rule
574    *
575    * This method is mostly useful when when chaining several rules together
576    * via {@link HTML_QuickForm2_Rule::and_()} and {@link HTML_QuickForm2_Rule::or_()}
577    * methods:
578    * <code>
579    * $first->addRule('nonempty', 'Fill in either first or second field')
580    *     ->or_($second->createRule('nonempty'));
581    * </code>
582    *
583    * @param string $type    Rule type
584    * @param string $message Message to display if validation fails
585    * @param mixed  $options Configuration data for the rule
586    *
587    * @return   HTML_QuickForm2_Rule    The created rule
588    * @throws   HTML_QuickForm2_InvalidArgumentException If rule type is unknown
589    * @throws   HTML_QuickForm2_NotFoundException        If class for the rule
590    *           can't be found and/or loaded from file
591    */
592    public function createRule($type, $message = '', $options = null)
593    {
594        return HTML_QuickForm2_Factory::createRule($type, $this, $message, $options);
595    }
596
597
598   /**
599    * Checks whether an element is required
600    *
601    * @return   boolean
602    */
603    public function isRequired()
604    {
605        foreach ($this->rules as $rule) {
606            if ($rule[0] instanceof HTML_QuickForm2_Rule_Required) {
607                return true;
608            }
609        }
610        return false;
611    }
612
613   /**
614    * Adds element's client-side validation rules to a builder object
615    *
616    * @param HTML_QuickForm2_JavascriptBuilder $builder
617    */
618    protected function renderClientRules(HTML_QuickForm2_JavascriptBuilder $builder)
619    {
620        if ($this->toggleFrozen()) {
621            return;
622        }
623        $onblur = HTML_QuickForm2_Rule::ONBLUR_CLIENT ^ HTML_QuickForm2_Rule::CLIENT;
624        foreach ($this->rules as $rule) {
625            if ($rule[1] & HTML_QuickForm2_Rule::CLIENT) {
626                $builder->addRule($rule[0], $rule[1] & $onblur);
627            }
628        }
629    }
630
631   /**
632    * Performs the server-side validation
633    *
634    * @return   boolean     Whether the element is valid
635    */
636    protected function validate()
637    {
638        foreach ($this->rules as $rule) {
639            if (strlen($this->error)) {
640                break;
641            }
642            if ($rule[1] & HTML_QuickForm2_Rule::SERVER) {
643                $rule[0]->validate();
644            }
645        }
646        return !strlen($this->error);
647    }
648
649   /**
650    * Sets the error message to the element
651    *
652    * @param string $error
653    *
654    * @return $this
655    */
656    public function setError($error = null)
657    {
658        $this->error = (string)$error;
659        return $this;
660    }
661
662   /**
663    * Returns the error message for the element
664    *
665    * @return   string
666    */
667    public function getError()
668    {
669        return $this->error;
670    }
671
672   /**
673    * Returns Javascript code for getting the element's value
674    *
675    * @param bool $inContainer Whether it should return a parameter for
676    *                          qf.form.getContainerValue()
677    *
678    * @return string
679    */
680    abstract public function getJavascriptValue($inContainer = false);
681
682   /**
683    * Returns IDs of form fields that should trigger "live" Javascript validation
684    *
685    * Rules added to this element with parameter HTML_QuickForm2_Rule::ONBLUR_CLIENT
686    * will be run by after these form elements change or lose focus
687    *
688    * @return array
689    */
690    abstract public function getJavascriptTriggers();
691
692    /**
693     * Adds a filter
694     *
695     * A filter is simply a PHP callback which will be applied to the element value
696     * when getValue() is called.
697     *
698     * @param callback $callback The PHP callback used for filter
699     * @param array    $options  Optional arguments for the callback. The first parameter
700     *                       will always be the element value, then these options will
701     *                       be used as parameters for the callback.
702     *
703     * @return   $this    The element
704     * @throws   HTML_QuickForm2_InvalidArgumentException    If callback is incorrect
705     */
706    public function addFilter($callback, array $options = [])
707    {
708        if (!is_callable($callback, false, $callbackName)) {
709            throw new HTML_QuickForm2_InvalidArgumentException(
710                "Filter should be a valid callback, '{$callbackName}' was given"
711            );
712        }
713        $this->filters[] = [$callback, $options];
714        return $this;
715    }
716
717    /**
718     * Adds a recursive filter
719     *
720     * A filter is simply a PHP callback which will be applied to the element value
721     * when getValue() is called. If the element value is an array, for example with
722     * selects of type 'multiple', the filter is applied to all values recursively.
723     * A filter on a container will not be applied on a container value but
724     * propagated to all contained elements instead.
725     *
726     * If the element is not a container and its value is not an array the behaviour
727     * will be identical to filters added via addFilter().
728     *
729     * @param callback $callback The PHP callback used for filter
730     * @param array    $options  Optional arguments for the callback. The first parameter
731     *                       will always be the element value, then these options will
732     *                       be used as parameters for the callback.
733     *
734     * @return   $this    The element
735     * @throws   HTML_QuickForm2_InvalidArgumentException    If callback is incorrect
736     */
737    public function addRecursiveFilter($callback, array $options = [])
738    {
739        if (!is_callable($callback, false, $callbackName)) {
740            throw new HTML_QuickForm2_InvalidArgumentException(
741                "Filter should be a valid callback, '{$callbackName}' was given"
742            );
743        }
744        $this->recursiveFilters[] = [$callback, $options];
745        return $this;
746    }
747
748   /**
749    * Helper function for applying filter callback to a value
750    *
751    * @param mixed &$value Value being filtered
752    * @param mixed $key    Array key (not used, present to be able to use this
753    *                      method as a callback to array_walk_recursive())
754    * @param array $filter Array containing callback and additional callback
755    *                      parameters
756    */
757    protected static function applyFilter(&$value, $key, $filter)
758    {
759        list($callback, $options) = $filter;
760        array_unshift($options, $value);
761        $value = call_user_func_array($callback, $options);
762    }
763
764    /**
765     * Applies non-recursive filters on element value
766     *
767     * @param mixed $value Element value
768     *
769     * @return   mixed   Filtered value
770     */
771    protected function applyFilters($value)
772    {
773        foreach ($this->filters as $filter) {
774            self::applyFilter($value, null, $filter);
775        }
776        return $value;
777    }
778
779   /**
780    * Renders the element using the given renderer
781    *
782    * @param HTML_QuickForm2_Renderer $renderer
783    * @return   HTML_QuickForm2_Renderer
784    */
785    abstract public function render(HTML_QuickForm2_Renderer $renderer);
786}
787
788// set default values for document-wide options
789if (null === HTML_Common2::getOption(HTML_QuickForm2_Node::OPTION_ID_FORCE_APPEND_INDEX)) {
790    HTML_Common2::setOption(HTML_QuickForm2_Node::OPTION_ID_FORCE_APPEND_INDEX, true);
791}
792if (null === HTML_Common2::getOption(HTML_QuickForm2_Node::OPTION_LANGUAGE)) {
793    HTML_Common2::setOption(HTML_QuickForm2_Node::OPTION_LANGUAGE, 'en');
794}
795?>
796