diff --git a/library/Icinga/Form/Builder.php b/library/Icinga/Form/Builder.php new file mode 100644 index 000000000..cc2b08591 --- /dev/null +++ b/library/Icinga/Form/Builder.php @@ -0,0 +1,385 @@ +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); + } +} diff --git a/test/php/application/views/helpers/QlinkTest.php b/test/php/application/views/helpers/QlinkTest.php index ad4c5e3be..7e8030ee8 100755 --- a/test/php/application/views/helpers/QlinkTest.php +++ b/test/php/application/views/helpers/QlinkTest.php @@ -1,19 +1,8 @@ view = $this; - $this->basename = $basename; - } - - public function baseUrl($url) { - return $this->basename.$url; - } - }; -} +require_once('Zend/View/Helper/Abstract.php'); +require_once('Zend/View.php'); require('../../application/views/helpers/Qlink.php'); @@ -30,30 +19,37 @@ class Zend_View_Helper_QlinkTest extends \PHPUnit_Framework_TestCase public function testURLPathParameter() { + $view = new Zend_View(); + $helper = new Zend_View_Helper_Qlink(); - $pathTpl = "path/%s/to/%s"; + $helper->setView($view); + $pathTpl = "/path/%s/to/%s"; $this->assertEquals( - "path/param1/to/param2", + "/path/param1/to/param2", $helper->getFormattedURL($pathTpl,array('param1','param2')) ); } public function testUrlGETParameter() { + $view = new Zend_View(); $helper = new Zend_View_Helper_Qlink(); + $helper->setView($view); $pathTpl = 'path'; $this->assertEquals( - 'path?param1=value1&param2=value2', + '/path?param1=value1&param2=value2', $helper->getFormattedURL($pathTpl,array('param1'=>'value1','param2'=>'value2')) ); } public function testMixedParameters() { + $view = new Zend_View(); $helper = new Zend_View_Helper_Qlink(); + $helper->setView($view); $pathTpl = 'path/%s/to/%s'; $this->assertEquals( - 'path/path1/to/path2?param1=value1&param2=value2', + '/path/path1/to/path2?param1=value1&param2=value2', $helper->getFormattedURL($pathTpl,array( 'path1','path2', 'param1'=>'value1', diff --git a/test/php/library/Icinga/Form/BuilderTest.php b/test/php/library/Icinga/Form/BuilderTest.php new file mode 100644 index 000000000..ede5919fc --- /dev/null +++ b/test/php/library/Icinga/Form/BuilderTest.php @@ -0,0 +1,224 @@ +test; + } + + public function setTest($test) + { + $this->test = $test; + } +} + +class BuilderTest extends \PHPUnit_Framework_TestCase +{ + /** + * + **/ + public function testFormCreation() + { + $builder = new Builder(null, array("CSRFProtection" => false)); + $this->assertInstanceOf("Zend_Form", $builder->getForm()); + } + + /** + * + **/ + public function testCSRFProtectionTokenCreation() + { + $view = new \Zend_View(); + $builder = new Builder(); // when no token is given, a CRSF field should be added + $builder->setView($view); + + $DOM = new \DOMDocument; + $DOM->loadHTML($builder); + $this->assertNotNull($DOM->getElementById(Builder::CSRF_ID)); + + $builder->disableCSRF(); + $DOM->loadHTML($builder); + $this->assertNull($DOM->getElementById(Builder::CSRF_ID)); + + } + /** + * Test whether form methods are passed to the Zend_Form object + * When called in the Builder instance + * + **/ + public function testMethodPassing() + { + $DOM = new \DOMDocument; + $view = new \Zend_View(); + $builder = new Builder(null, array("CSRFProtection" => false)); + $builder->setView($view); + + $DOM->loadHTML($builder); + $this->assertEquals(0, $DOM->getElementsByTagName("input")->length); + + $builder->addElement("text", "username"); + $DOM->loadHTML($builder); + $inputEls = $DOM->getElementsByTagName("input"); + $this->assertEquals(1, $inputEls->length); + $this->assertEquals("username", $inputEls->item(0)->attributes->getNamedItem("name")->value); + } + /** + * + * + **/ + public function testCreateByArray() + { + $DOM = new \DOMDocument; + $view = new \Zend_View(); + $builder = Builder::fromArray( + array( + 'username' => array( + 'text', + array( + 'label' => 'Username', + 'required' => true, + ) + ), + 'password' => array( + 'password', + array( + 'label' => 'Password', + 'required' => true, + ) + ), + 'submit' => array( + 'submit', + array( + 'label' => 'Login' + ) + ) + ), + array( + "CSRFProtection" => false + ) + ); + $builder->setView($view); + + $DOM->loadHTML($builder); + $inputEls = $DOM->getElementsByTagName("input"); + $this->assertEquals(3, $inputEls->length); + + $username = $inputEls->item(0); + $this->assertEquals("username", $username->attributes->getNamedItem("name")->value); + + $password= $inputEls->item(1); + $this->assertEquals("password", $password->attributes->getNamedItem("name")->value); + $this->assertEquals("password", $password->attributes->getNamedItem("type")->value); + + $submitBtn= $inputEls->item(2); + $this->assertEquals("submit", $submitBtn->attributes->getNamedItem("name")->value); + $this->assertEquals("submit", $submitBtn->attributes->getNamedItem("type")->value); + } + + /** + * + * + */ + public function testModelBindingWithArray() + { + $view = new \Zend_View(); + + $myModel = array( + "username" => "", + "password" => "" + ); + + $builder = new Builder( + null, + array( + "CSRFProtection" => false, + "model" => &$myModel + ) + ); + + $builder->setView($view); + + // $builder->bindToModel($myModel); + $builder->addElement("text", "username"); + $builder->addElement("password", "password"); + // test sync from form to model + $builder->populate( + array( + "username" => "User input", + "password" => "Secret$123" + ) + ); + $this->assertEquals("User input", $myModel["username"]); + $this->assertEquals("Secret$123", $myModel["password"]); + + // test sync from model to form + $myModel["username"] = "Another user"; + $myModel["password"] = "Another pass"; + + $builder->syncWithModel(); + $this->assertEquals("Another user", $builder->getElement("username")->getValue()); + $this->assertEquals("Another pass", $builder->getElement("password")->getValue()); + } + + /** + * + * + */ + public function testModelBindingWithObject() + { + $view = new \Zend_View(); + $builder = new Builder(null, array("CSRFProtection" => false)); + $builder->setView($view); + + + + $myModel = new BuilderTestModel(); + + $builder->bindToModel($myModel); + $builder->addElement("text", "username"); + $builder->addElement("password", "password"); + $builder->addElement("text", "test"); + // test sync from form to model + $builder->populate( + (object) array( + "username" => "User input", + "password" => "Secret$123", + "test" => 'test334' + ) + ); + $this->assertEquals("User input", $myModel->username); + $this->assertEquals("Secret$123", $myModel->password); + $this->assertEquals("test334", $myModel->getTest()); + + // test sync from model to form + $myModel->username = "Another user"; + $myModel->password = "Another pass"; + + $builder->syncWithModel(); + $this->assertEquals("Another user", $builder->getElement("username")->getValue()); + $this->assertEquals("Another pass", $builder->getElement("password")->getValue()); + } + + /** + * @expectedException \BadMethodCallException + * @expectedExceptionMessage Method doesNotExist123 does not exist either in \Icinga\Form\Builder nor in Zend_Form + */ + public function testBadCall1() + { + $builder = new Builder(null, array("CSRFProtection" => false)); + $builder->doesNotExist123(); + } +}