1<?php 2/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ 3 4namespace Icinga\Web; 5 6use Icinga\Web\Form\Element\DateTimePicker; 7use Zend_Config; 8use Zend_Form; 9use Zend_Form_Element; 10use Zend_View_Interface; 11use Icinga\Application\Icinga; 12use Icinga\Authentication\Auth; 13use Icinga\Exception\ProgrammingError; 14use Icinga\Security\SecurityException; 15use Icinga\Util\Translator; 16use Icinga\Web\Form\ErrorLabeller; 17use Icinga\Web\Form\Decorator\Autosubmit; 18use Icinga\Web\Form\Element\CsrfCounterMeasure; 19 20/** 21 * Base class for forms providing CSRF protection, confirmation logic and auto submission 22 * 23 * @method \Zend_Form_Element[] getElements() { 24 * {@inheritdoc} 25 * @return \Zend_Form_Element[] 26 * } 27 */ 28class Form extends Zend_Form 29{ 30 /** 31 * The suffix to append to a field's hidden default field name 32 */ 33 const DEFAULT_SUFFIX = '_default'; 34 35 /** 36 * A form's default CSS classes 37 */ 38 const DEFAULT_CLASSES = 'icinga-form icinga-controls'; 39 40 /** 41 * Identifier for notifications of type error 42 */ 43 const NOTIFICATION_ERROR = 0; 44 45 /** 46 * Identifier for notifications of type warning 47 */ 48 const NOTIFICATION_WARNING = 1; 49 50 /** 51 * Identifier for notifications of type info 52 */ 53 const NOTIFICATION_INFO = 2; 54 55 /** 56 * Whether this form has been created 57 * 58 * @var bool 59 */ 60 protected $created = false; 61 62 /** 63 * This form's parent 64 * 65 * Gets automatically set upon calling addSubForm(). 66 * 67 * @var Form 68 */ 69 protected $_parent; 70 71 /** 72 * Whether the form is an API target 73 * 74 * When the form is an API target, the form evaluates as submitted if the request method equals the form method. 75 * That means, that the submit button and form identification are not taken into account. In addition, the CSRF 76 * counter measure will not be added to the form's elements. 77 * 78 * @var bool 79 */ 80 protected $isApiTarget = false; 81 82 /** 83 * The request associated with this form 84 * 85 * @var Request 86 */ 87 protected $request; 88 89 /** 90 * The callback to call instead of Form::onSuccess() 91 * 92 * @var callable 93 */ 94 protected $onSuccess; 95 96 /** 97 * Label to use for the standard submit button 98 * 99 * @var string 100 */ 101 protected $submitLabel; 102 103 /** 104 * Label to use for showing the user an activity indicator when submitting the form 105 * 106 * @var string 107 */ 108 protected $progressLabel; 109 110 /** 111 * The url to redirect to upon success 112 * 113 * @var Url 114 */ 115 protected $redirectUrl; 116 117 /** 118 * The view script to use when rendering this form 119 * 120 * @var string 121 */ 122 protected $viewScript; 123 124 /** 125 * Whether this form should NOT add random generated "challenge" tokens that are associated with the user's current 126 * session in order to prevent Cross-Site Request Forgery (CSRF). It is the form's responsibility to verify the 127 * existence and correctness of this token 128 * 129 * @var bool 130 */ 131 protected $tokenDisabled = false; 132 133 /** 134 * Name of the CSRF token element 135 * 136 * @var string 137 */ 138 protected $tokenElementName = 'CSRFToken'; 139 140 /** 141 * Whether this form should add a UID element being used to distinct different forms posting to the same action 142 * 143 * @var bool 144 */ 145 protected $uidDisabled = false; 146 147 /** 148 * Name of the form identification element 149 * 150 * @var string 151 */ 152 protected $uidElementName = 'formUID'; 153 154 /** 155 * Whether the form should validate the sent data when being automatically submitted 156 * 157 * @var bool 158 */ 159 protected $validatePartial = false; 160 161 /** 162 * Whether element ids will be protected against collisions by appending a request-specific unique identifier 163 * 164 * @var bool 165 */ 166 protected $protectIds = true; 167 168 /** 169 * The cue that is appended to each element's label if it's required 170 * 171 * @var string 172 */ 173 protected $requiredCue = '*'; 174 175 /** 176 * The descriptions of this form 177 * 178 * @var array 179 */ 180 protected $descriptions; 181 182 /** 183 * The notifications of this form 184 * 185 * @var array 186 */ 187 protected $notifications; 188 189 /** 190 * The hints of this form 191 * 192 * @var array 193 */ 194 protected $hints; 195 196 /** 197 * Whether the Autosubmit decorator should be applied to this form 198 * 199 * If this is true, the Autosubmit decorator is being applied to this form instead of to each of its elements. 200 * 201 * @var bool 202 */ 203 protected $useFormAutosubmit = false; 204 205 /** 206 * Authentication manager 207 * 208 * @var Auth|null 209 */ 210 private $auth; 211 212 /** 213 * Default element decorators 214 * 215 * @var array 216 */ 217 public static $defaultElementDecorators = array( 218 array('Label', array('tag'=>'span', 'separator' => '', 'class' => 'control-label')), 219 array(array('labelWrap' => 'HtmlTag'), array('tag' => 'div', 'class' => 'control-label-group')), 220 array('ViewHelper', array('separator' => '')), 221 array('Help', array()), 222 array('Errors', array('separator' => '')), 223 array('HtmlTag', array('tag' => 'div', 'class' => 'control-group')) 224 ); 225 226 /** 227 * (non-PHPDoc) 228 * @see \Zend_Form::construct() For the method documentation. 229 */ 230 public function __construct($options = null) 231 { 232 // Zend's plugin loader reverses the order of added prefix paths thus trying our paths first before trying 233 // Zend paths 234 $this->addPrefixPaths(array( 235 array( 236 'prefix' => 'Icinga\\Web\\Form\\Element\\', 237 'path' => Icinga::app()->getLibraryDir('Icinga/Web/Form/Element'), 238 'type' => static::ELEMENT 239 ), 240 array( 241 'prefix' => 'Icinga\\Web\\Form\\Decorator\\', 242 'path' => Icinga::app()->getLibraryDir('Icinga/Web/Form/Decorator'), 243 'type' => static::DECORATOR 244 ) 245 )); 246 247 if (! isset($options['attribs']['class'])) { 248 $options['attribs']['class'] = static::DEFAULT_CLASSES; 249 } 250 251 parent::__construct($options); 252 } 253 254 /** 255 * Set this form's parent 256 * 257 * @param Form $form 258 * 259 * @return $this 260 */ 261 public function setParent(Form $form) 262 { 263 $this->_parent = $form; 264 return $this; 265 } 266 267 /** 268 * Return this form's parent 269 * 270 * @return Form 271 */ 272 public function getParent() 273 { 274 return $this->_parent; 275 } 276 277 /** 278 * Set a callback that is called instead of this form's onSuccess method 279 * 280 * It is called using the following signature: (Form $this). 281 * 282 * @param callable $onSuccess Callback 283 * 284 * @return $this 285 * 286 * @throws ProgrammingError If the callback is not callable 287 */ 288 public function setOnSuccess($onSuccess) 289 { 290 if (! is_callable($onSuccess)) { 291 throw new ProgrammingError('The option `onSuccess\' is not callable'); 292 } 293 $this->onSuccess = $onSuccess; 294 return $this; 295 } 296 297 /** 298 * Set the label to use for the standard submit button 299 * 300 * @param string $label The label to use for the submit button 301 * 302 * @return $this 303 */ 304 public function setSubmitLabel($label) 305 { 306 $this->submitLabel = $label; 307 return $this; 308 } 309 310 /** 311 * Return the label being used for the standard submit button 312 * 313 * @return string 314 */ 315 public function getSubmitLabel() 316 { 317 return $this->submitLabel; 318 } 319 320 /** 321 * Set the label to use for showing the user an activity indicator when submitting the form 322 * 323 * @param string $label 324 * 325 * @return $this 326 */ 327 public function setProgressLabel($label) 328 { 329 $this->progressLabel = $label; 330 return $this; 331 } 332 333 /** 334 * Return the label to use for showing the user an activity indicator when submitting the form 335 * 336 * @return string 337 */ 338 public function getProgressLabel() 339 { 340 return $this->progressLabel; 341 } 342 343 /** 344 * Set the url to redirect to upon success 345 * 346 * @param string|Url $url The url to redirect to 347 * 348 * @return $this 349 * 350 * @throws ProgrammingError In case $url is neither a string nor a instance of Icinga\Web\Url 351 */ 352 public function setRedirectUrl($url) 353 { 354 if (is_string($url)) { 355 $url = Url::fromPath($url, array(), $this->getRequest()); 356 } elseif (! $url instanceof Url) { 357 throw new ProgrammingError('$url must be a string or instance of Icinga\Web\Url'); 358 } 359 360 $this->redirectUrl = $url; 361 return $this; 362 } 363 364 /** 365 * Return the url to redirect to upon success 366 * 367 * @return Url 368 */ 369 public function getRedirectUrl() 370 { 371 if ($this->redirectUrl === null) { 372 $this->redirectUrl = $this->getRequest()->getUrl(); 373 if ($this->getMethod() === 'get') { 374 // Be sure to remove all form dependent params because we do not want to submit it again 375 $this->redirectUrl = $this->redirectUrl->without(array_keys($this->getElements())); 376 } 377 } 378 379 return $this->redirectUrl; 380 } 381 382 /** 383 * Set the view script to use when rendering this form 384 * 385 * @param string $viewScript The view script to use 386 * 387 * @return $this 388 */ 389 public function setViewScript($viewScript) 390 { 391 $this->viewScript = $viewScript; 392 return $this; 393 } 394 395 /** 396 * Return the view script being used when rendering this form 397 * 398 * @return string 399 */ 400 public function getViewScript() 401 { 402 return $this->viewScript; 403 } 404 405 /** 406 * Disable CSRF counter measure and remove its field if already added 407 * 408 * @param bool $disabled Set true in order to disable CSRF protection for this form, otherwise false 409 * 410 * @return $this 411 */ 412 public function setTokenDisabled($disabled = true) 413 { 414 $this->tokenDisabled = (bool) $disabled; 415 416 if ($disabled && $this->getElement($this->tokenElementName) !== null) { 417 $this->removeElement($this->tokenElementName); 418 } 419 420 return $this; 421 } 422 423 /** 424 * Return whether CSRF counter measures are disabled for this form 425 * 426 * @return bool 427 */ 428 public function getTokenDisabled() 429 { 430 return $this->tokenDisabled; 431 } 432 433 /** 434 * Set the name to use for the CSRF element 435 * 436 * @param string $name The name to set 437 * 438 * @return $this 439 */ 440 public function setTokenElementName($name) 441 { 442 $this->tokenElementName = $name; 443 return $this; 444 } 445 446 /** 447 * Return the name of the CSRF element 448 * 449 * @return string 450 */ 451 public function getTokenElementName() 452 { 453 return $this->tokenElementName; 454 } 455 456 /** 457 * Disable form identification and remove its field if already added 458 * 459 * @param bool $disabled Set true in order to disable identification for this form, otherwise false 460 * 461 * @return $this 462 */ 463 public function setUidDisabled($disabled = true) 464 { 465 $this->uidDisabled = (bool) $disabled; 466 467 if ($disabled && $this->getElement($this->uidElementName) !== null) { 468 $this->removeElement($this->uidElementName); 469 } 470 471 return $this; 472 } 473 474 /** 475 * Return whether identification is disabled for this form 476 * 477 * @return bool 478 */ 479 public function getUidDisabled() 480 { 481 return $this->uidDisabled; 482 } 483 484 /** 485 * Set the name to use for the form identification element 486 * 487 * @param string $name The name to set 488 * 489 * @return $this 490 */ 491 public function setUidElementName($name) 492 { 493 $this->uidElementName = $name; 494 return $this; 495 } 496 497 /** 498 * Return the name of the form identification element 499 * 500 * @return string 501 */ 502 public function getUidElementName() 503 { 504 return $this->uidElementName; 505 } 506 507 /** 508 * Set whether this form should validate the sent data when being automatically submitted 509 * 510 * @param bool $state 511 * 512 * @return $this 513 */ 514 public function setValidatePartial($state) 515 { 516 $this->validatePartial = $state; 517 return $this; 518 } 519 520 /** 521 * Return whether this form should validate the sent data when being automatically submitted 522 * 523 * @return bool 524 */ 525 public function getValidatePartial() 526 { 527 return $this->validatePartial; 528 } 529 530 /** 531 * Set whether each element's id should be altered to avoid duplicates 532 * 533 * @param bool $value 534 * 535 * @return Form 536 */ 537 public function setProtectIds($value = true) 538 { 539 $this->protectIds = (bool) $value; 540 return $this; 541 } 542 543 /** 544 * Return whether each element's id is being altered to avoid duplicates 545 * 546 * @return bool 547 */ 548 public function getProtectIds() 549 { 550 return $this->protectIds; 551 } 552 553 /** 554 * Set the cue to append to each element's label if it's required 555 * 556 * @param string $cue 557 * 558 * @return Form 559 */ 560 public function setRequiredCue($cue) 561 { 562 $this->requiredCue = $cue; 563 return $this; 564 } 565 566 /** 567 * Return the cue being appended to each element's label if it's required 568 * 569 * @return string 570 */ 571 public function getRequiredCue() 572 { 573 return $this->requiredCue; 574 } 575 576 /** 577 * Set the descriptions for this form 578 * 579 * @param array $descriptions 580 * 581 * @return Form 582 */ 583 public function setDescriptions(array $descriptions) 584 { 585 $this->descriptions = $descriptions; 586 return $this; 587 } 588 589 /** 590 * Add a description for this form 591 * 592 * If $description is an array the second value should be 593 * an array as well containing additional HTML properties. 594 * 595 * @param string|array $description 596 * 597 * @return Form 598 */ 599 public function addDescription($description) 600 { 601 $this->descriptions[] = $description; 602 return $this; 603 } 604 605 /** 606 * Return the descriptions of this form 607 * 608 * @return array 609 */ 610 public function getDescriptions() 611 { 612 if ($this->descriptions === null) { 613 return array(); 614 } 615 616 return $this->descriptions; 617 } 618 619 /** 620 * Set the notifications for this form 621 * 622 * @param array $notifications 623 * 624 * @return $this 625 */ 626 public function setNotifications(array $notifications) 627 { 628 $this->notifications = $notifications; 629 return $this; 630 } 631 632 /** 633 * Add a notification for this form 634 * 635 * If $notification is an array the second value should be 636 * an array as well containing additional HTML properties. 637 * 638 * @param string|array $notification 639 * @param int $type 640 * 641 * @return $this 642 */ 643 public function addNotification($notification, $type) 644 { 645 $this->notifications[$type][] = $notification; 646 return $this; 647 } 648 649 /** 650 * Return the notifications of this form 651 * 652 * @return array 653 */ 654 public function getNotifications() 655 { 656 if ($this->notifications === null) { 657 return array(); 658 } 659 660 return $this->notifications; 661 } 662 663 /** 664 * Set the hints for this form 665 * 666 * @param array $hints 667 * 668 * @return $this 669 */ 670 public function setHints(array $hints) 671 { 672 $this->hints = $hints; 673 return $this; 674 } 675 676 /** 677 * Add a hint for this form 678 * 679 * If $hint is an array the second value should be an 680 * array as well containing additional HTML properties. 681 * 682 * @param string|array $hint 683 * 684 * @return $this 685 */ 686 public function addHint($hint) 687 { 688 $this->hints[] = $hint; 689 return $this; 690 } 691 692 /** 693 * Return the hints of this form 694 * 695 * @return array 696 */ 697 public function getHints() 698 { 699 if ($this->hints === null) { 700 return array(); 701 } 702 703 return $this->hints; 704 } 705 706 /** 707 * Set whether the Autosubmit decorator should be applied to this form 708 * 709 * If true, the Autosubmit decorator is being applied to this form instead of to each of its elements. 710 * 711 * @param bool $state 712 * 713 * @return Form 714 */ 715 public function setUseFormAutosubmit($state = true) 716 { 717 $this->useFormAutosubmit = (bool) $state; 718 if ($this->useFormAutosubmit) { 719 $this->setAttrib('data-progress-element', 'header-' . $this->getId()); 720 } else { 721 $this->removeAttrib('data-progress-element'); 722 } 723 724 return $this; 725 } 726 727 /** 728 * Return whether the Autosubmit decorator is being applied to this form 729 * 730 * @return bool 731 */ 732 public function getUseFormAutosubmit() 733 { 734 return $this->useFormAutosubmit; 735 } 736 737 /** 738 * Get whether the form is an API target 739 * 740 * @return bool 741 */ 742 public function getIsApiTarget() 743 { 744 return $this->isApiTarget; 745 } 746 747 /** 748 * Set whether the form is an API target 749 * 750 * @param bool $isApiTarget 751 * 752 * @return $this 753 */ 754 public function setIsApiTarget($isApiTarget = true) 755 { 756 $this->isApiTarget = (bool) $isApiTarget; 757 return $this; 758 } 759 760 /** 761 * Create this form 762 * 763 * @param array $formData The data sent by the user 764 * 765 * @return $this 766 */ 767 public function create(array $formData = array()) 768 { 769 if (! $this->created) { 770 $this->createElements($formData); 771 $this->addFormIdentification() 772 ->addCsrfCounterMeasure() 773 ->addSubmitButton(); 774 775 // Use Form::getAttrib() instead of Form::getAction() here because we want to explicitly check against 776 // null. Form::getAction() would return the empty string '' if the action is not set. 777 // For not setting the action attribute use Form::setAction(''). This is required for for the 778 // accessibility's enable/disable auto-refresh mechanic 779 if ($this->getAttrib('action') === null) { 780 $action = $this->getRequest()->getUrl(); 781 if ($this->getMethod() === 'get') { 782 $action = $action->without(array_keys($this->getElements())); 783 } 784 785 // TODO(el): Re-evalute this necessity. 786 // JavaScript could use the container'sURL if there's no action set. 787 // We MUST set an action as JS gets confused otherwise, if 788 // this form is being displayed in an additional column 789 $this->setAction($action); 790 } 791 792 $this->created = true; 793 } 794 795 return $this; 796 } 797 798 /** 799 * Create and add elements to this form 800 * 801 * Intended to be implemented by concrete form classes. 802 * 803 * @param array $formData The data sent by the user 804 */ 805 public function createElements(array $formData) 806 { 807 } 808 809 /** 810 * Perform actions after this form was submitted using a valid request 811 * 812 * Intended to be implemented by concrete form classes. The base implementation returns always FALSE. 813 * 814 * @return null|bool Return FALSE in case no redirect should take place 815 */ 816 public function onSuccess() 817 { 818 return false; 819 } 820 821 /** 822 * Perform actions when no form dependent data was sent 823 * 824 * Intended to be implemented by concrete form classes. 825 */ 826 public function onRequest() 827 { 828 } 829 830 /** 831 * Add a submit button to this form 832 * 833 * Uses the label previously set with Form::setSubmitLabel(). Overwrite this 834 * method in order to add multiple submit buttons or one with a custom name. 835 * 836 * @return $this 837 */ 838 public function addSubmitButton() 839 { 840 $submitLabel = $this->getSubmitLabel(); 841 if ($submitLabel) { 842 $this->addElement( 843 'submit', 844 'btn_submit', 845 array( 846 'class' => 'btn-primary', 847 'ignore' => true, 848 'label' => $submitLabel, 849 'data-progress-label' => $this->getProgressLabel(), 850 'decorators' => array( 851 'ViewHelper', 852 array('Spinner', array('separator' => '')), 853 array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls')) 854 ) 855 ) 856 ); 857 } 858 859 return $this; 860 } 861 862 /** 863 * Add a subform 864 * 865 * @param Zend_Form $form The subform to add 866 * @param string $name The name of the subform or null to use the name of $form 867 * @param int $order The location where to insert the form 868 * 869 * @return Zend_Form 870 */ 871 public function addSubForm(Zend_Form $form, $name = null, $order = null) 872 { 873 if ($form instanceof self) { 874 $form->setDecorators(array('FormElements')); // TODO: Makes it difficult to customise subform decorators.. 875 $form->setSubmitLabel(''); 876 $form->setTokenDisabled(); 877 $form->setUidDisabled(); 878 $form->setParent($this); 879 } 880 881 if ($name === null) { 882 $name = $form->getName(); 883 } 884 885 return parent::addSubForm($form, $name, $order); 886 } 887 888 /** 889 * Create a new element 890 * 891 * Icinga Web 2 loads its own default element decorators. For loading Zend's default element decorators set the 892 * `disableLoadDefaultDecorators' option to any other value than `true'. For loading custom element decorators use 893 * the 'decorators' option. 894 * 895 * @param string $type The type of the element 896 * @param string $name The name of the element 897 * @param mixed $options The options for the element 898 * 899 * @return Zend_Form_Element 900 * 901 * @see Form::$defaultElementDecorators For Icinga Web 2's default element decorators. 902 */ 903 public function createElement($type, $name, $options = null) 904 { 905 if ($options !== null) { 906 if ($options instanceof Zend_Config) { 907 $options = $options->toArray(); 908 } 909 if (! isset($options['decorators']) 910 && ! array_key_exists('disabledLoadDefaultDecorators', $options) 911 ) { 912 $options['decorators'] = static::$defaultElementDecorators; 913 if (! isset($options['data-progress-label']) && ($type === 'submit' 914 || ($type === 'button' && isset($options['type']) && $options['type'] === 'submit')) 915 ) { 916 array_splice($options['decorators'], 1, 0, array(array('Spinner', array('separator' => '')))); 917 } elseif ($type === 'hidden') { 918 $options['decorators'] = array('ViewHelper'); 919 } 920 } 921 } else { 922 $options = array('decorators' => static::$defaultElementDecorators); 923 if ($type === 'submit') { 924 array_splice($options['decorators'], 1, 0, array(array('Spinner', array('separator' => '')))); 925 } elseif ($type === 'hidden') { 926 $options['decorators'] = array('ViewHelper'); 927 } 928 } 929 930 $el = parent::createElement($type, $name, $options); 931 $el->setTranslator(new ErrorLabeller(array('element' => $el))); 932 933 $el->addPrefixPaths(array( 934 array( 935 'prefix' => 'Icinga\\Web\\Form\\Validator\\', 936 'path' => Icinga::app()->getLibraryDir('Icinga/Web/Form/Validator'), 937 'type' => $el::VALIDATE 938 ) 939 )); 940 941 if ($this->protectIds) { 942 $el->setAttrib('id', $this->getRequest()->protectId($this->getId(false) . '_' . $el->getId())); 943 } 944 945 if ($el->getAttrib('autosubmit')) { 946 if ($this->getUseFormAutosubmit()) { 947 $warningId = 'autosubmit_warning_' . $el->getId(); 948 $warningText = $this->getView()->escape($this->translate( 949 'This page will be automatically updated upon change of the value' 950 )); 951 $autosubmitDecorator = $this->_getDecorator('Callback', array( 952 'placement' => 'PREPEND', 953 'callback' => function ($content) use ($warningId, $warningText) { 954 return '<span class="sr-only" id="' . $warningId . '">' . $warningText . '</span>'; 955 } 956 )); 957 } else { 958 $autosubmitDecorator = new Autosubmit(); 959 $autosubmitDecorator->setAccessible(); 960 $warningId = $autosubmitDecorator->getWarningId($el); 961 } 962 963 $decorators = $el->getDecorators(); 964 $pos = array_search('Zend_Form_Decorator_ViewHelper', array_keys($decorators), true) + 1; 965 $el->setDecorators( 966 array_slice($decorators, 0, $pos, true) 967 + array('autosubmit' => $autosubmitDecorator) 968 + array_slice($decorators, $pos, count($decorators) - $pos, true) 969 ); 970 971 if (($describedBy = $el->getAttrib('aria-describedby')) !== null) { 972 $el->setAttrib('aria-describedby', $describedBy . ' ' . $warningId); 973 } else { 974 $el->setAttrib('aria-describedby', $warningId); 975 } 976 977 $class = $el->getAttrib('class'); 978 if (is_array($class)) { 979 $class[] = 'autosubmit'; 980 } elseif ($class === null) { 981 $class = 'autosubmit'; 982 } else { 983 $class .= ' autosubmit'; 984 } 985 $el->setAttrib('class', $class); 986 987 unset($el->autosubmit); 988 } 989 990 if ($el->getAttrib('preserveDefault')) { 991 $el->addDecorator( 992 array('preserveDefault' => 'HtmlTag'), 993 array( 994 'tag' => 'input', 995 'type' => 'hidden', 996 'name' => $name . static::DEFAULT_SUFFIX, 997 'value' => $el instanceof DateTimePicker 998 ? $el->getValue()->format($el->getFormat()) 999 : $el->getValue() 1000 ) 1001 ); 1002 1003 unset($el->preserveDefault); 1004 } 1005 1006 return $this->ensureElementAccessibility($el); 1007 } 1008 1009 /** 1010 * Add accessibility related attributes 1011 * 1012 * @param Zend_Form_Element $element 1013 * 1014 * @return Zend_Form_Element 1015 */ 1016 public function ensureElementAccessibility(Zend_Form_Element $element) 1017 { 1018 if ($element->isRequired()) { 1019 $element->setAttrib('aria-required', 'true'); // ARIA 1020 $element->setAttrib('required', ''); // HTML5 1021 if (($cue = $this->getRequiredCue()) !== null && ($label = $element->getDecorator('label')) !== false) { 1022 $element->setLabel($this->getView()->escape($element->getLabel())); 1023 $label->setOption('escape', false); 1024 $label->setRequiredSuffix(sprintf(' <span aria-hidden="true">%s</span>', $cue)); 1025 } 1026 } 1027 1028 if ($element->getDescription() !== null && ($help = $element->getDecorator('help')) !== false) { 1029 if (($describedBy = $element->getAttrib('aria-describedby')) !== null) { 1030 // Assume that it's because of the element being of type autosubmit or 1031 // that one who did set the property manually removes the help decorator 1032 // in case it has already an aria-describedby property set 1033 $element->setAttrib( 1034 'aria-describedby', 1035 $help->setAccessible()->getDescriptionId($element) . ' ' . $describedBy 1036 ); 1037 } else { 1038 $element->setAttrib('aria-describedby', $help->setAccessible()->getDescriptionId($element)); 1039 } 1040 } 1041 1042 return $element; 1043 } 1044 1045 /** 1046 * Add a field with a unique and form specific ID 1047 * 1048 * @return $this 1049 */ 1050 public function addFormIdentification() 1051 { 1052 if (! $this->uidDisabled && $this->getElement($this->uidElementName) === null) { 1053 $this->addElement( 1054 'hidden', 1055 $this->uidElementName, 1056 array( 1057 'ignore' => true, 1058 'value' => $this->getName(), 1059 'decorators' => array('ViewHelper') 1060 ) 1061 ); 1062 } 1063 1064 return $this; 1065 } 1066 1067 /** 1068 * Add CSRF counter measure field to this form 1069 * 1070 * @return $this 1071 */ 1072 public function addCsrfCounterMeasure() 1073 { 1074 if (! $this->tokenDisabled) { 1075 $request = $this->getRequest(); 1076 if (! $request->isXmlHttpRequest() 1077 && ($this->getIsApiTarget() || $request->isApiRequest()) 1078 ) { 1079 return $this; 1080 } 1081 if ($this->getElement($this->tokenElementName) === null) { 1082 $this->addElement(new CsrfCounterMeasure($this->tokenElementName)); 1083 } 1084 } 1085 return $this; 1086 } 1087 1088 /** 1089 * {@inheritdoc} 1090 * 1091 * Creates the form if not created yet. 1092 * 1093 * @param array $values 1094 * 1095 * @return $this 1096 */ 1097 public function setDefaults(array $values) 1098 { 1099 $this->create($values); 1100 return parent::setDefaults($values); 1101 } 1102 1103 /** 1104 * Populate the elements with the given values 1105 * 1106 * @param array $defaults The values to populate the elements with 1107 * 1108 * @return $this 1109 */ 1110 public function populate(array $defaults) 1111 { 1112 $this->create($defaults); 1113 $this->preserveDefaults($this, $defaults); 1114 return parent::populate($defaults); 1115 } 1116 1117 /** 1118 * Recurse the given form and unset all unchanged default values 1119 * 1120 * @param Zend_Form $form 1121 * @param array $defaults 1122 */ 1123 protected function preserveDefaults(Zend_Form $form, array &$defaults) 1124 { 1125 foreach ($form->getElements() as $name => $element) { 1126 if ((array_key_exists($name, $defaults) 1127 && array_key_exists($name . static::DEFAULT_SUFFIX, $defaults) 1128 && $defaults[$name] === $defaults[$name . static::DEFAULT_SUFFIX]) 1129 || $element->getAttrib('disabled') 1130 ) { 1131 unset($defaults[$name]); 1132 } 1133 } 1134 1135 foreach ($form->getSubForms() as $_ => $subForm) { 1136 $this->preserveDefaults($subForm, $defaults); 1137 } 1138 } 1139 1140 /** 1141 * Process the given request using this form 1142 * 1143 * Redirects to the url set with setRedirectUrl() upon success. See onSuccess() 1144 * and onRequest() wherewith you can customize the processing logic. 1145 * 1146 * @param Request $request The request to be processed 1147 * 1148 * @return Request The request supposed to be processed 1149 */ 1150 public function handleRequest(Request $request = null) 1151 { 1152 if ($request === null) { 1153 $request = $this->getRequest(); 1154 } else { 1155 $this->request = $request; 1156 } 1157 1158 $formData = $this->getRequestData(); 1159 if ($this->getIsApiTarget() 1160 || $this->getRequest()->isApiRequest() 1161 || $this->getUidDisabled() 1162 || $this->wasSent($formData) 1163 ) { 1164 if (($frameUpload = (bool) $request->getUrl()->shift('_frameUpload', false))) { 1165 $this->getView()->layout()->setLayout('wrapped'); 1166 } 1167 $this->populate($formData); // Necessary to get isSubmitted() to work 1168 if (! $this->getSubmitLabel() || $this->isSubmitted()) { 1169 if ($this->isValid($formData) 1170 && (($this->onSuccess !== null && false !== call_user_func($this->onSuccess, $this)) 1171 || ($this->onSuccess === null && false !== $this->onSuccess())) 1172 ) { 1173 if ($this->getIsApiTarget() || $this->getRequest()->isApiRequest()) { 1174 // API targets and API requests will never redirect but immediately respond w/ JSON-encoded 1175 // notifications 1176 $notifications = Notification::getInstance()->popMessages(); 1177 $message = null; 1178 foreach ($notifications as $notification) { 1179 if ($notification->type === Notification::SUCCESS) { 1180 $message = $notification->message; 1181 break; 1182 } 1183 } 1184 $this->getResponse()->json() 1185 ->setSuccessData($message !== null ? array('message' => $message) : null) 1186 ->sendResponse(); 1187 } elseif (! $frameUpload) { 1188 $this->getResponse()->redirectAndExit($this->getRedirectUrl()); 1189 } else { 1190 $this->getView()->layout()->redirectUrl = $this->getRedirectUrl()->getAbsoluteUrl(); 1191 } 1192 } elseif ($this->getIsApiTarget() || $this->getRequest()->isApiRequest()) { 1193 $this->getResponse()->json()->setFailData($this->getMessages())->sendResponse(); 1194 } 1195 } elseif ($this->getValidatePartial()) { 1196 // The form can't be processed but we may want to show validation errors though 1197 $this->isValidPartial($formData); 1198 } 1199 } else { 1200 $this->onRequest(); 1201 } 1202 1203 return $request; 1204 } 1205 1206 /** 1207 * Return whether the submit button of this form was pressed 1208 * 1209 * When overwriting Form::addSubmitButton() be sure to overwrite this method as well. 1210 * 1211 * @return bool True in case it was pressed, False otherwise or no submit label was set 1212 */ 1213 public function isSubmitted() 1214 { 1215 if (strtolower($this->getRequest()->getMethod()) !== $this->getMethod()) { 1216 return false; 1217 } 1218 if ($this->getIsApiTarget() || $this->getRequest()->isApiRequest()) { 1219 return true; 1220 } 1221 if ($this->getSubmitLabel()) { 1222 return $this->getElement('btn_submit')->isChecked(); 1223 } 1224 1225 return false; 1226 } 1227 1228 /** 1229 * Return whether the data sent by the user refers to this form 1230 * 1231 * Ensures that the correct form gets processed in case there are multiple forms 1232 * with equal submit button names being posted against the same route. 1233 * 1234 * @param array $formData The data sent by the user 1235 * 1236 * @return bool Whether the given data refers to this form 1237 */ 1238 public function wasSent(array $formData) 1239 { 1240 return isset($formData[$this->uidElementName]) && $formData[$this->uidElementName] === $this->getName(); 1241 } 1242 1243 /** 1244 * Return whether the given values (possibly incomplete) are valid 1245 * 1246 * Unlike Zend_Form::isValid() this will not set NULL as value for 1247 * an element that is not present in the given data. 1248 * 1249 * @param array $formData The data to validate 1250 * 1251 * @return bool 1252 */ 1253 public function isValidPartial(array $formData) 1254 { 1255 $this->create($formData); 1256 1257 foreach ($this->getElements() as $name => $element) { 1258 if (array_key_exists($name, $formData)) { 1259 if ($element->getAttrib('disabled')) { 1260 // Ensure that disabled elements are not overwritten 1261 // (http://www.zendframework.com/issues/browse/ZF-6909) 1262 $formData[$name] = $element->getValue(); 1263 } elseif (array_key_exists($name . static::DEFAULT_SUFFIX, $formData) 1264 && $formData[$name] === $formData[$name . static::DEFAULT_SUFFIX] 1265 ) { 1266 unset($formData[$name]); 1267 } 1268 } 1269 } 1270 1271 return parent::isValidPartial($formData); 1272 } 1273 1274 /** 1275 * Return whether the given values are valid 1276 * 1277 * @param array $formData The data to validate 1278 * 1279 * @return bool 1280 */ 1281 public function isValid($formData) 1282 { 1283 $this->create($formData); 1284 1285 // Ensure that disabled elements are not overwritten (http://www.zendframework.com/issues/browse/ZF-6909) 1286 foreach ($this->getElements() as $name => $element) { 1287 if ($element->getAttrib('disabled')) { 1288 $formData[$name] = $element->getValue(); 1289 } 1290 } 1291 1292 return parent::isValid($formData); 1293 } 1294 1295 /** 1296 * Remove all elements of this form 1297 * 1298 * @return self 1299 */ 1300 public function clearElements() 1301 { 1302 $this->created = false; 1303 return parent::clearElements(); 1304 } 1305 1306 /** 1307 * Load the default decorators 1308 * 1309 * Overwrites Zend_Form::loadDefaultDecorators to avoid having 1310 * the HtmlTag-Decorator added and to provide view script usage 1311 * 1312 * @return $this 1313 */ 1314 public function loadDefaultDecorators() 1315 { 1316 if ($this->loadDefaultDecoratorsIsDisabled()) { 1317 return $this; 1318 } 1319 1320 $decorators = $this->getDecorators(); 1321 if (empty($decorators)) { 1322 if ($this->viewScript) { 1323 $this->addDecorator('ViewScript', array( 1324 'viewScript' => $this->viewScript, 1325 'form' => $this 1326 )); 1327 } else { 1328 $this->addDecorator('Description', array('tag' => 'h1')); 1329 if ($this->getUseFormAutosubmit()) { 1330 $this->getDecorator('Description')->setEscape(false); 1331 $this->addDecorator( 1332 'HtmlTag', 1333 array( 1334 'tag' => 'div', 1335 'class' => 'header', 1336 'id' => 'header-' . $this->getId() 1337 ) 1338 ); 1339 } 1340 1341 $this->addDecorator('FormDescriptions') 1342 ->addDecorator('FormNotifications') 1343 ->addDecorator('FormErrors', array('onlyCustomFormErrors' => true)) 1344 ->addDecorator('FormElements') 1345 ->addDecorator('FormHints') 1346 //->addDecorator('HtmlTag', array('tag' => 'dl', 'class' => 'zend_form')) 1347 ->addDecorator('Form'); 1348 } 1349 } 1350 1351 return $this; 1352 } 1353 1354 /** 1355 * Get element id 1356 * 1357 * Returns the protected id, in case id protection is enabled. 1358 * 1359 * @param bool $protect 1360 * 1361 * @return string 1362 */ 1363 public function getId($protect = true) 1364 { 1365 $id = parent::getId(); 1366 return $protect && $this->protectIds ? $this->getRequest()->protectId($id) : $id; 1367 } 1368 1369 /** 1370 * Return the name of this form 1371 * 1372 * @return string 1373 */ 1374 public function getName() 1375 { 1376 $name = parent::getName(); 1377 if (! $name) { 1378 $name = get_class($this); 1379 $this->setName($name); 1380 $name = parent::getName(); 1381 } 1382 return $name; 1383 } 1384 1385 /** 1386 * Retrieve form description 1387 * 1388 * This will return the escaped description with the autosubmit warning icon if form autosubmit is enabled. 1389 * 1390 * @return string 1391 */ 1392 public function getDescription() 1393 { 1394 $description = parent::getDescription(); 1395 if ($description && $this->getUseFormAutosubmit()) { 1396 $autosubmit = $this->_getDecorator('Autosubmit', array('accessible' => true)); 1397 $autosubmit->setElement($this); 1398 $description = $autosubmit->render($this->getView()->escape($description)); 1399 } 1400 1401 return $description; 1402 } 1403 1404 /** 1405 * Set the action to submit this form against 1406 * 1407 * Note that if you'll pass a instance of URL, Url::getAbsoluteUrl('&') is called to set the action. 1408 * 1409 * @param Url|string $action 1410 * 1411 * @return $this 1412 */ 1413 public function setAction($action) 1414 { 1415 if ($action instanceof Url) { 1416 $action = $action->getAbsoluteUrl('&'); 1417 } 1418 1419 return parent::setAction($action); 1420 } 1421 1422 /** 1423 * Set form description 1424 * 1425 * Alias for Zend_Form::setDescription(). 1426 * 1427 * @param string $value 1428 * 1429 * @return Form 1430 */ 1431 public function setTitle($value) 1432 { 1433 return $this->setDescription($value); 1434 } 1435 1436 /** 1437 * Return the request associated with this form 1438 * 1439 * Returns the global request if none has been set for this form yet. 1440 * 1441 * @return Request 1442 */ 1443 public function getRequest() 1444 { 1445 if ($this->request === null) { 1446 $this->request = Icinga::app()->getRequest(); 1447 } 1448 1449 return $this->request; 1450 } 1451 1452 /** 1453 * Set the request 1454 * 1455 * @param Request $request 1456 * 1457 * @return $this 1458 */ 1459 public function setRequest(Request $request) 1460 { 1461 $this->request = $request; 1462 return $this; 1463 } 1464 1465 /** 1466 * Return the current Response 1467 * 1468 * @return Response 1469 */ 1470 public function getResponse() 1471 { 1472 return Icinga::app()->getFrontController()->getResponse(); 1473 } 1474 1475 /** 1476 * Return the request data based on this form's request method 1477 * 1478 * @return array 1479 */ 1480 protected function getRequestData() 1481 { 1482 if (strtolower($this->request->getMethod()) === $this->getMethod()) { 1483 return $this->request->{'get' . ($this->request->isPost() ? 'Post' : 'Query')}(); 1484 } 1485 1486 return array(); 1487 } 1488 1489 /** 1490 * Get the translation domain for this form 1491 * 1492 * The returned translation domain is either determined based on this form's qualified name or it is the default 1493 * 'icinga' domain 1494 * 1495 * @return string 1496 */ 1497 protected function getTranslationDomain() 1498 { 1499 $parts = explode('\\', get_called_class()); 1500 if (count($parts) > 1 && $parts[1] === 'Module') { 1501 // Assume format Icinga\Module\ModuleName\Forms\... 1502 return strtolower($parts[2]); 1503 } 1504 1505 return 'icinga'; 1506 } 1507 1508 /** 1509 * Translate a string 1510 * 1511 * @param string $text The string to translate 1512 * @param string|null $context Optional parameter for context based translation 1513 * 1514 * @return string The translated string 1515 */ 1516 protected function translate($text, $context = null) 1517 { 1518 return Translator::translate($text, $this->getTranslationDomain(), $context); 1519 } 1520 1521 /** 1522 * Translate a plural string 1523 * 1524 * @param string $textSingular The string in singular form to translate 1525 * @param string $textPlural The string in plural form to translate 1526 * @param integer $number The amount to determine from whether to return singular or plural 1527 * @param string|null $context Optional parameter for context based translation 1528 * 1529 * @return string The translated string 1530 */ 1531 protected function translatePlural($textSingular, $textPlural, $number, $context = null) 1532 { 1533 return Translator::translatePlural( 1534 $textSingular, 1535 $textPlural, 1536 $number, 1537 $this->getTranslationDomain(), 1538 $context 1539 ); 1540 } 1541 1542 /** 1543 * Render this form 1544 * 1545 * @param Zend_View_Interface $view The view context to use 1546 * 1547 * @return string 1548 */ 1549 public function render(Zend_View_Interface $view = null) 1550 { 1551 $this->create(); 1552 return parent::render($view); 1553 } 1554 1555 /** 1556 * Get the authentication manager 1557 * 1558 * @return Auth 1559 */ 1560 public function Auth() 1561 { 1562 if ($this->auth === null) { 1563 $this->auth = Auth::getInstance(); 1564 } 1565 return $this->auth; 1566 } 1567 1568 /** 1569 * Whether the current user has the given permission 1570 * 1571 * @param string $permission Name of the permission 1572 * 1573 * @return bool 1574 */ 1575 public function hasPermission($permission) 1576 { 1577 return $this->Auth()->hasPermission($permission); 1578 } 1579 1580 /** 1581 * Assert that the current user has the given permission 1582 * 1583 * @param string $permission Name of the permission 1584 * 1585 * @throws SecurityException If the current user lacks the given permission 1586 */ 1587 public function assertPermission($permission) 1588 { 1589 if (! $this->Auth()->hasPermission($permission)) { 1590 throw new SecurityException('No permission for %s', $permission); 1591 } 1592 } 1593 1594 /** 1595 * Add a error notification 1596 * 1597 * @param string|array $message The notification message 1598 * @param bool $markAsError Whether to prevent the form from being successfully validated or not 1599 * 1600 * @return $this 1601 */ 1602 public function error($message, $markAsError = true) 1603 { 1604 if ($this->getIsApiTarget()) { 1605 $this->addErrorMessage($message); 1606 } else { 1607 $this->addNotification($message, self::NOTIFICATION_ERROR); 1608 } 1609 1610 if ($markAsError) { 1611 $this->markAsError(); 1612 } 1613 1614 return $this; 1615 } 1616 1617 /** 1618 * Add a warning notification 1619 * 1620 * @param string|array $message The notification message 1621 * @param bool $markAsError Whether to prevent the form from being successfully validated or not 1622 * 1623 * @return $this 1624 */ 1625 public function warning($message, $markAsError = true) 1626 { 1627 if ($this->getIsApiTarget()) { 1628 $this->addErrorMessage($message); 1629 } else { 1630 $this->addNotification($message, self::NOTIFICATION_WARNING); 1631 } 1632 1633 if ($markAsError) { 1634 $this->markAsError(); 1635 } 1636 1637 return $this; 1638 } 1639 1640 /** 1641 * Add a info notification 1642 * 1643 * @param string|array $message The notification message 1644 * @param bool $markAsError Whether to prevent the form from being successfully validated or not 1645 * 1646 * @return $this 1647 */ 1648 public function info($message, $markAsError = true) 1649 { 1650 if ($this->getIsApiTarget()) { 1651 $this->addErrorMessage($message); 1652 } else { 1653 $this->addNotification($message, self::NOTIFICATION_INFO); 1654 } 1655 1656 if ($markAsError) { 1657 $this->markAsError(); 1658 } 1659 1660 return $this; 1661 } 1662} 1663