diff options
Diffstat (limited to 'library/Icinga/Web/Form')
28 files changed, 1834 insertions, 0 deletions
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; + } +} |