diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:46:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:46:43 +0000 |
commit | 3e02d5aff85babc3ffbfcf52313f2108e313aa23 (patch) | |
tree | b01f3923360c20a6a504aff42d45670c58af3ec5 /library/Icinga/Web/Form.php | |
parent | Initial commit. (diff) | |
download | icingaweb2-3e02d5aff85babc3ffbfcf52313f2108e313aa23.tar.xz icingaweb2-3e02d5aff85babc3ffbfcf52313f2108e313aa23.zip |
Adding upstream version 2.12.1.upstream/2.12.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'library/Icinga/Web/Form.php')
-rw-r--r-- | library/Icinga/Web/Form.php | 1666 |
1 files changed, 1666 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; + } +} |