summaryrefslogtreecommitdiffstats
path: root/library/Icinga/Web/Form
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--library/Icinga/Web/Form.php1666
-rw-r--r--library/Icinga/Web/Form/Decorator/Autosubmit.php133
-rw-r--r--library/Icinga/Web/Form/Decorator/ConditionalHidden.php35
-rw-r--r--library/Icinga/Web/Form/Decorator/ElementDoubler.php63
-rw-r--r--library/Icinga/Web/Form/Decorator/FormDescriptions.php76
-rw-r--r--library/Icinga/Web/Form/Decorator/FormHints.php142
-rw-r--r--library/Icinga/Web/Form/Decorator/FormNotifications.php125
-rw-r--r--library/Icinga/Web/Form/Decorator/Help.php113
-rw-r--r--library/Icinga/Web/Form/Decorator/Spinner.php48
-rw-r--r--library/Icinga/Web/Form/Element/Button.php80
-rw-r--r--library/Icinga/Web/Form/Element/Checkbox.php9
-rw-r--r--library/Icinga/Web/Form/Element/CsrfCounterMeasure.php99
-rw-r--r--library/Icinga/Web/Form/Element/Date.php19
-rw-r--r--library/Icinga/Web/Form/Element/DateTimePicker.php80
-rw-r--r--library/Icinga/Web/Form/Element/Note.php55
-rw-r--r--library/Icinga/Web/Form/Element/Number.php144
-rw-r--r--library/Icinga/Web/Form/Element/Textarea.php20
-rw-r--r--library/Icinga/Web/Form/Element/Time.php19
-rw-r--r--library/Icinga/Web/Form/ErrorLabeller.php71
-rw-r--r--library/Icinga/Web/Form/FormElement.php61
-rw-r--r--library/Icinga/Web/Form/InvalidCSRFTokenException.php11
-rw-r--r--library/Icinga/Web/Form/Validator/DateFormatValidator.php61
-rw-r--r--library/Icinga/Web/Form/Validator/DateTimeValidator.php77
-rw-r--r--library/Icinga/Web/Form/Validator/InArray.php28
-rw-r--r--library/Icinga/Web/Form/Validator/InternalUrlValidator.php41
-rw-r--r--library/Icinga/Web/Form/Validator/ReadablePathValidator.php53
-rw-r--r--library/Icinga/Web/Form/Validator/TimeFormatValidator.php58
-rw-r--r--library/Icinga/Web/Form/Validator/UrlValidator.php40
-rw-r--r--library/Icinga/Web/Form/Validator/WritablePathValidator.php72
29 files changed, 3499 insertions, 0 deletions
diff --git a/library/Icinga/Web/Form.php b/library/Icinga/Web/Form.php
new file mode 100644
index 0000000..b421849
--- /dev/null
+++ b/library/Icinga/Web/Form.php
@@ -0,0 +1,1666 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web;
+
+use Icinga\Web\Form\Element\DateTimePicker;
+use ipl\I18n\Translation;
+use Zend_Config;
+use Zend_Form;
+use Zend_Form_Element;
+use Zend_View_Interface;
+use Icinga\Application\Icinga;
+use Icinga\Authentication\Auth;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Security\SecurityException;
+use Icinga\Web\Form\ErrorLabeller;
+use Icinga\Web\Form\Decorator\Autosubmit;
+use Icinga\Web\Form\Element\CsrfCounterMeasure;
+
+/**
+ * Base class for forms providing CSRF protection, confirmation logic and auto submission
+ *
+ * @method \Zend_Form_Element[] getElements() {
+ * {@inheritdoc}
+ * @return \Zend_Form_Element[]
+ * }
+ */
+class Form extends Zend_Form
+{
+ use Translation {
+ translate as i18nTranslate;
+ translatePlural as i18nTranslatePlural;
+ }
+
+ /**
+ * The suffix to append to a field's hidden default field name
+ */
+ const DEFAULT_SUFFIX = '_default';
+
+ /**
+ * A form's default CSS classes
+ */
+ const DEFAULT_CLASSES = 'icinga-form icinga-controls';
+
+ /**
+ * Identifier for notifications of type error
+ */
+ const NOTIFICATION_ERROR = 0;
+
+ /**
+ * Identifier for notifications of type warning
+ */
+ const NOTIFICATION_WARNING = 1;
+
+ /**
+ * Identifier for notifications of type info
+ */
+ const NOTIFICATION_INFO = 2;
+
+ /**
+ * Whether this form has been created
+ *
+ * @var bool
+ */
+ protected $created = false;
+
+ /**
+ * This form's parent
+ *
+ * Gets automatically set upon calling addSubForm().
+ *
+ * @var Form
+ */
+ protected $_parent;
+
+ /**
+ * Whether the form is an API target
+ *
+ * When the form is an API target, the form evaluates as submitted if the request method equals the form method.
+ * That means, that the submit button and form identification are not taken into account. In addition, the CSRF
+ * counter measure will not be added to the form's elements.
+ *
+ * @var bool
+ */
+ protected $isApiTarget = false;
+
+ /**
+ * The request associated with this form
+ *
+ * @var Request
+ */
+ protected $request;
+
+ /**
+ * The callback to call instead of Form::onSuccess()
+ *
+ * @var callable
+ */
+ protected $onSuccess;
+
+ /**
+ * Label to use for the standard submit button
+ *
+ * @var string
+ */
+ protected $submitLabel;
+
+ /**
+ * Label to use for showing the user an activity indicator when submitting the form
+ *
+ * @var string
+ */
+ protected $progressLabel;
+
+ /**
+ * The url to redirect to upon success
+ *
+ * @var Url
+ */
+ protected $redirectUrl;
+
+ /**
+ * The view script to use when rendering this form
+ *
+ * @var string
+ */
+ protected $viewScript;
+
+ /**
+ * Whether this form should NOT add random generated "challenge" tokens that are associated with the user's current
+ * session in order to prevent Cross-Site Request Forgery (CSRF). It is the form's responsibility to verify the
+ * existence and correctness of this token
+ *
+ * @var bool
+ */
+ protected $tokenDisabled = false;
+
+ /**
+ * Name of the CSRF token element
+ *
+ * @var string
+ */
+ protected $tokenElementName = 'CSRFToken';
+
+ /**
+ * Whether this form should add a UID element being used to distinct different forms posting to the same action
+ *
+ * @var bool
+ */
+ protected $uidDisabled = false;
+
+ /**
+ * Name of the form identification element
+ *
+ * @var string
+ */
+ protected $uidElementName = 'formUID';
+
+ /**
+ * Whether the form should validate the sent data when being automatically submitted
+ *
+ * @var bool
+ */
+ protected $validatePartial = false;
+
+ /**
+ * Whether element ids will be protected against collisions by appending a request-specific unique identifier
+ *
+ * @var bool
+ */
+ protected $protectIds = true;
+
+ /**
+ * The cue that is appended to each element's label if it's required
+ *
+ * @var string
+ */
+ protected $requiredCue = '*';
+
+ /**
+ * The descriptions of this form
+ *
+ * @var array
+ */
+ protected $descriptions;
+
+ /**
+ * The notifications of this form
+ *
+ * @var array
+ */
+ protected $notifications;
+
+ /**
+ * The hints of this form
+ *
+ * @var array
+ */
+ protected $hints;
+
+ /**
+ * Whether the Autosubmit decorator should be applied to this form
+ *
+ * If this is true, the Autosubmit decorator is being applied to this form instead of to each of its elements.
+ *
+ * @var bool
+ */
+ protected $useFormAutosubmit = false;
+
+ /**
+ * Authentication manager
+ *
+ * @var Auth|null
+ */
+ private $auth;
+
+ /**
+ * Default element decorators
+ *
+ * @var array
+ */
+ public static $defaultElementDecorators = array(
+ array('Label', array('tag'=>'span', 'separator' => '', 'class' => 'control-label')),
+ array(array('labelWrap' => 'HtmlTag'), array('tag' => 'div', 'class' => 'control-label-group')),
+ array('ViewHelper', array('separator' => '')),
+ array('Help', array()),
+ array('Errors', array('separator' => '')),
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group'))
+ );
+
+ /**
+ * (non-PHPDoc)
+ * @see \Zend_Form::construct() For the method documentation.
+ */
+ public function __construct($options = null)
+ {
+ // Zend's plugin loader reverses the order of added prefix paths thus trying our paths first before trying
+ // Zend paths
+ $this->addPrefixPaths(array(
+ array(
+ 'prefix' => 'Icinga\\Web\\Form\\Element\\',
+ 'path' => Icinga::app()->getLibraryDir('Icinga/Web/Form/Element'),
+ 'type' => static::ELEMENT
+ ),
+ array(
+ 'prefix' => 'Icinga\\Web\\Form\\Decorator\\',
+ 'path' => Icinga::app()->getLibraryDir('Icinga/Web/Form/Decorator'),
+ 'type' => static::DECORATOR
+ )
+ ));
+
+ if (! isset($options['attribs']['class'])) {
+ $options['attribs']['class'] = static::DEFAULT_CLASSES;
+ }
+
+ parent::__construct($options);
+ }
+
+ /**
+ * Set this form's parent
+ *
+ * @param Form $form
+ *
+ * @return $this
+ */
+ public function setParent(Form $form)
+ {
+ $this->_parent = $form;
+ return $this;
+ }
+
+ /**
+ * Return this form's parent
+ *
+ * @return Form
+ */
+ public function getParent()
+ {
+ return $this->_parent;
+ }
+
+ /**
+ * Set a callback that is called instead of this form's onSuccess method
+ *
+ * It is called using the following signature: (Form $this).
+ *
+ * @param callable $onSuccess Callback
+ *
+ * @return $this
+ *
+ * @throws ProgrammingError If the callback is not callable
+ */
+ public function setOnSuccess($onSuccess)
+ {
+ if (! is_callable($onSuccess)) {
+ throw new ProgrammingError('The option `onSuccess\' is not callable');
+ }
+ $this->onSuccess = $onSuccess;
+ return $this;
+ }
+
+ /**
+ * Set the label to use for the standard submit button
+ *
+ * @param string $label The label to use for the submit button
+ *
+ * @return $this
+ */
+ public function setSubmitLabel($label)
+ {
+ $this->submitLabel = $label;
+ return $this;
+ }
+
+ /**
+ * Return the label being used for the standard submit button
+ *
+ * @return string
+ */
+ public function getSubmitLabel()
+ {
+ return $this->submitLabel;
+ }
+
+ /**
+ * Set the label to use for showing the user an activity indicator when submitting the form
+ *
+ * @param string $label
+ *
+ * @return $this
+ */
+ public function setProgressLabel($label)
+ {
+ $this->progressLabel = $label;
+ return $this;
+ }
+
+ /**
+ * Return the label to use for showing the user an activity indicator when submitting the form
+ *
+ * @return string
+ */
+ public function getProgressLabel()
+ {
+ return $this->progressLabel;
+ }
+
+ /**
+ * Set the url to redirect to upon success
+ *
+ * @param string|Url $url The url to redirect to
+ *
+ * @return $this
+ *
+ * @throws ProgrammingError In case $url is neither a string nor a instance of Icinga\Web\Url
+ */
+ public function setRedirectUrl($url)
+ {
+ if (is_string($url)) {
+ $url = Url::fromPath($url, array(), $this->getRequest());
+ } elseif (! $url instanceof Url) {
+ throw new ProgrammingError('$url must be a string or instance of Icinga\Web\Url');
+ }
+
+ $this->redirectUrl = $url;
+ return $this;
+ }
+
+ /**
+ * Return the url to redirect to upon success
+ *
+ * @return Url
+ */
+ public function getRedirectUrl()
+ {
+ if ($this->redirectUrl === null) {
+ $this->redirectUrl = $this->getRequest()->getUrl();
+ if ($this->getMethod() === 'get') {
+ // Be sure to remove all form dependent params because we do not want to submit it again
+ $this->redirectUrl = $this->redirectUrl->without(array_keys($this->getElements()));
+ }
+ }
+
+ return $this->redirectUrl;
+ }
+
+ /**
+ * Set the view script to use when rendering this form
+ *
+ * @param string $viewScript The view script to use
+ *
+ * @return $this
+ */
+ public function setViewScript($viewScript)
+ {
+ $this->viewScript = $viewScript;
+ return $this;
+ }
+
+ /**
+ * Return the view script being used when rendering this form
+ *
+ * @return string
+ */
+ public function getViewScript()
+ {
+ return $this->viewScript;
+ }
+
+ /**
+ * Disable CSRF counter measure and remove its field if already added
+ *
+ * @param bool $disabled Set true in order to disable CSRF protection for this form, otherwise false
+ *
+ * @return $this
+ */
+ public function setTokenDisabled($disabled = true)
+ {
+ $this->tokenDisabled = (bool) $disabled;
+
+ if ($disabled && $this->getElement($this->tokenElementName) !== null) {
+ $this->removeElement($this->tokenElementName);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Return whether CSRF counter measures are disabled for this form
+ *
+ * @return bool
+ */
+ public function getTokenDisabled()
+ {
+ return $this->tokenDisabled;
+ }
+
+ /**
+ * Set the name to use for the CSRF element
+ *
+ * @param string $name The name to set
+ *
+ * @return $this
+ */
+ public function setTokenElementName($name)
+ {
+ $this->tokenElementName = $name;
+ return $this;
+ }
+
+ /**
+ * Return the name of the CSRF element
+ *
+ * @return string
+ */
+ public function getTokenElementName()
+ {
+ return $this->tokenElementName;
+ }
+
+ /**
+ * Disable form identification and remove its field if already added
+ *
+ * @param bool $disabled Set true in order to disable identification for this form, otherwise false
+ *
+ * @return $this
+ */
+ public function setUidDisabled($disabled = true)
+ {
+ $this->uidDisabled = (bool) $disabled;
+
+ if ($disabled && $this->getElement($this->uidElementName) !== null) {
+ $this->removeElement($this->uidElementName);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Return whether identification is disabled for this form
+ *
+ * @return bool
+ */
+ public function getUidDisabled()
+ {
+ return $this->uidDisabled;
+ }
+
+ /**
+ * Set the name to use for the form identification element
+ *
+ * @param string $name The name to set
+ *
+ * @return $this
+ */
+ public function setUidElementName($name)
+ {
+ $this->uidElementName = $name;
+ return $this;
+ }
+
+ /**
+ * Return the name of the form identification element
+ *
+ * @return string
+ */
+ public function getUidElementName()
+ {
+ return $this->uidElementName;
+ }
+
+ /**
+ * Set whether this form should validate the sent data when being automatically submitted
+ *
+ * @param bool $state
+ *
+ * @return $this
+ */
+ public function setValidatePartial($state)
+ {
+ $this->validatePartial = $state;
+ return $this;
+ }
+
+ /**
+ * Return whether this form should validate the sent data when being automatically submitted
+ *
+ * @return bool
+ */
+ public function getValidatePartial()
+ {
+ return $this->validatePartial;
+ }
+
+ /**
+ * Set whether each element's id should be altered to avoid duplicates
+ *
+ * @param bool $value
+ *
+ * @return Form
+ */
+ public function setProtectIds($value = true)
+ {
+ $this->protectIds = (bool) $value;
+ return $this;
+ }
+
+ /**
+ * Return whether each element's id is being altered to avoid duplicates
+ *
+ * @return bool
+ */
+ public function getProtectIds()
+ {
+ return $this->protectIds;
+ }
+
+ /**
+ * Set the cue to append to each element's label if it's required
+ *
+ * @param string $cue
+ *
+ * @return Form
+ */
+ public function setRequiredCue($cue)
+ {
+ $this->requiredCue = $cue;
+ return $this;
+ }
+
+ /**
+ * Return the cue being appended to each element's label if it's required
+ *
+ * @return string
+ */
+ public function getRequiredCue()
+ {
+ return $this->requiredCue;
+ }
+
+ /**
+ * Set the descriptions for this form
+ *
+ * @param array $descriptions
+ *
+ * @return Form
+ */
+ public function setDescriptions(array $descriptions)
+ {
+ $this->descriptions = $descriptions;
+ return $this;
+ }
+
+ /**
+ * Add a description for this form
+ *
+ * If $description is an array the second value should be
+ * an array as well containing additional HTML properties.
+ *
+ * @param string|array $description
+ *
+ * @return Form
+ */
+ public function addDescription($description)
+ {
+ $this->descriptions[] = $description;
+ return $this;
+ }
+
+ /**
+ * Return the descriptions of this form
+ *
+ * @return array
+ */
+ public function getDescriptions()
+ {
+ if ($this->descriptions === null) {
+ return array();
+ }
+
+ return $this->descriptions;
+ }
+
+ /**
+ * Set the notifications for this form
+ *
+ * @param array $notifications
+ *
+ * @return $this
+ */
+ public function setNotifications(array $notifications)
+ {
+ $this->notifications = $notifications;
+ return $this;
+ }
+
+ /**
+ * Add a notification for this form
+ *
+ * If $notification is an array the second value should be
+ * an array as well containing additional HTML properties.
+ *
+ * @param string|array $notification
+ * @param int $type
+ *
+ * @return $this
+ */
+ public function addNotification($notification, $type)
+ {
+ $this->notifications[$type][] = $notification;
+ return $this;
+ }
+
+ /**
+ * Return the notifications of this form
+ *
+ * @return array
+ */
+ public function getNotifications()
+ {
+ if ($this->notifications === null) {
+ return array();
+ }
+
+ return $this->notifications;
+ }
+
+ /**
+ * Set the hints for this form
+ *
+ * @param array $hints
+ *
+ * @return $this
+ */
+ public function setHints(array $hints)
+ {
+ $this->hints = $hints;
+ return $this;
+ }
+
+ /**
+ * Add a hint for this form
+ *
+ * If $hint is an array the second value should be an
+ * array as well containing additional HTML properties.
+ *
+ * @param string|array $hint
+ *
+ * @return $this
+ */
+ public function addHint($hint)
+ {
+ $this->hints[] = $hint;
+ return $this;
+ }
+
+ /**
+ * Return the hints of this form
+ *
+ * @return array
+ */
+ public function getHints()
+ {
+ if ($this->hints === null) {
+ return array();
+ }
+
+ return $this->hints;
+ }
+
+ /**
+ * Set whether the Autosubmit decorator should be applied to this form
+ *
+ * If true, the Autosubmit decorator is being applied to this form instead of to each of its elements.
+ *
+ * @param bool $state
+ *
+ * @return Form
+ */
+ public function setUseFormAutosubmit($state = true)
+ {
+ $this->useFormAutosubmit = (bool) $state;
+ if ($this->useFormAutosubmit) {
+ $this->setAttrib('data-progress-element', 'header-' . $this->getId());
+ } else {
+ $this->removeAttrib('data-progress-element');
+ }
+
+ return $this;
+ }
+
+ /**
+ * Return whether the Autosubmit decorator is being applied to this form
+ *
+ * @return bool
+ */
+ public function getUseFormAutosubmit()
+ {
+ return $this->useFormAutosubmit;
+ }
+
+ /**
+ * Get whether the form is an API target
+ *
+ * @todo This should probably only return true if the request is also an api request
+ * @return bool
+ */
+ public function getIsApiTarget()
+ {
+ return $this->isApiTarget;
+ }
+
+ /**
+ * Set whether the form is an API target
+ *
+ * @param bool $isApiTarget
+ *
+ * @return $this
+ */
+ public function setIsApiTarget($isApiTarget = true)
+ {
+ $this->isApiTarget = (bool) $isApiTarget;
+ return $this;
+ }
+
+ /**
+ * Create this form
+ *
+ * @param array $formData The data sent by the user
+ *
+ * @return $this
+ */
+ public function create(array $formData = array())
+ {
+ if (! $this->created) {
+ $this->createElements($formData);
+ $this->addFormIdentification()
+ ->addCsrfCounterMeasure()
+ ->addSubmitButton();
+
+ // Use Form::getAttrib() instead of Form::getAction() here because we want to explicitly check against
+ // null. Form::getAction() would return the empty string '' if the action is not set.
+ // For not setting the action attribute use Form::setAction(''). This is required for for the
+ // accessibility's enable/disable auto-refresh mechanic
+ if ($this->getAttrib('action') === null) {
+ $action = $this->getRequest()->getUrl();
+ if ($this->getMethod() === 'get') {
+ $action = $action->without(array_keys($this->getElements()));
+ }
+
+ // TODO(el): Re-evalute this necessity.
+ // JavaScript could use the container'sURL if there's no action set.
+ // We MUST set an action as JS gets confused otherwise, if
+ // this form is being displayed in an additional column
+ $this->setAction($action);
+ }
+
+ $this->created = true;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * Intended to be implemented by concrete form classes.
+ *
+ * @param array $formData The data sent by the user
+ */
+ public function createElements(array $formData)
+ {
+ }
+
+ /**
+ * Perform actions after this form was submitted using a valid request
+ *
+ * Intended to be implemented by concrete form classes. The base implementation returns always FALSE.
+ *
+ * @return null|bool Return FALSE in case no redirect should take place
+ */
+ public function onSuccess()
+ {
+ return false;
+ }
+
+ /**
+ * Perform actions when no form dependent data was sent
+ *
+ * Intended to be implemented by concrete form classes.
+ */
+ public function onRequest()
+ {
+ }
+
+ /**
+ * Add a submit button to this form
+ *
+ * Uses the label previously set with Form::setSubmitLabel(). Overwrite this
+ * method in order to add multiple submit buttons or one with a custom name.
+ *
+ * @return $this
+ */
+ public function addSubmitButton()
+ {
+ $submitLabel = $this->getSubmitLabel();
+ if ($submitLabel) {
+ $this->addElement(
+ 'submit',
+ 'btn_submit',
+ array(
+ 'class' => 'btn-primary',
+ 'ignore' => true,
+ 'label' => $submitLabel,
+ 'data-progress-label' => $this->getProgressLabel(),
+ 'decorators' => array(
+ 'ViewHelper',
+ array('Spinner', array('separator' => '')),
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ )
+ )
+ );
+ }
+
+ return $this;
+ }
+
+ /**
+ * Add a subform
+ *
+ * @param Zend_Form $form The subform to add
+ * @param string $name The name of the subform or null to use the name of $form
+ * @param int $order The location where to insert the form
+ *
+ * @return Zend_Form
+ */
+ public function addSubForm(Zend_Form $form, $name = null, $order = null)
+ {
+ if ($form instanceof self) {
+ $form->setDecorators(array('FormElements')); // TODO: Makes it difficult to customise subform decorators..
+ $form->setSubmitLabel('');
+ $form->setTokenDisabled();
+ $form->setUidDisabled();
+ $form->setParent($this);
+ }
+
+ if ($name === null) {
+ $name = $form->getName();
+ }
+
+ return parent::addSubForm($form, $name, $order);
+ }
+
+ /**
+ * Create a new element
+ *
+ * Icinga Web 2 loads its own default element decorators. For loading Zend's default element decorators set the
+ * `disableLoadDefaultDecorators' option to any other value than `true'. For loading custom element decorators use
+ * the 'decorators' option.
+ *
+ * @param string $type The type of the element
+ * @param string $name The name of the element
+ * @param mixed $options The options for the element
+ *
+ * @return Zend_Form_Element
+ *
+ * @see Form::$defaultElementDecorators For Icinga Web 2's default element decorators.
+ */
+ public function createElement($type, $name, $options = null)
+ {
+ if ($options !== null) {
+ if ($options instanceof Zend_Config) {
+ $options = $options->toArray();
+ }
+ if (! isset($options['decorators'])
+ && ! array_key_exists('disabledLoadDefaultDecorators', $options)
+ ) {
+ $options['decorators'] = static::$defaultElementDecorators;
+ if (! isset($options['data-progress-label']) && ($type === 'submit'
+ || ($type === 'button' && isset($options['type']) && $options['type'] === 'submit'))
+ ) {
+ array_splice($options['decorators'], 1, 0, array(array('Spinner', array('separator' => ''))));
+ } elseif ($type === 'hidden') {
+ $options['decorators'] = array('ViewHelper');
+ }
+ }
+ } else {
+ $options = array('decorators' => static::$defaultElementDecorators);
+ if ($type === 'submit') {
+ array_splice($options['decorators'], 1, 0, array(array('Spinner', array('separator' => ''))));
+ } elseif ($type === 'hidden') {
+ $options['decorators'] = array('ViewHelper');
+ }
+ }
+
+ $el = parent::createElement($type, $name, $options);
+ $el->setTranslator(new ErrorLabeller(array('element' => $el)));
+
+ $el->addPrefixPaths(array(
+ array(
+ 'prefix' => 'Icinga\\Web\\Form\\Validator\\',
+ 'path' => Icinga::app()->getLibraryDir('Icinga/Web/Form/Validator'),
+ 'type' => $el::VALIDATE
+ )
+ ));
+
+ if ($this->protectIds) {
+ $el->setAttrib('id', $this->getRequest()->protectId($this->getId(false) . '_' . $el->getId()));
+ }
+
+ if ($el->getAttrib('autosubmit')) {
+ if ($this->getUseFormAutosubmit()) {
+ $warningId = 'autosubmit_warning_' . $el->getId();
+ $warningText = $this->getView()->escape($this->translate(
+ 'This page will be automatically updated upon change of the value'
+ ));
+ $autosubmitDecorator = $this->_getDecorator('Callback', array(
+ 'placement' => 'PREPEND',
+ 'callback' => function ($content) use ($warningId, $warningText) {
+ return '<span class="sr-only" id="' . $warningId . '">' . $warningText . '</span>';
+ }
+ ));
+ } else {
+ $autosubmitDecorator = new Autosubmit();
+ $autosubmitDecorator->setAccessible();
+ $warningId = $autosubmitDecorator->getWarningId($el);
+ }
+
+ $decorators = $el->getDecorators();
+ $pos = array_search('Zend_Form_Decorator_ViewHelper', array_keys($decorators), true) + 1;
+ $el->setDecorators(
+ array_slice($decorators, 0, $pos, true)
+ + array('autosubmit' => $autosubmitDecorator)
+ + array_slice($decorators, $pos, count($decorators) - $pos, true)
+ );
+
+ if (($describedBy = $el->getAttrib('aria-describedby')) !== null) {
+ $el->setAttrib('aria-describedby', $describedBy . ' ' . $warningId);
+ } else {
+ $el->setAttrib('aria-describedby', $warningId);
+ }
+
+ $class = $el->getAttrib('class');
+ if (is_array($class)) {
+ $class[] = 'autosubmit';
+ } elseif ($class === null) {
+ $class = 'autosubmit';
+ } else {
+ $class .= ' autosubmit';
+ }
+ $el->setAttrib('class', $class);
+
+ unset($el->autosubmit);
+ }
+
+ if ($el->getAttrib('preserveDefault')) {
+ $el->addDecorator(
+ array('preserveDefault' => 'HtmlTag'),
+ array(
+ 'tag' => 'input',
+ 'type' => 'hidden',
+ 'name' => $name . static::DEFAULT_SUFFIX,
+ 'value' => $el instanceof DateTimePicker
+ ? $el->getValue()->format($el->getFormat())
+ : $el->getValue()
+ )
+ );
+
+ unset($el->preserveDefault);
+ }
+
+ return $this->ensureElementAccessibility($el);
+ }
+
+ /**
+ * Add accessibility related attributes
+ *
+ * @param Zend_Form_Element $element
+ *
+ * @return Zend_Form_Element
+ */
+ public function ensureElementAccessibility(Zend_Form_Element $element)
+ {
+ if ($element->isRequired()) {
+ $element->setAttrib('aria-required', 'true'); // ARIA
+ $element->setAttrib('required', ''); // HTML5
+ if (($cue = $this->getRequiredCue()) !== null && ($label = $element->getDecorator('label')) !== false) {
+ $element->setLabel($this->getView()->escape($element->getLabel()));
+ $label->setOption('escape', false);
+ $label->setRequiredSuffix(sprintf(' <span aria-hidden="true">%s</span>', $cue));
+ }
+ }
+
+ if ($element->getDescription() !== null && ($help = $element->getDecorator('help')) !== false) {
+ if (($describedBy = $element->getAttrib('aria-describedby')) !== null) {
+ // Assume that it's because of the element being of type autosubmit or
+ // that one who did set the property manually removes the help decorator
+ // in case it has already an aria-describedby property set
+ $element->setAttrib(
+ 'aria-describedby',
+ $help->setAccessible()->getDescriptionId($element) . ' ' . $describedBy
+ );
+ } else {
+ $element->setAttrib('aria-describedby', $help->setAccessible()->getDescriptionId($element));
+ }
+ }
+
+ return $element;
+ }
+
+ /**
+ * Add a field with a unique and form specific ID
+ *
+ * @return $this
+ */
+ public function addFormIdentification()
+ {
+ if (! $this->uidDisabled && $this->getElement($this->uidElementName) === null) {
+ $this->addElement(
+ 'hidden',
+ $this->uidElementName,
+ array(
+ 'ignore' => true,
+ 'value' => $this->getName(),
+ 'decorators' => array('ViewHelper')
+ )
+ );
+ }
+
+ return $this;
+ }
+
+ /**
+ * Add CSRF counter measure field to this form
+ *
+ * @return $this
+ */
+ public function addCsrfCounterMeasure()
+ {
+ if (! $this->tokenDisabled) {
+ $request = $this->getRequest();
+ if (! $request->isXmlHttpRequest()
+ && ($this->getIsApiTarget() || $request->isApiRequest())
+ ) {
+ return $this;
+ }
+ if ($this->getElement($this->tokenElementName) === null) {
+ $this->addElement('CsrfCounterMeasure', $this->tokenElementName);
+ }
+ }
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * Creates the form if not created yet.
+ *
+ * @param array $values
+ *
+ * @return $this
+ */
+ public function setDefaults(array $values)
+ {
+ $this->create($values);
+ return parent::setDefaults($values);
+ }
+
+ /**
+ * Populate the elements with the given values
+ *
+ * @param array $defaults The values to populate the elements with
+ *
+ * @return $this
+ */
+ public function populate(array $defaults)
+ {
+ $this->create($defaults);
+ $this->preserveDefaults($this, $defaults);
+ return parent::populate($defaults);
+ }
+
+ /**
+ * Recurse the given form and unset all unchanged default values
+ *
+ * @param Zend_Form $form
+ * @param array $defaults
+ */
+ protected function preserveDefaults(Zend_Form $form, array &$defaults)
+ {
+ foreach ($form->getElements() as $name => $element) {
+ if ((array_key_exists($name, $defaults)
+ && array_key_exists($name . static::DEFAULT_SUFFIX, $defaults)
+ && $defaults[$name] === $defaults[$name . static::DEFAULT_SUFFIX])
+ || $element->getAttrib('disabled')
+ ) {
+ unset($defaults[$name]);
+ }
+ }
+
+ foreach ($form->getSubForms() as $_ => $subForm) {
+ $this->preserveDefaults($subForm, $defaults);
+ }
+ }
+
+ /**
+ * Process the given request using this form
+ *
+ * Redirects to the url set with setRedirectUrl() upon success. See onSuccess()
+ * and onRequest() wherewith you can customize the processing logic.
+ *
+ * @param Request $request The request to be processed
+ *
+ * @return Request The request supposed to be processed
+ */
+ public function handleRequest(Request $request = null)
+ {
+ if ($request === null) {
+ $request = $this->getRequest();
+ } else {
+ $this->request = $request;
+ }
+
+ $formData = $this->getRequestData();
+ if ($this->getIsApiTarget()
+ // TODO: Very very bad, wasSent() must not be bypassed if it's only an api request but not an qpi target
+ || $this->getRequest()->isApiRequest()
+ || $this->getUidDisabled()
+ || $this->wasSent($formData)
+ ) {
+ $this->populate($formData); // Necessary to get isSubmitted() to work
+ if (! $this->getSubmitLabel() || $this->isSubmitted()) {
+ if ($this->isValid($formData)
+ && (($this->onSuccess !== null && false !== call_user_func($this->onSuccess, $this))
+ || ($this->onSuccess === null && false !== $this->onSuccess()))
+ ) {
+ // TODO: Still bad. An api target must not behave as one if it's not an api request
+ if ($this->getIsApiTarget() || $this->getRequest()->isApiRequest()) {
+ // API targets and API requests will never redirect but immediately respond w/ JSON-encoded
+ // notifications
+ $notifications = Notification::getInstance()->popMessages();
+ $message = null;
+ foreach ($notifications as $notification) {
+ if ($notification->type === Notification::SUCCESS) {
+ $message = $notification->message;
+ break;
+ }
+ }
+ $this->getResponse()->json()
+ ->setSuccessData($message !== null ? array('message' => $message) : null)
+ ->sendResponse();
+ } else {
+ $this->getResponse()->redirectAndExit($this->getRedirectUrl());
+ }
+ // TODO: Still bad. An api target must not behave as one if it's not an api request
+ } elseif ($this->getIsApiTarget() || $this->getRequest()->isApiRequest()) {
+ $this->getResponse()->json()->setFailData($this->getMessages())->sendResponse();
+ }
+ } elseif ($this->getValidatePartial()) {
+ // The form can't be processed but we may want to show validation errors though
+ $this->isValidPartial($formData);
+ }
+ } else {
+ $this->onRequest();
+ }
+
+ return $request;
+ }
+
+ /**
+ * Return whether the submit button of this form was pressed
+ *
+ * When overwriting Form::addSubmitButton() be sure to overwrite this method as well.
+ *
+ * @return bool True in case it was pressed, False otherwise or no submit label was set
+ */
+ public function isSubmitted()
+ {
+ $requestMethod = $this->getRequest()->getMethod();
+ if (strtolower($requestMethod ?: '') !== $this->getMethod()) {
+ return false;
+ }
+ if ($this->getIsApiTarget() || $this->getRequest()->isApiRequest()) {
+ return true;
+ }
+ if ($this->getSubmitLabel()) {
+ return $this->getElement('btn_submit')->isChecked();
+ }
+
+ return false;
+ }
+
+ /**
+ * Return whether the data sent by the user refers to this form
+ *
+ * Ensures that the correct form gets processed in case there are multiple forms
+ * with equal submit button names being posted against the same route.
+ *
+ * @param array $formData The data sent by the user
+ *
+ * @return bool Whether the given data refers to this form
+ */
+ public function wasSent(array $formData)
+ {
+ return isset($formData[$this->uidElementName]) && $formData[$this->uidElementName] === $this->getName();
+ }
+
+ /**
+ * Return whether the given values (possibly incomplete) are valid
+ *
+ * Unlike Zend_Form::isValid() this will not set NULL as value for
+ * an element that is not present in the given data.
+ *
+ * @param array $formData The data to validate
+ *
+ * @return bool
+ */
+ public function isValidPartial(array $formData)
+ {
+ $this->create($formData);
+
+ foreach ($this->getElements() as $name => $element) {
+ if (array_key_exists($name, $formData)) {
+ if ($element->getAttrib('disabled')) {
+ // Ensure that disabled elements are not overwritten
+ // (http://www.zendframework.com/issues/browse/ZF-6909)
+ $formData[$name] = $element->getValue();
+ } elseif (array_key_exists($name . static::DEFAULT_SUFFIX, $formData)
+ && $formData[$name] === $formData[$name . static::DEFAULT_SUFFIX]
+ ) {
+ unset($formData[$name]);
+ }
+ }
+ }
+
+ return parent::isValidPartial($formData);
+ }
+
+ /**
+ * Return whether the given values are valid
+ *
+ * @param array $formData The data to validate
+ *
+ * @return bool
+ */
+ public function isValid($formData)
+ {
+ $this->create($formData);
+
+ // Ensure that disabled elements are not overwritten (http://www.zendframework.com/issues/browse/ZF-6909)
+ foreach ($this->getElements() as $name => $element) {
+ if ($element->getAttrib('disabled')) {
+ $formData[$name] = $element->getValue();
+ }
+ }
+
+ return parent::isValid($formData);
+ }
+
+ /**
+ * Remove all elements of this form
+ *
+ * @return self
+ */
+ public function clearElements()
+ {
+ $this->created = false;
+ return parent::clearElements();
+ }
+
+ /**
+ * Load the default decorators
+ *
+ * Overwrites Zend_Form::loadDefaultDecorators to avoid having
+ * the HtmlTag-Decorator added and to provide view script usage
+ *
+ * @return $this
+ */
+ public function loadDefaultDecorators()
+ {
+ if ($this->loadDefaultDecoratorsIsDisabled()) {
+ return $this;
+ }
+
+ $decorators = $this->getDecorators();
+ if (empty($decorators)) {
+ if ($this->viewScript) {
+ $this->addDecorator('ViewScript', array(
+ 'viewScript' => $this->viewScript,
+ 'form' => $this
+ ));
+ } else {
+ $this->addDecorator('Description', array('tag' => 'h1'));
+ if ($this->getUseFormAutosubmit()) {
+ $this->getDecorator('Description')->setEscape(false);
+ $this->addDecorator(
+ 'HtmlTag',
+ array(
+ 'tag' => 'div',
+ 'class' => 'header',
+ 'id' => 'header-' . $this->getId()
+ )
+ );
+ }
+
+ $this->addDecorator('FormDescriptions')
+ ->addDecorator('FormNotifications')
+ ->addDecorator('FormErrors', array('onlyCustomFormErrors' => true))
+ ->addDecorator('FormElements')
+ ->addDecorator('FormHints')
+ //->addDecorator('HtmlTag', array('tag' => 'dl', 'class' => 'zend_form'))
+ ->addDecorator('Form');
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get element id
+ *
+ * Returns the protected id, in case id protection is enabled.
+ *
+ * @param bool $protect
+ *
+ * @return string
+ */
+ public function getId($protect = true)
+ {
+ $id = parent::getId();
+ return $protect && $this->protectIds ? $this->getRequest()->protectId($id) : $id;
+ }
+
+ /**
+ * Return the name of this form
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ $name = parent::getName();
+ if (! $name) {
+ $name = get_class($this);
+ $this->setName($name);
+ $name = parent::getName();
+ }
+ return $name;
+ }
+
+ /**
+ * Retrieve form description
+ *
+ * This will return the escaped description with the autosubmit warning icon if form autosubmit is enabled.
+ *
+ * @return string
+ */
+ public function getDescription()
+ {
+ $description = parent::getDescription();
+ if ($description && $this->getUseFormAutosubmit()) {
+ $autosubmit = $this->_getDecorator('Autosubmit', array('accessible' => true));
+ $autosubmit->setElement($this);
+ $description = $autosubmit->render($this->getView()->escape($description));
+ }
+
+ return $description;
+ }
+
+ /**
+ * Set the action to submit this form against
+ *
+ * Note that if you'll pass a instance of URL, Url::getAbsoluteUrl('&') is called to set the action.
+ *
+ * @param Url|string $action
+ *
+ * @return $this
+ */
+ public function setAction($action)
+ {
+ if ($action instanceof Url) {
+ $action = $action->getAbsoluteUrl('&');
+ }
+
+ return parent::setAction($action);
+ }
+
+ /**
+ * Set form description
+ *
+ * Alias for Zend_Form::setDescription().
+ *
+ * @param string $value
+ *
+ * @return Form
+ */
+ public function setTitle($value)
+ {
+ return $this->setDescription($value);
+ }
+
+ /**
+ * Return the request associated with this form
+ *
+ * Returns the global request if none has been set for this form yet.
+ *
+ * @return Request
+ */
+ public function getRequest()
+ {
+ if ($this->request === null) {
+ $this->request = Icinga::app()->getRequest();
+ }
+
+ return $this->request;
+ }
+
+ /**
+ * Set the request
+ *
+ * @param Request $request
+ *
+ * @return $this
+ */
+ public function setRequest(Request $request)
+ {
+ $this->request = $request;
+ return $this;
+ }
+
+ /**
+ * Return the current Response
+ *
+ * @return Response
+ */
+ public function getResponse()
+ {
+ return Icinga::app()->getFrontController()->getResponse();
+ }
+
+ /**
+ * Return the request data based on this form's request method
+ *
+ * @return array
+ */
+ protected function getRequestData()
+ {
+ $requestMethod = $this->getRequest()->getMethod();
+ if (strtolower($requestMethod ?: '') === $this->getMethod()) {
+ return $this->request->{'get' . ($this->request->isPost() ? 'Post' : 'Query')}();
+ }
+
+ return array();
+ }
+
+ /**
+ * Get the translation domain for this form
+ *
+ * The returned translation domain is either determined based on this form's qualified name or it is the default
+ * 'icinga' domain
+ *
+ * @return string
+ */
+ protected function getTranslationDomain()
+ {
+ $parts = explode('\\', get_called_class());
+ if (count($parts) > 1 && $parts[1] === 'Module') {
+ // Assume format Icinga\Module\ModuleName\Forms\...
+ return strtolower($parts[2]);
+ }
+
+ return 'icinga';
+ }
+
+ /**
+ * Translate a string
+ *
+ * @param string $text The string to translate
+ * @param string|null $context Optional parameter for context based translation
+ *
+ * @return string The translated string
+ */
+ protected function translate($text, $context = null)
+ {
+ $this->translationDomain = $this->getTranslationDomain();
+
+ return $this->i18nTranslate($text, $context);
+ }
+
+ /**
+ * Translate a plural string
+ *
+ * @param string $textSingular The string in singular form to translate
+ * @param string $textPlural The string in plural form to translate
+ * @param integer $number The amount to determine from whether to return singular or plural
+ * @param string|null $context Optional parameter for context based translation
+ *
+ * @return string The translated string
+ */
+ protected function translatePlural($textSingular, $textPlural, $number, $context = null)
+ {
+ $this->translationDomain = $this->getTranslationDomain();
+
+ return $this->i18nTranslatePlural($textSingular, $textPlural, $number, $context);
+ }
+
+ /**
+ * Render this form
+ *
+ * @param Zend_View_Interface $view The view context to use
+ *
+ * @return string
+ */
+ public function render(Zend_View_Interface $view = null)
+ {
+ $this->create();
+ return parent::render($view);
+ }
+
+ /**
+ * Get the authentication manager
+ *
+ * @return Auth
+ */
+ public function Auth()
+ {
+ if ($this->auth === null) {
+ $this->auth = Auth::getInstance();
+ }
+ return $this->auth;
+ }
+
+ /**
+ * Whether the current user has the given permission
+ *
+ * @param string $permission Name of the permission
+ *
+ * @return bool
+ */
+ public function hasPermission($permission)
+ {
+ return $this->Auth()->hasPermission($permission);
+ }
+
+ /**
+ * Assert that the current user has the given permission
+ *
+ * @param string $permission Name of the permission
+ *
+ * @throws SecurityException If the current user lacks the given permission
+ */
+ public function assertPermission($permission)
+ {
+ if (! $this->Auth()->hasPermission($permission)) {
+ throw new SecurityException('No permission for %s', $permission);
+ }
+ }
+
+ /**
+ * Add a error notification
+ *
+ * @param string|array $message The notification message
+ * @param bool $markAsError Whether to prevent the form from being successfully validated or not
+ *
+ * @return $this
+ */
+ public function error($message, $markAsError = true)
+ {
+ if ($this->getIsApiTarget()) {
+ $this->addErrorMessage($message);
+ } else {
+ $this->addNotification($message, self::NOTIFICATION_ERROR);
+ }
+
+ if ($markAsError) {
+ $this->markAsError();
+ }
+
+ return $this;
+ }
+
+ /**
+ * Add a warning notification
+ *
+ * @param string|array $message The notification message
+ * @param bool $markAsError Whether to prevent the form from being successfully validated or not
+ *
+ * @return $this
+ */
+ public function warning($message, $markAsError = true)
+ {
+ if ($this->getIsApiTarget()) {
+ $this->addErrorMessage($message);
+ } else {
+ $this->addNotification($message, self::NOTIFICATION_WARNING);
+ }
+
+ if ($markAsError) {
+ $this->markAsError();
+ }
+
+ return $this;
+ }
+
+ /**
+ * Add a info notification
+ *
+ * @param string|array $message The notification message
+ * @param bool $markAsError Whether to prevent the form from being successfully validated or not
+ *
+ * @return $this
+ */
+ public function info($message, $markAsError = true)
+ {
+ if ($this->getIsApiTarget()) {
+ $this->addErrorMessage($message);
+ } else {
+ $this->addNotification($message, self::NOTIFICATION_INFO);
+ }
+
+ if ($markAsError) {
+ $this->markAsError();
+ }
+
+ return $this;
+ }
+}
diff --git a/library/Icinga/Web/Form/Decorator/Autosubmit.php b/library/Icinga/Web/Form/Decorator/Autosubmit.php
new file mode 100644
index 0000000..4405d0b
--- /dev/null
+++ b/library/Icinga/Web/Form/Decorator/Autosubmit.php
@@ -0,0 +1,133 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Decorator;
+
+use Zend_Form_Decorator_Abstract;
+use Icinga\Application\Icinga;
+use Icinga\Web\View;
+use Icinga\Web\Form;
+
+/**
+ * Decorator to add an icon and a submit button encapsulated in noscript-tags
+ *
+ * The icon is shown in JS environments to indicate that a specific form field does automatically request an update
+ * of its form upon it has changed. The button allows users in non-JS environments to trigger the update manually.
+ */
+class Autosubmit extends Zend_Form_Decorator_Abstract
+{
+ /**
+ * Whether a hidden <span> should be created with the same warning as in the icon label
+ *
+ * @var bool
+ */
+ protected $accessible;
+
+ /**
+ * The id used to identify the auto-submit warning associated with the decorated form element
+ *
+ * @var string
+ */
+ protected $warningId;
+
+ /**
+ * Set whether a hidden <span> should be created with the same warning as in the icon label
+ *
+ * @param bool $state
+ *
+ * @return Autosubmit
+ */
+ public function setAccessible($state = true)
+ {
+ $this->accessible = (bool) $state;
+ return $this;
+ }
+
+ /**
+ * Return whether a hidden <span> is being created with the same warning as in the icon label
+ *
+ * @return bool
+ */
+ public function getAccessible()
+ {
+ if ($this->accessible === null) {
+ $this->accessible = $this->getOption('accessible') ?: false;
+ }
+
+ return $this->accessible;
+ }
+
+ /**
+ * Return the id used to identify the auto-submit warning associated with the decorated element
+ *
+ * @param mixed $element The element for which to generate a id
+ *
+ * @return string
+ */
+ public function getWarningId($element = null)
+ {
+ if ($this->warningId === null) {
+ $element = $element ?: $this->getElement();
+ $this->warningId = 'autosubmit_warning_' . $element->getId();
+ }
+
+ return $this->warningId;
+ }
+
+ /**
+ * Return the current view
+ *
+ * @return View
+ */
+ protected function getView()
+ {
+ return Icinga::app()->getViewRenderer()->view;
+ }
+
+ /**
+ * Add a auto-submit icon and submit button encapsulated in noscript-tags to the element
+ *
+ * @param string $content The html rendered so far
+ *
+ * @return string The updated html
+ */
+ public function render($content = '')
+ {
+ if ($content) {
+ $isForm = $this->getElement() instanceof Form;
+ $warning = $isForm
+ ? t('This page will be automatically updated upon change of any of this form\'s fields')
+ : t('This page will be automatically updated upon change of the value');
+ $content .= $this->getView()->icon('cw', $warning, array(
+ 'aria-hidden' => $isForm ? 'false' : 'true',
+ 'class' => 'spinner autosubmit-info'
+ ));
+ if (! $isForm && $this->getAccessible()) {
+ $content = '<span id="'
+ . $this->getWarningId()
+ . '" class="sr-only">'
+ . $warning
+ . '</span>'
+ . $content;
+ }
+
+ $content .= sprintf(
+ '<noscript><button'
+ . ' name="noscript_apply"'
+ . ' class="noscript-apply"'
+ . ' type="submit"'
+ . ' value="1"'
+ . ($this->getAccessible() ? ' aria-label="%1$s"' : '')
+ . ' title="%1$s"'
+ . '>%2$s</button></noscript>',
+ $isForm
+ ? t('Push this button to update the form to reflect the changes that were made below')
+ : t('Push this button to update the form to reflect the change'
+ . ' that was made in the field on the left'),
+ $this->getView()->icon('cw') . t('Apply')
+ );
+ }
+
+ return $content;
+ }
+}
diff --git a/library/Icinga/Web/Form/Decorator/ConditionalHidden.php b/library/Icinga/Web/Form/Decorator/ConditionalHidden.php
new file mode 100644
index 0000000..0f84535
--- /dev/null
+++ b/library/Icinga/Web/Form/Decorator/ConditionalHidden.php
@@ -0,0 +1,35 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Decorator;
+
+use Zend_Form_Decorator_Abstract;
+
+/**
+ * Decorator to hide elements using a &gt;noscript&lt; tag instead of
+ * type='hidden' or css styles.
+ *
+ * This allows to hide depending elements for browsers with javascript
+ * (who can then automatically refresh their pages) but show them in
+ * case JavaScript is disabled
+ */
+class ConditionalHidden extends Zend_Form_Decorator_Abstract
+{
+ /**
+ * Generate a field that will be wrapped in <noscript> tag if the
+ * "condition" attribute is set and false or 0
+ *
+ * @param string $content The tag's content
+ *
+ * @return string The generated tag
+ */
+ public function render($content = '')
+ {
+ $attributes = $this->getElement()->getAttribs();
+ $condition = isset($attributes['condition']) ? $attributes['condition'] : 1;
+ if ($condition != 1) {
+ $content = '<noscript>' . $content . '</noscript>';
+ }
+ return $content;
+ }
+}
diff --git a/library/Icinga/Web/Form/Decorator/ElementDoubler.php b/library/Icinga/Web/Form/Decorator/ElementDoubler.php
new file mode 100644
index 0000000..2da5646
--- /dev/null
+++ b/library/Icinga/Web/Form/Decorator/ElementDoubler.php
@@ -0,0 +1,63 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Decorator;
+
+use Zend_Form_Element;
+use Zend_Form_Decorator_Abstract;
+
+/**
+ * A decorator that will double a single element of a display group
+ *
+ * The options `condition', `double' and `attributes' can be passed to the constructor and are used to affect whether
+ * the doubling should take effect, which element should be doubled and which HTML attributes should be applied to the
+ * doubled element, respectively.
+ *
+ * `condition' must be an element's name that when it's part of the display group causes the condition to be met.
+ * `double' must be an element's name and must be part of the display group.
+ * `attributes' is just an array of key-value pairs.
+ *
+ * You can also pass `placement' to control whether the doubled element is prepended or appended.
+ */
+class ElementDoubler extends Zend_Form_Decorator_Abstract
+{
+ /**
+ * Return the display group's elements with an additional copy of an element being added if the condition is met
+ *
+ * @param string $content The HTML rendered so far
+ *
+ * @return string
+ */
+ public function render($content)
+ {
+ $group = $this->getElement();
+ if ($group->getElement($this->getOption('condition')) !== null) {
+ if ($this->getPlacement() === static::APPEND) {
+ return $content . $this->applyAttributes($group->getElement($this->getOption('double')))->render();
+ } else { // $this->getPlacement() === static::PREPEND
+ return $this->applyAttributes($group->getElement($this->getOption('double')))->render() . $content;
+ }
+ }
+
+ return $content;
+ }
+
+ /**
+ * Apply all element attributes
+ *
+ * @param Zend_Form_Element $element The element to apply the attributes to
+ *
+ * @return Zend_Form_Element
+ */
+ protected function applyAttributes(Zend_Form_Element $element)
+ {
+ $attributes = $this->getOption('attributes');
+ if ($attributes !== null) {
+ foreach ($attributes as $name => $value) {
+ $element->setAttrib($name, $value);
+ }
+ }
+
+ return $element;
+ }
+}
diff --git a/library/Icinga/Web/Form/Decorator/FormDescriptions.php b/library/Icinga/Web/Form/Decorator/FormDescriptions.php
new file mode 100644
index 0000000..88ea5d9
--- /dev/null
+++ b/library/Icinga/Web/Form/Decorator/FormDescriptions.php
@@ -0,0 +1,76 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Decorator;
+
+use Icinga\Application\Icinga;
+use Icinga\Web\Form;
+use Zend_Form_Decorator_Abstract;
+
+/**
+ * Decorator to add a list of descriptions at the top or bottom of a form
+ */
+class FormDescriptions extends Zend_Form_Decorator_Abstract
+{
+ /**
+ * Render form descriptions
+ *
+ * @param string $content The html rendered so far
+ *
+ * @return string The updated html
+ */
+ public function render($content = '')
+ {
+ $form = $this->getElement();
+ if (! $form instanceof Form) {
+ return $content;
+ }
+
+ $view = $form->getView();
+ if ($view === null) {
+ return $content;
+ }
+
+ $descriptions = $this->recurseForm($form);
+ if (empty($descriptions)) {
+ return $content;
+ }
+
+ $html = '<div class="form-description">'
+ . Icinga::app()->getViewRenderer()->view->icon('info-circled', '', ['class' => 'form-description-icon'])
+ . '<ul class="form-description-list">';
+
+ foreach ($descriptions as $description) {
+ if (is_array($description)) {
+ list($description, $properties) = $description;
+ $html .= '<li' . $view->propertiesToString($properties) . '>' . $view->escape($description) . '</li>';
+ } else {
+ $html .= '<li>' . $view->escape($description) . '</li>';
+ }
+ }
+
+ switch ($this->getPlacement()) {
+ case self::APPEND:
+ return $content . $html . '</ul></div>';
+ case self::PREPEND:
+ return $html . '</ul></div>' . $content;
+ }
+ }
+
+ /**
+ * Recurse the given form and return the descriptions for it and all of its subforms
+ *
+ * @param Form $form The form to recurse
+ *
+ * @return array
+ */
+ protected function recurseForm(Form $form)
+ {
+ $descriptions = array($form->getDescriptions());
+ foreach ($form->getSubForms() as $subForm) {
+ $descriptions[] = $this->recurseForm($subForm);
+ }
+
+ return call_user_func_array('array_merge', $descriptions);
+ }
+}
diff --git a/library/Icinga/Web/Form/Decorator/FormHints.php b/library/Icinga/Web/Form/Decorator/FormHints.php
new file mode 100644
index 0000000..797be26
--- /dev/null
+++ b/library/Icinga/Web/Form/Decorator/FormHints.php
@@ -0,0 +1,142 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Decorator;
+
+use Zend_Form_Decorator_Abstract;
+use Icinga\Web\Form;
+
+/**
+ * Decorator to add a list of hints at the top or bottom of a form
+ *
+ * The hint for required form elements is automatically being handled.
+ */
+class FormHints extends Zend_Form_Decorator_Abstract
+{
+ /**
+ * A list of element class names to be ignored when detecting which message to use to describe required elements
+ *
+ * @var array
+ */
+ protected $blacklist;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct($options = null)
+ {
+ parent::__construct($options);
+ $this->blacklist = array(
+ 'Zend_Form_Element_Hidden',
+ 'Zend_Form_Element_Submit',
+ 'Zend_Form_Element_Button',
+ 'Icinga\Web\Form\Element\Note',
+ 'Icinga\Web\Form\Element\Button',
+ 'Icinga\Web\Form\Element\CsrfCounterMeasure'
+ );
+ }
+
+ /**
+ * Render form hints
+ *
+ * @param string $content The html rendered so far
+ *
+ * @return string The updated html
+ */
+ public function render($content = '')
+ {
+ $form = $this->getElement();
+ if (! $form instanceof Form) {
+ return $content;
+ }
+
+ $view = $form->getView();
+ if ($view === null) {
+ return $content;
+ }
+
+ $hints = $this->recurseForm($form, $entirelyRequired);
+ if ($entirelyRequired !== null) {
+ $hints[] = sprintf(
+ $form->getView()->translate('%s Required field'),
+ $form->getRequiredCue()
+ );
+ }
+
+ if (empty($hints)) {
+ return $content;
+ }
+
+ $html = '<ul class="form-info">';
+ foreach ($hints as $hint) {
+ if (is_array($hint)) {
+ list($hint, $properties) = $hint;
+ $html .= '<li' . $view->propertiesToString($properties) . '>' . $view->escape($hint) . '</li>';
+ } else {
+ $html .= '<li>' . $view->escape($hint) . '</li>';
+ }
+ }
+
+ switch ($this->getPlacement()) {
+ case self::APPEND:
+ return $content . $html . '</ul>';
+ case self::PREPEND:
+ return $html . '</ul>' . $content;
+ }
+ }
+
+ /**
+ * Recurse the given form and return the hints for it and all of its subforms
+ *
+ * @param Form $form The form to recurse
+ * @param mixed $entirelyRequired Set by reference, true means all elements in the hierarchy are
+ * required, false only a partial subset and null none at all
+ * @param bool $elementsPassed Whether there were any elements passed during the recursion until now
+ *
+ * @return array
+ */
+ protected function recurseForm(Form $form, &$entirelyRequired = null, $elementsPassed = false)
+ {
+ $requiredLabels = array();
+ if ($form->getRequiredCue() !== null) {
+ $partiallyRequired = $partiallyOptional = false;
+ foreach ($form->getElements() as $element) {
+ if (! in_array($element->getType(), $this->blacklist)) {
+ if (! $element->isRequired()) {
+ $partiallyOptional = true;
+ if ($entirelyRequired) {
+ $entirelyRequired = false;
+ }
+ } else {
+ $partiallyRequired = true;
+ if (($label = $element->getDecorator('label')) !== false) {
+ $requiredLabels[] = $label;
+ }
+ }
+ }
+ }
+
+ if (! $elementsPassed) {
+ $elementsPassed = $partiallyRequired || $partiallyOptional;
+ if ($entirelyRequired === null && $partiallyRequired) {
+ $entirelyRequired = ! $partiallyOptional;
+ }
+ } elseif ($entirelyRequired === null && $partiallyRequired) {
+ $entirelyRequired = false;
+ }
+ }
+
+ $hints = array($form->getHints());
+ foreach ($form->getSubForms() as $subForm) {
+ $hints[] = $this->recurseForm($subForm, $entirelyRequired, $elementsPassed);
+ }
+
+ if ($entirelyRequired) {
+ foreach ($requiredLabels as $label) {
+ $label->setRequiredSuffix('');
+ }
+ }
+
+ return call_user_func_array('array_merge', $hints);
+ }
+}
diff --git a/library/Icinga/Web/Form/Decorator/FormNotifications.php b/library/Icinga/Web/Form/Decorator/FormNotifications.php
new file mode 100644
index 0000000..46734df
--- /dev/null
+++ b/library/Icinga/Web/Form/Decorator/FormNotifications.php
@@ -0,0 +1,125 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Decorator;
+
+use Icinga\Application\Icinga;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Web\Form;
+use Zend_Form_Decorator_Abstract;
+
+/**
+ * Decorator to add a list of notifications at the top or bottom of a form
+ */
+class FormNotifications extends Zend_Form_Decorator_Abstract
+{
+ /**
+ * Render form notifications
+ *
+ * @param string $content The html rendered so far
+ *
+ * @return string The updated html
+ */
+ public function render($content = '')
+ {
+ $form = $this->getElement();
+ if (! $form instanceof Form) {
+ return $content;
+ }
+
+ $view = $form->getView();
+ if ($view === null) {
+ return $content;
+ }
+
+ $notifications = $this->recurseForm($form);
+ if (empty($notifications)) {
+ return $content;
+ }
+
+ $html = '<ul class="form-notification-list">';
+ foreach (array(Form::NOTIFICATION_ERROR, Form::NOTIFICATION_WARNING, Form::NOTIFICATION_INFO) as $type) {
+ if (isset($notifications[$type])) {
+ $html .= '<li><ul class="notification-' . $this->getNotificationTypeName($type) . '">';
+ foreach ($notifications[$type] as $message) {
+ if (is_array($message)) {
+ list($message, $properties) = $message;
+ $html .= '<li' . $view->propertiesToString($properties) . '>'
+ . $view->escape($message)
+ . '</li>';
+ } else {
+ $html .= '<li>' . $view->escape($message) . '</li>';
+ }
+ }
+
+ $html .= '</ul></li>';
+ }
+ }
+
+ if (isset($notifications[Form::NOTIFICATION_ERROR])) {
+ $icon = 'cancel';
+ $class = 'error';
+ } elseif (isset($notifications[Form::NOTIFICATION_WARNING])) {
+ $icon = 'warning-empty';
+ $class = 'warning';
+ } else {
+ $icon = 'info';
+ $class = 'info';
+ }
+
+ $html = "<div class=\"form-notifications $class\">"
+ . Icinga::app()->getViewRenderer()->view->icon($icon, '', ['class' => 'form-notification-icon'])
+ . $html;
+
+ switch ($this->getPlacement()) {
+ case self::APPEND:
+ return $content . $html . '</ul></div>';
+ case self::PREPEND:
+ return $html . '</ul></div>' . $content;
+ }
+ }
+
+ /**
+ * Recurse the given form and return the notifications for it and all of its subforms
+ *
+ * @param Form $form The form to recurse
+ *
+ * @return array
+ */
+ protected function recurseForm(Form $form)
+ {
+ $notifications = $form->getNotifications();
+ foreach ($form->getSubForms() as $subForm) {
+ foreach ($this->recurseForm($subForm) as $type => $messages) {
+ foreach ($messages as $message) {
+ $notifications[$type][] = $message;
+ }
+ }
+ }
+
+ return $notifications;
+ }
+
+ /**
+ * Return the name for the given notification type
+ *
+ * @param int $type
+ *
+ * @return string
+ *
+ * @throws ProgrammingError In case the given type is invalid
+ */
+ protected function getNotificationTypeName($type)
+ {
+ switch ($type) {
+ case Form::NOTIFICATION_ERROR:
+ return 'error';
+ case Form::NOTIFICATION_WARNING:
+ return 'warning';
+ case Form::NOTIFICATION_INFO:
+ return 'info';
+ default:
+ throw new ProgrammingError('Invalid notification type "%s" provided', $type);
+ }
+ }
+}
diff --git a/library/Icinga/Web/Form/Decorator/Help.php b/library/Icinga/Web/Form/Decorator/Help.php
new file mode 100644
index 0000000..c521abe
--- /dev/null
+++ b/library/Icinga/Web/Form/Decorator/Help.php
@@ -0,0 +1,113 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Decorator;
+
+use Zend_Form_Element;
+use Zend_Form_Decorator_Abstract;
+use Icinga\Application\Icinga;
+use Icinga\Web\View;
+
+/**
+ * Decorator to add helptext to a form element
+ */
+class Help extends Zend_Form_Decorator_Abstract
+{
+ /**
+ * Whether a hidden <span> should be created to describe the decorated form element
+ *
+ * @var bool
+ */
+ protected $accessible = false;
+
+ /**
+ * The id used to identify the description associated with the decorated form element
+ *
+ * @var string
+ */
+ protected $descriptionId;
+
+ /**
+ * Set whether a hidden <span> should be created to describe the decorated form element
+ *
+ * @param bool $state
+ *
+ * @return Help
+ */
+ public function setAccessible($state = true)
+ {
+ $this->accessible = (bool) $state;
+ return $this;
+ }
+
+ /**
+ * Return the id used to identify the description associated with the decorated element
+ *
+ * @param Zend_Form_Element $element The element for which to generate a id
+ *
+ * @return string
+ */
+ public function getDescriptionId(Zend_Form_Element $element = null)
+ {
+ if ($this->descriptionId === null) {
+ $element = $element ?: $this->getElement();
+ $this->descriptionId = 'desc_' . $element->getId();
+ }
+
+ return $this->descriptionId;
+ }
+
+ /**
+ * Return the current view
+ *
+ * @return View
+ */
+ protected function getView()
+ {
+ return Icinga::app()->getViewRenderer()->view;
+ }
+
+ /**
+ * Add a help icon to the left of an element
+ *
+ * @param string $content The html rendered so far
+ *
+ * @return string The updated html
+ */
+ public function render($content = '')
+ {
+ $element = $this->getElement();
+ $description = $element->getDescription();
+ $requirement = $element->getAttrib('requirement');
+ unset($element->requirement);
+
+ $helpContent = '';
+ if ($description || $requirement) {
+ if ($this->accessible) {
+ $helpContent = '<span id="'
+ . $this->getDescriptionId()
+ . '" class="sr-only">'
+ . $description
+ . ($description && $requirement ? ' ' : '')
+ . $requirement
+ . '</span>';
+ }
+
+ $helpContent = $this->getView()->icon(
+ 'info-circled',
+ $description . ($description && $requirement ? ' ' : '') . $requirement,
+ array(
+ 'class' => 'control-info',
+ 'aria-hidden' => $this->accessible ? 'true' : 'false'
+ )
+ ) . $helpContent;
+ }
+
+ switch ($this->getPlacement()) {
+ case self::APPEND:
+ return $content . $helpContent;
+ case self::PREPEND:
+ return $helpContent . $content;
+ }
+ }
+}
diff --git a/library/Icinga/Web/Form/Decorator/Spinner.php b/library/Icinga/Web/Form/Decorator/Spinner.php
new file mode 100644
index 0000000..9cfa568
--- /dev/null
+++ b/library/Icinga/Web/Form/Decorator/Spinner.php
@@ -0,0 +1,48 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Decorator;
+
+use Zend_Form_Decorator_Abstract;
+use Icinga\Application\Icinga;
+use Icinga\Web\View;
+
+/**
+ * Decorator to add a spinner next to an element
+ */
+class Spinner extends Zend_Form_Decorator_Abstract
+{
+ /**
+ * Return the current view
+ *
+ * @return View
+ */
+ protected function getView()
+ {
+ return Icinga::app()->getViewRenderer()->view;
+ }
+
+ /**
+ * Add a spinner icon to a form element
+ *
+ * @param string $content The html rendered so far
+ *
+ * @return string The updated html
+ */
+ public function render($content = '')
+ {
+ $spinner = '<div '
+ . ($this->getOption('id') !== null ? ' id="' . $this->getOption('id') . '"' : '')
+ . 'class="spinner ' . ($this->getOption('class') ?: '') . '"'
+ . '>'
+ . $this->getView()->icon('spin6')
+ . '</div>';
+
+ switch ($this->getPlacement()) {
+ case self::APPEND:
+ return $content . $spinner;
+ case self::PREPEND:
+ return $spinner . $content;
+ }
+ }
+}
diff --git a/library/Icinga/Web/Form/Element/Button.php b/library/Icinga/Web/Form/Element/Button.php
new file mode 100644
index 0000000..9cbe915
--- /dev/null
+++ b/library/Icinga/Web/Form/Element/Button.php
@@ -0,0 +1,80 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Element;
+
+use Icinga\Web\Request;
+use Icinga\Application\Icinga;
+use Icinga\Web\Form\FormElement;
+
+/**
+ * A button
+ */
+class Button extends FormElement
+{
+ /**
+ * Use formButton view helper by default
+ *
+ * @var string
+ */
+ public $helper = 'formButton';
+
+ /**
+ * Constructor
+ *
+ * @param string|array|Zend_Config $spec Element name or configuration
+ * @param string|array|Zend_Config $options Element value or configuration
+ */
+ public function __construct($spec, $options = null)
+ {
+ if (is_string($spec) && ((null !== $options) && is_string($options))) {
+ $options = array('label' => $options);
+ }
+
+ if (!isset($options['ignore'])) {
+ $options['ignore'] = true;
+ }
+
+ parent::__construct($spec, $options);
+
+ if ($label = $this->getLabel()) {
+ // Necessary to get the label shown on the generated HTML
+ $this->content = $label;
+ }
+ }
+
+ /**
+ * Validate element value (pseudo)
+ *
+ * There is no need to reset the value
+ *
+ * @param mixed $value Is always ignored
+ * @param mixed $context Is always ignored
+ *
+ * @return bool Returns always TRUE
+ */
+ public function isValid($value, $context = null)
+ {
+ return true;
+ }
+
+ /**
+ * Has this button been selected?
+ *
+ * @return bool
+ */
+ public function isChecked()
+ {
+ return $this->getRequest()->getParam($this->getName()) === $this->getValue();
+ }
+
+ /**
+ * Return the current request
+ *
+ * @return Request
+ */
+ protected function getRequest()
+ {
+ return Icinga::app()->getRequest();
+ }
+}
diff --git a/library/Icinga/Web/Form/Element/Checkbox.php b/library/Icinga/Web/Form/Element/Checkbox.php
new file mode 100644
index 0000000..d4499a0
--- /dev/null
+++ b/library/Icinga/Web/Form/Element/Checkbox.php
@@ -0,0 +1,9 @@
+<?php
+/* Icinga Web 2 | (c) 2019 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Web\Form\Element;
+
+class Checkbox extends \Zend_Form_Element_Checkbox
+{
+ public $helper = 'icingaCheckbox';
+}
diff --git a/library/Icinga/Web/Form/Element/CsrfCounterMeasure.php b/library/Icinga/Web/Form/Element/CsrfCounterMeasure.php
new file mode 100644
index 0000000..c59e1f9
--- /dev/null
+++ b/library/Icinga/Web/Form/Element/CsrfCounterMeasure.php
@@ -0,0 +1,99 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Element;
+
+use Icinga\Web\Session;
+use Icinga\Web\Form\FormElement;
+use Icinga\Web\Form\InvalidCSRFTokenException;
+
+/**
+ * CSRF counter measure element
+ *
+ * You must not set a value to successfully use this element, just give it a name and you're good to go.
+ */
+class CsrfCounterMeasure extends FormElement
+{
+ /**
+ * Default form view helper to use for rendering
+ *
+ * @var string
+ */
+ public $helper = 'formHidden';
+
+ /**
+ * Counter measure element is required
+ *
+ * @var bool
+ */
+ protected $_ignore = true;
+
+ /**
+ * Ignore element when retrieving values at form level
+ *
+ * @var bool
+ */
+ protected $_required = true;
+
+ /**
+ * Initialize this form element
+ */
+ public function init()
+ {
+ $this->setDecorators(['ViewHelper']);
+ $this->setValue($this->generateCsrfToken());
+ }
+
+ /**
+ * Check whether $value is a valid CSRF token
+ *
+ * @param string $value The value to check
+ * @param mixed $context Context to use
+ *
+ * @return bool True, in case the CSRF token is valid
+ *
+ * @throws InvalidCSRFTokenException In case the CSRF token is not valid
+ */
+ public function isValid($value, $context = null)
+ {
+ if (parent::isValid($value, $context) && $this->isValidCsrfToken($value)) {
+ return true;
+ }
+
+ throw new InvalidCSRFTokenException();
+ }
+
+ /**
+ * Check whether the given value is a valid CSRF token for the current session
+ *
+ * @param string $token The CSRF token
+ *
+ * @return bool
+ */
+ protected function isValidCsrfToken($token)
+ {
+ if (strpos($token, '|') === false) {
+ return false;
+ }
+
+ list($seed, $hash) = explode('|', $token);
+
+ if (false === is_numeric($seed)) {
+ return false;
+ }
+
+ return $hash === hash('sha256', Session::getSession()->getId() . $seed);
+ }
+
+ /**
+ * Generate a new (seed, token) pair
+ *
+ * @return string
+ */
+ protected function generateCsrfToken()
+ {
+ $seed = mt_rand();
+ $hash = hash('sha256', Session::getSession()->getId() . $seed);
+ return sprintf('%s|%s', $seed, $hash);
+ }
+}
diff --git a/library/Icinga/Web/Form/Element/Date.php b/library/Icinga/Web/Form/Element/Date.php
new file mode 100644
index 0000000..8e0985c
--- /dev/null
+++ b/library/Icinga/Web/Form/Element/Date.php
@@ -0,0 +1,19 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Element;
+
+use Icinga\Web\Form\FormElement;
+
+/**
+ * A date input control
+ */
+class Date extends FormElement
+{
+ /**
+ * Form view helper to use for rendering
+ *
+ * @var string
+ */
+ public $helper = 'formDate';
+}
diff --git a/library/Icinga/Web/Form/Element/DateTimePicker.php b/library/Icinga/Web/Form/Element/DateTimePicker.php
new file mode 100644
index 0000000..284a744
--- /dev/null
+++ b/library/Icinga/Web/Form/Element/DateTimePicker.php
@@ -0,0 +1,80 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Element;
+
+use DateTime;
+use Icinga\Web\Form\FormElement;
+use Icinga\Web\Form\Validator\DateTimeValidator;
+
+/**
+ * A date-and-time input control
+ */
+class DateTimePicker extends FormElement
+{
+ /**
+ * Form view helper to use for rendering
+ *
+ * @var string
+ */
+ public $helper = 'formDateTime';
+
+ /**
+ * @var bool
+ */
+ protected $local = true;
+
+ /**
+ * (non-PHPDoc)
+ * @see Zend_Form_Element::init() For the method documentation.
+ */
+ public function init()
+ {
+ $this->addValidator(
+ new DateTimeValidator($this->local),
+ true // true for breaking the validator chain on failure
+ );
+ }
+
+ /**
+ * Get the expected date and time format of any user input
+ *
+ * @return string
+ */
+ public function getFormat()
+ {
+ return $this->local ? 'Y-m-d\TH:i:s' : DateTime::RFC3339;
+ }
+
+ /**
+ * Is the date and time valid?
+ *
+ * @param string|DateTime $value
+ * @param mixed $context
+ *
+ * @return bool
+ */
+ public function isValid($value, $context = null)
+ {
+ if (is_scalar($value) && $value !== '' && ! preg_match('/\D/', $value)) {
+ $dateTime = new DateTime();
+ $value = $dateTime->setTimestamp($value)->format($this->getFormat());
+ }
+
+ if (! parent::isValid($value, $context)) {
+ return false;
+ }
+
+ if (! $value instanceof DateTime) {
+ $format = $this->getFormat();
+ $dateTime = DateTime::createFromFormat($format, $value);
+ if ($dateTime === false) {
+ $dateTime = DateTime::createFromFormat(substr($format, 0, strrpos($format, ':')), $value);
+ }
+
+ $this->setValue($dateTime);
+ }
+
+ return true;
+ }
+}
diff --git a/library/Icinga/Web/Form/Element/Note.php b/library/Icinga/Web/Form/Element/Note.php
new file mode 100644
index 0000000..9569dee
--- /dev/null
+++ b/library/Icinga/Web/Form/Element/Note.php
@@ -0,0 +1,55 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Element;
+
+use Icinga\Web\Form\FormElement;
+
+/**
+ * A note
+ */
+class Note extends FormElement
+{
+ /**
+ * Form view helper to use for rendering
+ *
+ * @var string
+ */
+ public $helper = 'formNote';
+
+ /**
+ * Ignore element when retrieving values at form level
+ *
+ * @var bool
+ */
+ protected $_ignore = true;
+
+ /**
+ * (non-PHPDoc)
+ * @see Zend_Form_Element::init() For the method documentation.
+ */
+ public function init()
+ {
+ if (count($this->getDecorators()) === 0) {
+ $this->setDecorators(array(
+ 'ViewHelper',
+ array(
+ 'HtmlTag',
+ array('tag' => 'p')
+ )
+ ));
+ }
+ }
+
+ /**
+ * Validate element value (pseudo)
+ *
+ * @param mixed $value Ignored
+ *
+ * @return bool Always true
+ */
+ public function isValid($value, $context = null)
+ {
+ return true;
+ }
+}
diff --git a/library/Icinga/Web/Form/Element/Number.php b/library/Icinga/Web/Form/Element/Number.php
new file mode 100644
index 0000000..afbd07d
--- /dev/null
+++ b/library/Icinga/Web/Form/Element/Number.php
@@ -0,0 +1,144 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Element;
+
+use Icinga\Web\Form\FormElement;
+
+/**
+ * A number input control
+ */
+class Number extends FormElement
+{
+ /**
+ * Form view helper to use for rendering
+ *
+ * @var string
+ */
+ public $helper = 'formNumber';
+
+ /**
+ * The expected lower bound for the element’s value
+ *
+ * @var float|null
+ */
+ protected $min;
+
+ /**
+ * The expected upper bound for the element’s
+ *
+ * @var float|null
+ */
+ protected $max;
+
+ /**
+ * The value granularity of the element’s value
+ *
+ * Normally, number input controls are limited to an accuracy of integer values.
+ *
+ * @var float|string|null
+ */
+ protected $step;
+
+ /**
+ * (non-PHPDoc)
+ * @see \Zend_Form_Element::init() For the method documentation.
+ */
+ public function init()
+ {
+ if ($this->min !== null || $this->max !== null) {
+ $this->addValidator('Between', true, array(
+ 'min' => $this->min === null ? -INF : $this->min,
+ 'max' => $this->max === null ? INF : $this->max,
+ 'inclusive' => true
+ ));
+ }
+ }
+
+ /**
+ * Set the expected lower bound for the element’s value
+ *
+ * @param float $min
+ *
+ * @return $this
+ */
+ public function setMin($min)
+ {
+ $this->min = (float) $min;
+ return $this;
+ }
+
+ /**
+ * Get the expected lower bound for the element’s value
+ *
+ * @return float|null
+ */
+ public function getMin()
+ {
+ return $this->min;
+ }
+
+ /**
+ * Set the expected upper bound for the element’s value
+ *
+ * @param float $max
+ *
+ * @return $this
+ */
+ public function setMax($max)
+ {
+ $this->max = (float) $max;
+ return $this;
+ }
+
+ /**
+ * Get the expected upper bound for the element’s value
+ *
+ * @return float|null
+ */
+ public function getMax()
+ {
+ return $this->max;
+ }
+
+ /**
+ * Set the value granularity of the element’s value
+ *
+ * @param float|string $step
+ *
+ * @return $this
+ */
+ public function setStep($step)
+ {
+ if ($step !== 'any') {
+ $step = (float) $step;
+ }
+ $this->step = $step;
+ return $this;
+ }
+
+ /**
+ * Get the value granularity of the element’s value
+ *
+ * @return float|string|null
+ */
+ public function getStep()
+ {
+ return $this->step;
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see \Zend_Form_Element::isValid() For the method documentation.
+ */
+ public function isValid($value, $context = null)
+ {
+ $this->setValue($value);
+ $value = $this->getValue();
+ if ($value !== null && $value !== '' && ! is_numeric($value)) {
+ $this->addError(sprintf(t('\'%s\' is not a valid number'), $value));
+ return false;
+ }
+ return parent::isValid($value, $context);
+ }
+}
diff --git a/library/Icinga/Web/Form/Element/Textarea.php b/library/Icinga/Web/Form/Element/Textarea.php
new file mode 100644
index 0000000..119cd56
--- /dev/null
+++ b/library/Icinga/Web/Form/Element/Textarea.php
@@ -0,0 +1,20 @@
+<?php
+/* Icinga Web 2 | (c) 2019 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Element;
+
+use Icinga\Web\Form\FormElement;
+
+class Textarea extends FormElement
+{
+ public $helper = 'formTextarea';
+
+ public function __construct($spec, $options = null)
+ {
+ parent::__construct($spec, $options);
+
+ if ($this->getAttrib('rows') === null) {
+ $this->setAttrib('rows', 3);
+ }
+ }
+}
diff --git a/library/Icinga/Web/Form/Element/Time.php b/library/Icinga/Web/Form/Element/Time.php
new file mode 100644
index 0000000..4b76a33
--- /dev/null
+++ b/library/Icinga/Web/Form/Element/Time.php
@@ -0,0 +1,19 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Element;
+
+use Icinga\Web\Form\FormElement;
+
+/**
+ * A time input control
+ */
+class Time extends FormElement
+{
+ /**
+ * Form view helper to use for rendering
+ *
+ * @var string
+ */
+ public $helper = 'formTime';
+}
diff --git a/library/Icinga/Web/Form/ErrorLabeller.php b/library/Icinga/Web/Form/ErrorLabeller.php
new file mode 100644
index 0000000..3f822d5
--- /dev/null
+++ b/library/Icinga/Web/Form/ErrorLabeller.php
@@ -0,0 +1,71 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form;
+
+use BadMethodCallException;
+use Zend_Translate_Adapter;
+use Zend_Validate_NotEmpty;
+use Zend_Validate_File_MimeType;
+use Icinga\Web\Form\Validator\DateTimeValidator;
+use Icinga\Web\Form\Validator\ReadablePathValidator;
+use Icinga\Web\Form\Validator\WritablePathValidator;
+
+class ErrorLabeller extends Zend_Translate_Adapter
+{
+ protected $messages;
+
+ public function __construct($options = array())
+ {
+ if (! isset($options['element'])) {
+ throw new BadMethodCallException('Option "element" is missing');
+ }
+
+ $this->messages = $this->createMessages($options['element']);
+ }
+
+ public function isTranslated($messageId, $original = false, $locale = null)
+ {
+ return array_key_exists($messageId, $this->messages);
+ }
+
+ public function translate($messageId, $locale = null)
+ {
+ if (array_key_exists($messageId, $this->messages)) {
+ return $this->messages[$messageId];
+ }
+
+ return $messageId;
+ }
+
+ protected function createMessages($element)
+ {
+ $label = $element->getLabel() ?: $element->getName();
+
+ return array(
+ Zend_Validate_NotEmpty::IS_EMPTY => sprintf(t('%s is required and must not be empty'), $label),
+ Zend_Validate_File_MimeType::FALSE_TYPE => sprintf(
+ t('%s (%%value%%) has a false MIME type of "%%type%%"'),
+ $label
+ ),
+ Zend_Validate_File_MimeType::NOT_DETECTED => sprintf(t('%s (%%value%%) has no MIME type'), $label),
+ WritablePathValidator::NOT_WRITABLE => sprintf(t('%s is not writable', 'config.path'), $label),
+ WritablePathValidator::DOES_NOT_EXIST => sprintf(t('%s does not exist', 'config.path'), $label),
+ ReadablePathValidator::NOT_READABLE => sprintf(t('%s is not readable', 'config.path'), $label),
+ DateTimeValidator::INVALID_DATETIME_FORMAT => sprintf(
+ t('%s not in the expected format: %%value%%'),
+ $label
+ )
+ );
+ }
+
+ protected function _loadTranslationData($data, $locale, array $options = array())
+ {
+ // nonsense, required as being abstract otherwise...
+ }
+
+ public function toString()
+ {
+ return 'ErrorLabeller'; // nonsense, required as being abstract otherwise...
+ }
+}
diff --git a/library/Icinga/Web/Form/FormElement.php b/library/Icinga/Web/Form/FormElement.php
new file mode 100644
index 0000000..0ac2ee4
--- /dev/null
+++ b/library/Icinga/Web/Form/FormElement.php
@@ -0,0 +1,61 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form;
+
+use Zend_Form_Element;
+use Icinga\Web\Form;
+
+/**
+ * Base class for Icinga Web 2 form elements
+ */
+class FormElement extends Zend_Form_Element
+{
+ /**
+ * Whether loading default decorators is disabled
+ *
+ * Icinga Web 2 loads its own default element decorators. For loading Zend's default element decorators set this
+ * property to false.
+ *
+ * @var null|bool
+ */
+ protected $_disableLoadDefaultDecorators;
+
+ /**
+ * Whether loading default decorators is disabled
+ *
+ * @return bool
+ */
+ public function loadDefaultDecoratorsIsDisabled()
+ {
+ return $this->_disableLoadDefaultDecorators === true;
+ }
+
+ /**
+ * Load default decorators
+ *
+ * Icinga Web 2 loads its own default element decorators. For loading Zend's default element decorators set
+ * FormElement::$_disableLoadDefaultDecorators to false.
+ *
+ * @return this
+ * @see Form::$defaultElementDecorators For Icinga Web 2's default element decorators.
+ */
+ public function loadDefaultDecorators()
+ {
+ if ($this->loadDefaultDecoratorsIsDisabled()) {
+ return $this;
+ }
+
+ if (! isset($this->_disableLoadDefaultDecorators)) {
+ $decorators = $this->getDecorators();
+ if (empty($decorators)) {
+ // Load Icinga Web 2's default element decorators
+ $this->addDecorators(Form::$defaultElementDecorators);
+ }
+ } else {
+ // Load Zend's default decorators
+ parent::loadDefaultDecorators();
+ }
+ return $this;
+ }
+}
diff --git a/library/Icinga/Web/Form/InvalidCSRFTokenException.php b/library/Icinga/Web/Form/InvalidCSRFTokenException.php
new file mode 100644
index 0000000..d0eb68a
--- /dev/null
+++ b/library/Icinga/Web/Form/InvalidCSRFTokenException.php
@@ -0,0 +1,11 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form;
+
+/**
+ * Exceptions for invalid form tokens
+ */
+class InvalidCSRFTokenException extends \Exception
+{
+}
diff --git a/library/Icinga/Web/Form/Validator/DateFormatValidator.php b/library/Icinga/Web/Form/Validator/DateFormatValidator.php
new file mode 100644
index 0000000..eacb29c
--- /dev/null
+++ b/library/Icinga/Web/Form/Validator/DateFormatValidator.php
@@ -0,0 +1,61 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Validator;
+
+use Zend_Validate_Abstract;
+
+/**
+ * Validator that checks if a textfield contains a correct date format
+ */
+class DateFormatValidator extends Zend_Validate_Abstract
+{
+
+ /**
+ * Valid date characters according to @see http://www.php.net/manual/en/function.date.php
+ *
+ * @var array
+ *
+ * @see http://www.php.net/manual/en/function.date.php
+ */
+ private $validChars =
+ array('d', 'D', 'j', 'l', 'N', 'S', 'w', 'z', 'W', 'F', 'm', 'M', 'n', 't', 'L', 'o', 'Y', 'y');
+
+ /**
+ * List of sensible time separators
+ *
+ * @var array
+ */
+ private $validSeparators = array(' ', ':', '-', '/', ';', ',', '.');
+
+ /**
+ * Error templates
+ *
+ * @var array
+ *
+ * @see Zend_Validate_Abstract::$_messageTemplates
+ */
+ protected $_messageTemplates = array(
+ 'INVALID_CHARACTERS' => 'Invalid date format'
+ );
+
+ /**
+ * Validate the input value
+ *
+ * @param string $value The format string to validate
+ * @param null $context The form context (ignored)
+ *
+ * @return bool True when the input is valid, otherwise false
+ *
+ * @see Zend_Validate_Abstract::isValid()
+ */
+ public function isValid($value, $context = null)
+ {
+ $rest = trim($value, join(' ', array_merge($this->validChars, $this->validSeparators)));
+ if (strlen($rest) > 0) {
+ $this->_error('INVALID_CHARACTERS');
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/library/Icinga/Web/Form/Validator/DateTimeValidator.php b/library/Icinga/Web/Form/Validator/DateTimeValidator.php
new file mode 100644
index 0000000..5ef327d
--- /dev/null
+++ b/library/Icinga/Web/Form/Validator/DateTimeValidator.php
@@ -0,0 +1,77 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Validator;
+
+use DateTime;
+use Zend_Validate_Abstract;
+
+/**
+ * Validator for date-and-time input controls
+ *
+ * @see \Icinga\Web\Form\Element\DateTimePicker For the date-and-time input control.
+ */
+class DateTimeValidator extends Zend_Validate_Abstract
+{
+ const INVALID_DATETIME_TYPE = 'invalidDateTimeType';
+ const INVALID_DATETIME_FORMAT = 'invalidDateTimeFormat';
+
+ /**
+ * The messages to write on differen error states
+ *
+ * @var array
+ *
+ * @see Zend_Validate_Abstract::$_messageTemplates‚
+ */
+ protected $_messageTemplates = array(
+ self::INVALID_DATETIME_TYPE => 'Invalid type given. Instance of DateTime or date/time string expected',
+ self::INVALID_DATETIME_FORMAT => 'Date/time string not in the expected format: %value%'
+ );
+
+ protected $local;
+
+ /**
+ * Create a new date-and-time input control validator
+ *
+ * @param bool $local
+ */
+ public function __construct($local)
+ {
+ $this->local = (bool) $local;
+ }
+
+ /**
+ * Is the date and time valid?
+ *
+ * @param string|DateTime $value
+ * @param mixed $context
+ *
+ * @return bool
+ *
+ * @see \Zend_Validate_Interface::isValid()
+ */
+ public function isValid($value, $context = null)
+ {
+ if (! $value instanceof DateTime && ! is_string($value)) {
+ $this->_error(self::INVALID_DATETIME_TYPE);
+ return false;
+ }
+
+ if (! $value instanceof DateTime) {
+ $format = $baseFormat = $this->local === true ? 'Y-m-d\TH:i:s' : DateTime::RFC3339;
+ $dateTime = DateTime::createFromFormat($format, $value);
+
+ if ($dateTime === false) {
+ $format = substr($format, 0, strrpos($format, ':'));
+ $dateTime = DateTime::createFromFormat($format, $value);
+ }
+
+ if ($dateTime === false || $dateTime->format($format) !== $value) {
+ $this->_error(self::INVALID_DATETIME_FORMAT, $baseFormat);
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/library/Icinga/Web/Form/Validator/InArray.php b/library/Icinga/Web/Form/Validator/InArray.php
new file mode 100644
index 0000000..5d3925e
--- /dev/null
+++ b/library/Icinga/Web/Form/Validator/InArray.php
@@ -0,0 +1,28 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Validator;
+
+use Zend_Validate_InArray;
+use Icinga\Util\StringHelper;
+
+class InArray extends Zend_Validate_InArray
+{
+ protected function _error($messageKey, $value = null)
+ {
+ if ($messageKey === static::NOT_IN_ARRAY) {
+ $matches = StringHelper::findSimilar($this->_value, $this->_haystack);
+ if (empty($matches)) {
+ $this->_messages[$messageKey] = sprintf(t('"%s" is not in the list of allowed values.'), $this->_value);
+ } else {
+ $this->_messages[$messageKey] = sprintf(
+ t('"%s" is not in the list of allowed values. Did you mean one of the following?: %s'),
+ $this->_value,
+ implode(', ', $matches)
+ );
+ }
+ } else {
+ parent::_error($messageKey, $value);
+ }
+ }
+}
diff --git a/library/Icinga/Web/Form/Validator/InternalUrlValidator.php b/library/Icinga/Web/Form/Validator/InternalUrlValidator.php
new file mode 100644
index 0000000..f936bb5
--- /dev/null
+++ b/library/Icinga/Web/Form/Validator/InternalUrlValidator.php
@@ -0,0 +1,41 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Validator;
+
+use Icinga\Application\Icinga;
+use Zend_Validate_Abstract;
+use Icinga\Web\Url;
+
+/**
+ * Validator that checks whether a textfield doesn't contain an external URL
+ */
+class InternalUrlValidator extends Zend_Validate_Abstract
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function isValid($value)
+ {
+ $url = Url::fromPath($value);
+ if ($url->getRelativeUrl() === '' || $url->isExternal()) {
+ $this->_error('IS_EXTERNAL');
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function _error($messageKey, $value = null)
+ {
+ if ($messageKey === 'IS_EXTERNAL') {
+ $this->_messages[$messageKey] = t('The url must not be external.');
+ } else {
+ parent::_error($messageKey, $value);
+ }
+ }
+}
diff --git a/library/Icinga/Web/Form/Validator/ReadablePathValidator.php b/library/Icinga/Web/Form/Validator/ReadablePathValidator.php
new file mode 100644
index 0000000..826421c
--- /dev/null
+++ b/library/Icinga/Web/Form/Validator/ReadablePathValidator.php
@@ -0,0 +1,53 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Validator;
+
+use Zend_Validate_Abstract;
+
+/**
+ * Validator that interprets the value as a filepath and checks if it's readable
+ *
+ * This validator should be preferred due to Zend_Validate_File_Exists is
+ * getting confused if there is another element in the form called `name'.
+ */
+class ReadablePathValidator extends Zend_Validate_Abstract
+{
+ const NOT_READABLE = 'notReadable';
+ const DOES_NOT_EXIST = 'doesNotExist';
+
+ /**
+ * The messages to write on different error states
+ *
+ * @var array
+ *
+ * @see Zend_Validate_Abstract::$_messageTemplates‚
+ */
+ protected $_messageTemplates = array(
+ self::NOT_READABLE => 'Path is not readable',
+ self::DOES_NOT_EXIST => 'Path does not exist'
+ );
+
+ /**
+ * Check whether the given value is a readable filepath
+ *
+ * @param string $value The value submitted in the form
+ * @param mixed $context The context of the form
+ *
+ * @return bool Whether the value was successfully validated
+ */
+ public function isValid($value, $context = null)
+ {
+ if (false === file_exists($value)) {
+ $this->_error(self::DOES_NOT_EXIST);
+ return false;
+ }
+
+ if (false === is_readable($value)) {
+ $this->_error(self::NOT_READABLE);
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/library/Icinga/Web/Form/Validator/TimeFormatValidator.php b/library/Icinga/Web/Form/Validator/TimeFormatValidator.php
new file mode 100644
index 0000000..9c1c99a
--- /dev/null
+++ b/library/Icinga/Web/Form/Validator/TimeFormatValidator.php
@@ -0,0 +1,58 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Validator;
+
+use Zend_Validate_Abstract;
+
+/**
+ * Validator that checks if a textfield contains a correct time format
+ */
+class TimeFormatValidator extends Zend_Validate_Abstract
+{
+
+ /**
+ * Valid time characters according to @see http://www.php.net/manual/en/function.date.php
+ *
+ * @var array
+ * @see http://www.php.net/manual/en/function.date.php
+ */
+ private $validChars = array('a', 'A', 'B', 'g', 'G', 'h', 'H', 'i', 's', 'u');
+
+ /**
+ * List of sensible time separators
+ *
+ * @var array
+ */
+ private $validSeparators = array(' ', ':', '-', '/', ';', ',', '.');
+
+ /**
+ * Error templates
+ *
+ * @var array
+ * @see Zend_Validate_Abstract::$_messageTemplates
+ */
+ protected $_messageTemplates = array(
+ 'INVALID_CHARACTERS' => 'Invalid time format'
+ );
+
+ /**
+ * Validate the input value
+ *
+ * @param string $value The format string to validate
+ * @param null $context The form context (ignored)
+ *
+ * @return bool True when the input is valid, otherwise false
+ *
+ * @see Zend_Validate_Abstract::isValid()
+ */
+ public function isValid($value, $context = null)
+ {
+ $rest = trim($value, join(' ', array_merge($this->validChars, $this->validSeparators)));
+ if (strlen($rest) > 0) {
+ $this->_error('INVALID_CHARACTERS');
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/library/Icinga/Web/Form/Validator/UrlValidator.php b/library/Icinga/Web/Form/Validator/UrlValidator.php
new file mode 100644
index 0000000..b1b578f
--- /dev/null
+++ b/library/Icinga/Web/Form/Validator/UrlValidator.php
@@ -0,0 +1,40 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Validator;
+
+use Zend_Validate_Abstract;
+
+/**
+ * Validator that checks whether a textfield doesn't contain raw double quotes
+ */
+class UrlValidator extends Zend_Validate_Abstract
+{
+ /**
+ * Constructor
+ */
+ public function __construct()
+ {
+ $this->_messageTemplates = array('HAS_QUOTES' => t(
+ 'The url must not contain raw double quotes. If you really need double quotes, use %22 instead.'
+ ));
+ }
+
+ /**
+ * Validate the input value
+ *
+ * @param string $value The string to validate
+ *
+ * @return bool true if and only if the input is valid, otherwise false
+ *
+ * @see Zend_Validate_Abstract::isValid()
+ */
+ public function isValid($value)
+ {
+ $hasQuotes = false === strpos($value, '"');
+ if (! $hasQuotes) {
+ $this->_error('HAS_QUOTES');
+ }
+ return $hasQuotes;
+ }
+}
diff --git a/library/Icinga/Web/Form/Validator/WritablePathValidator.php b/library/Icinga/Web/Form/Validator/WritablePathValidator.php
new file mode 100644
index 0000000..76efb58
--- /dev/null
+++ b/library/Icinga/Web/Form/Validator/WritablePathValidator.php
@@ -0,0 +1,72 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Validator;
+
+use Zend_Validate_Abstract;
+
+/**
+ * Validator that interprets the value as a path and checks if it's writable
+ */
+class WritablePathValidator extends Zend_Validate_Abstract
+{
+ const NOT_WRITABLE = 'notWritable';
+ const DOES_NOT_EXIST = 'doesNotExist';
+
+ /**
+ * The messages to write on differen error states
+ *
+ * @var array
+ *
+ * @see Zend_Validate_Abstract::$_messageTemplates‚
+ */
+ protected $_messageTemplates = array(
+ self::NOT_WRITABLE => 'Path is not writable',
+ self::DOES_NOT_EXIST => 'Path does not exist'
+ );
+
+ /**
+ * When true, the file or directory must exist
+ *
+ * @var bool
+ */
+ private $requireExistence = false;
+
+ /**
+ * Set this validator to require the target file to exist
+ */
+ public function setRequireExistence()
+ {
+ $this->requireExistence = true;
+ }
+
+ /**
+ * Check whether the given value is writable path
+ *
+ * @param string $value The value submitted in the form
+ * @param mixed $context The context of the form
+ *
+ * @return bool True when validation worked, otherwise false
+ *
+ * @see Zend_Validate_Abstract::isValid()
+ */
+ public function isValid($value, $context = null)
+ {
+ $value = (string) $value;
+
+ $this->_setValue($value);
+ if ($this->requireExistence && !file_exists($value)) {
+ $this->_error(self::DOES_NOT_EXIST);
+ return false;
+ }
+
+ if ((file_exists($value) && is_writable($value)) ||
+ (is_dir(dirname($value)) && is_writable(dirname($value)))
+ ) {
+ return true;
+ }
+
+ $this->_error(self::NOT_WRITABLE);
+ return false;
+ }
+}