386 lines
9.8 KiB
PHP
386 lines
9.8 KiB
PHP
<?php
|
|
// {{{ICINGA_LICENSE_HEADER}}}
|
|
// {{{ICINGA_LICENSE_HEADER}}}
|
|
|
|
namespace Icinga\Form;
|
|
|
|
/**
|
|
* Class that helps building and validating forms and offers a rudimentary
|
|
* data-binding mechanismn.
|
|
*
|
|
* The underlying form can be accessed by expicitly calling $builder->getForm() or
|
|
* by directly calling the forms method (which is, in case of populate() the preferred way)
|
|
* like: $builder->getElements()
|
|
*
|
|
* @method \Zend_Form_Element getElement(string $name)
|
|
* @method \Zend_Form addElement(\string $element, string $name = null, array $options = null)
|
|
* @method \Zend_Form setView(\Zend_View $view)
|
|
* @package Icinga\Form
|
|
*/
|
|
class Builder
|
|
{
|
|
const CSRF_ID = "icinga_csrf_id";
|
|
|
|
/**
|
|
* @var \Zend_Form
|
|
*/
|
|
private $form;
|
|
|
|
/**
|
|
* @var null
|
|
*/
|
|
private $boundModel = null;
|
|
|
|
/**
|
|
* @var bool
|
|
*/
|
|
private $disableCSRF = false;
|
|
|
|
/**
|
|
* Constructrs a new Formbuilder, containing an empty form if no
|
|
* $form parameter is given or the Zend form from the $form parameter.
|
|
*
|
|
* @param \Zend_Form $form The form to use with this Builder
|
|
* @param Array $options an optional array of Options:
|
|
* - CSRFProtection true to add a crsf token to the
|
|
* form (default), false to remove it
|
|
* - model An referenced array or object to use
|
|
* for value binding
|
|
**/
|
|
public function __construct(\Zend_Form $form = null, array $options = array())
|
|
{
|
|
if ($form === null) {
|
|
$myModel = array(
|
|
"username" => "",
|
|
"password" => ""
|
|
);
|
|
|
|
$form = new \Zend_Form();
|
|
}
|
|
|
|
$this->setZendForm($form);
|
|
|
|
if (isset($options["CSRFProtection"])) {
|
|
$this->disableCSRF = !$options["CSRFProtection"];
|
|
}
|
|
if (isset($options["model"])) {
|
|
$this->bindToModel($options["model"]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Setter for Zend_Form
|
|
* @param \Zend_Form $form
|
|
*/
|
|
public function setZendForm(\Zend_Form $form)
|
|
{
|
|
$this->form = $form;
|
|
}
|
|
|
|
/**
|
|
* Getter for Zent_Form
|
|
* @return \Zend_Form
|
|
*/
|
|
public function getForm()
|
|
{
|
|
if (!$this->disableCSRF) {
|
|
$this->addCSRFFieldToForm();
|
|
}
|
|
if (!$this->form) {
|
|
return new \Zend_Form();
|
|
}
|
|
return $this->form;
|
|
}
|
|
|
|
/**
|
|
* Add elements to form
|
|
* @param array $elements
|
|
*/
|
|
public function addElementsFromConfig(array $elements)
|
|
{
|
|
foreach ($elements as $key => $values) {
|
|
$this->addElement($values[0], $key, $values[1]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Quick add elements to a new builder instance
|
|
* @param array $elements
|
|
* @param array $options
|
|
* @return Builder
|
|
*/
|
|
public static function fromArray(array $elements, $options = array())
|
|
{
|
|
$builder = new Builder(null, $options);
|
|
$builder->addElementsFromConfig($elements);
|
|
return $builder;
|
|
}
|
|
|
|
/**
|
|
* Test that the form is valid
|
|
*
|
|
* @param array $data
|
|
* @return bool
|
|
*/
|
|
public function isValid(array $data = null)
|
|
{
|
|
if ($data === null) {
|
|
$data = $_POST;
|
|
}
|
|
return $this->hasValidToken() && $this->form->isValid($data);
|
|
}
|
|
|
|
/**
|
|
* Test if the form was submitted
|
|
* @param string $btnName
|
|
* @return bool
|
|
*/
|
|
public function isSubmitted($btnName = 'submit')
|
|
{
|
|
$btn = $this->getElement($btnName);
|
|
if (!$btn || !isset($_POST[$btnName])) {
|
|
return false;
|
|
}
|
|
return $_POST[$btnName] === $btn->getLabel();
|
|
}
|
|
|
|
/**
|
|
* Render the form
|
|
* @return string
|
|
*/
|
|
public function render()
|
|
{
|
|
return $this->getForm()->render();
|
|
}
|
|
|
|
public function __toString()
|
|
{
|
|
return $this->getForm()->__toString();
|
|
}
|
|
|
|
/**
|
|
* Enable CSRF token field
|
|
*/
|
|
public function enableCSRF()
|
|
{
|
|
$this->disableCSRF = false;
|
|
}
|
|
|
|
/**
|
|
* Disable CSRF token field
|
|
*/
|
|
public function disableCSRF()
|
|
{
|
|
$this->disableCSRF = true;
|
|
$this->form->removeElement(self::CSRF_ID);
|
|
$this->form->removeElement(self::CSRF_ID."_seed");
|
|
}
|
|
|
|
/**
|
|
* Add CSRF field to form
|
|
*/
|
|
private function addCSRFFieldToForm()
|
|
{
|
|
if (!$this->form || $this->disableCSRF || $this->form->getElement(self::CSRF_ID)) {
|
|
return;
|
|
}
|
|
list($seed, $token) = $this->getSeedTokenPair();
|
|
|
|
$this->form->addElement("hidden", self::CSRF_ID);
|
|
$this->form->getElement(self::CSRF_ID)
|
|
->setValue($token)
|
|
->setDecorators(array('ViewHelper'));
|
|
|
|
$this->form->addElement("hidden", self::CSRF_ID."_seed");
|
|
$this->form->getElement(self::CSRF_ID."_seed")
|
|
->setValue($seed)
|
|
->setDecorators(array('ViewHelper'));
|
|
|
|
}
|
|
|
|
/**
|
|
* Bind model to a form
|
|
* @param $model
|
|
*/
|
|
public function bindToModel(&$model)
|
|
{
|
|
$this->boundModel = &$model;
|
|
}
|
|
|
|
/**
|
|
* Repopulate
|
|
*/
|
|
public function repopulate()
|
|
{
|
|
if (!empty($_POST)) {
|
|
$this->populate($_POST);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Populate form
|
|
* @param $data
|
|
* @param bool $ignoreModel
|
|
* @throws \InvalidArgumentException
|
|
*/
|
|
public function populate($data, $ignoreModel = false)
|
|
{
|
|
if (is_array($data)) {
|
|
$this->form->populate($data);
|
|
} elseif (is_object($data)) {
|
|
$this->populateFromObject($data);
|
|
} else {
|
|
throw new \InvalidArgumentException("Builder::populate() expects and object or array, $data given");
|
|
}
|
|
if ($this->boundModel === null || $ignoreModel) {
|
|
return;
|
|
}
|
|
$this->updateModel();
|
|
|
|
}
|
|
|
|
/**
|
|
* Populate form object
|
|
* @param $data
|
|
*/
|
|
private function populateFromObject($data)
|
|
{
|
|
/** @var \Zend_Form_Element $element */
|
|
|
|
foreach ($this->form->getElements() as $name => $element) {
|
|
if (isset($data->$name)) {
|
|
$element->setValue($data->$name);
|
|
|
|
} else {
|
|
$getter = "get".ucfirst($name);
|
|
if (method_exists($data, $getter)) {
|
|
$element->setValue($data->$getter());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update model instance
|
|
*/
|
|
public function updateModel()
|
|
{
|
|
if (is_array($this->boundModel)) {
|
|
$this->updateArrayModel();
|
|
} elseif (is_object($this->boundModel)) {
|
|
$this->updateObjectModel();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updater for objects
|
|
*/
|
|
private function updateObjectModel()
|
|
{
|
|
/** @var \Zend_Form_Element $element */
|
|
|
|
foreach ($this->form->getElements() as $name => $element) {
|
|
if (isset($this->boundModel->$name)) {
|
|
$this->boundModel->$name = $element->getValue();
|
|
} else {
|
|
$setter = "set".ucfirst($name);
|
|
if (method_exists($this->boundModel, $setter)) {
|
|
$this->boundModel->$setter($element->getValue());
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updater for arrays
|
|
*/
|
|
private function updateArrayModel()
|
|
{
|
|
/** @var \Zend_Form_Element $element */
|
|
|
|
foreach ($this->form->getElements() as $name => $element) {
|
|
if (isset($this->boundModel[$name])) {
|
|
$this->boundModel[$name] = $element->getValue();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Synchronize model with form
|
|
*/
|
|
public function syncWithModel()
|
|
{
|
|
$this->populate($this->boundModel, true);
|
|
}
|
|
|
|
/**
|
|
* Magic caller, pass through method calls to form
|
|
* @param $fn
|
|
* @param array $args
|
|
* @return mixed
|
|
* @throws \BadMethodCallException
|
|
*/
|
|
public function __call($fn, array $args)
|
|
{
|
|
if (method_exists($this->form, $fn)) {
|
|
return call_user_func_array(array($this->form, $fn), $args);
|
|
} else {
|
|
throw new \BadMethodCallException(
|
|
"Method $fn does not exist either ".
|
|
"in \Icinga\Form\Builder nor in Zend_Form"
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Whether the token parameter is valid
|
|
*
|
|
* @param int $maxAge Max allowed token age
|
|
* @param string $sessionId A specific session id (useful for tests?)
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function hasValidToken($maxAge = 600, $sessionId = null)
|
|
{
|
|
if ($this->disableCSRF) {
|
|
return true;
|
|
}
|
|
|
|
if ($this->form->getElement(self::CSRF_ID) == null) {
|
|
return false;
|
|
}
|
|
|
|
$sessionId = $sessionId ? $sessionId : session_id();
|
|
$seed = $this->form->getElement(self::CSRF_ID.'_seed')->getValue();
|
|
if (! is_numeric($seed)) {
|
|
return false;
|
|
}
|
|
|
|
// Remove quantitized timestamp portion so maxAge applies
|
|
$seed -= (intval(time() / $maxAge) * $maxAge);
|
|
$token = $this->getElement(self::CSRF_ID)->getValue();
|
|
return $token === hash('sha256', $sessionId . $seed);
|
|
}
|
|
|
|
/**
|
|
* Get a new seed/token pair
|
|
*
|
|
* @param int $maxAge Max allowed token age
|
|
* @param string $sessionId A specific session id (useful for tests?)
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getSeedTokenPair($maxAge = 600, $sessionId = null)
|
|
{
|
|
$sessionId = $sessionId ? $sessionId : session_id();
|
|
$seed = mt_rand();
|
|
$hash = hash('sha256', $sessionId . $seed);
|
|
|
|
// Add quantitized timestamp portion to apply maxAge
|
|
$seed += (intval(time() / $maxAge) * $maxAge);
|
|
return array($seed, $hash);
|
|
}
|
|
}
|