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