diff options
Diffstat (limited to '')
29 files changed, 3500 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 >noscript< 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..5bd5f6a --- /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..2a0f193 --- /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..87d12aa --- /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..9e30e86 --- /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..09a3ae9 --- /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..307247e --- /dev/null +++ b/library/Icinga/Web/Form/Element/Button.php @@ -0,0 +1,81 @@ +<?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; +use Zend_Config; + +/** + * 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..766d916 --- /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; + } +} |