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 <script> 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