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