about summary refs log tree commit diff stats
path: root/wiki/inc/Form
diff options
context:
space:
mode:
authorahriman <ahriman@falte.red>2018-12-03 19:22:25 -0500
committerahriman <ahriman@falte.red>2018-12-03 19:22:25 -0500
commit0ae8cbf5c0b1a198b963490985b7738392ebcb97 (patch)
treeb2c77ae72c6b717e2b97492065196ac5ffb2d9e2 /wiki/inc/Form
parentf57f6cc5a2d159f90168d292437dc4bd8cd7f934 (diff)
downloadsite-0ae8cbf5c0b1a198b963490985b7738392ebcb97.tar.gz
installed dokuwiki, added to navbar, updated news
Diffstat (limited to 'wiki/inc/Form')
-rw-r--r--wiki/inc/Form/ButtonElement.php34
-rw-r--r--wiki/inc/Form/CheckableElement.php62
-rw-r--r--wiki/inc/Form/DropdownElement.php190
-rw-r--r--wiki/inc/Form/Element.php151
-rw-r--r--wiki/inc/Form/FieldsetCloseElement.php30
-rw-r--r--wiki/inc/Form/FieldsetOpenElement.php36
-rw-r--r--wiki/inc/Form/Form.php456
-rw-r--r--wiki/inc/Form/HTMLElement.php29
-rw-r--r--wiki/inc/Form/InputElement.php159
-rw-r--r--wiki/inc/Form/LabelElement.php27
-rw-r--r--wiki/inc/Form/LegacyForm.php181
-rw-r--r--wiki/inc/Form/OptGroup.php100
-rw-r--r--wiki/inc/Form/TagCloseElement.php88
-rw-r--r--wiki/inc/Form/TagElement.php29
-rw-r--r--wiki/inc/Form/TagOpenElement.php30
-rw-r--r--wiki/inc/Form/TextareaElement.php51
-rw-r--r--wiki/inc/Form/ValueElement.php45
17 files changed, 1698 insertions, 0 deletions
diff --git a/wiki/inc/Form/ButtonElement.php b/wiki/inc/Form/ButtonElement.php
new file mode 100644
index 0000000..e2afe9c
--- /dev/null
+++ b/wiki/inc/Form/ButtonElement.php
@@ -0,0 +1,34 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class ButtonElement
+ *
+ * Represents a simple button
+ *
+ * @package dokuwiki\Form
+ */
+class ButtonElement extends Element {
+
+    /** @var string HTML content */
+    protected $content = '';
+
+    /**
+     * @param string $name
+     * @param string $content HTML content of the button. You have to escape it yourself.
+     */
+    function __construct($name, $content = '') {
+        parent::__construct('button', array('name' => $name, 'value' => 1));
+        $this->content = $content;
+    }
+
+    /**
+     * The HTML representation of this element
+     *
+     * @return string
+     */
+    public function toHTML() {
+        return '<button ' . buildAttributes($this->attrs(), true) . '>'.$this->content.'</button>';
+    }
+
+}
diff --git a/wiki/inc/Form/CheckableElement.php b/wiki/inc/Form/CheckableElement.php
new file mode 100644
index 0000000..27d5c2e
--- /dev/null
+++ b/wiki/inc/Form/CheckableElement.php
@@ -0,0 +1,62 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class CheckableElement
+ *
+ * For Radio- and Checkboxes
+ *
+ * @package dokuwiki\Form
+ */
+class CheckableElement extends InputElement {
+
+    /**
+     * @param string $type The type of this element
+     * @param string $name The name of this form element
+     * @param string $label The label text for this element
+     */
+    public function __construct($type, $name, $label) {
+        parent::__construct($type, $name, $label);
+        // default value is 1
+        $this->attr('value', 1);
+    }
+
+    /**
+     * Handles the useInput flag and sets the checked attribute accordingly
+     */
+    protected function prefillInput() {
+        global $INPUT;
+        list($name, $key) = $this->getInputName();
+        $myvalue = $this->val();
+
+        if(!$INPUT->has($name)) return;
+
+        if($key === null) {
+            // no key - single value
+            $value = $INPUT->str($name);
+            if($value == $myvalue) {
+                $this->attr('checked', 'checked');
+            } else {
+                $this->rmattr('checked');
+            }
+        } else {
+            // we have an array, there might be several values in it
+            $input = $INPUT->arr($name);
+            if(isset($input[$key])) {
+                $this->rmattr('checked');
+
+                // values seem to be in another sub array
+                if(is_array($input[$key])) {
+                    $input = $input[$key];
+                }
+
+                foreach($input as $value) {
+                    if($value == $myvalue) {
+                        $this->attr('checked', 'checked');
+                    }
+                }
+            }
+        }
+    }
+
+}
diff --git a/wiki/inc/Form/DropdownElement.php b/wiki/inc/Form/DropdownElement.php
new file mode 100644
index 0000000..023b67d
--- /dev/null
+++ b/wiki/inc/Form/DropdownElement.php
@@ -0,0 +1,190 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class DropdownElement
+ *
+ * Represents a HTML select. Please note that this does not support multiple selected options!
+ *
+ * @package dokuwiki\Form
+ */
+class DropdownElement extends InputElement {
+
+    /** @var array OptGroup[] */
+    protected $optGroups = array();
+
+    /**
+     * @param string $name The name of this form element
+     * @param array  $options The available options
+     * @param string $label The label text for this element (will be autoescaped)
+     */
+    public function __construct($name, $options, $label = '') {
+        parent::__construct('dropdown', $name, $label);
+        $this->rmattr('type');
+        $this->optGroups[''] = new OptGroup(null, $options);
+        $this->val('');
+    }
+
+    /**
+     * Add an `<optgroup>` and respective options
+     *
+     * @param string $label
+     * @param array  $options
+     * @return OptGroup a reference to the added optgroup
+     * @throws \Exception
+     */
+    public function addOptGroup($label, $options) {
+        if (empty($label)) {
+            throw new \InvalidArgumentException(hsc('<optgroup> must have a label!'));
+        }
+        $this->optGroups[$label] = new OptGroup($label, $options);
+        return end($this->optGroups);
+    }
+
+    /**
+     * Set or get the optgroups of an Dropdown-Element.
+     *
+     * optgroups have to be given as associative array
+     *   * the key being the label of the group
+     *   * the value being an array of options as defined in @see OptGroup::options()
+     *
+     * @param null|array $optGroups
+     * @return OptGroup[]|DropdownElement
+     */
+    public function optGroups($optGroups = null) {
+        if($optGroups === null) {
+            return $this->optGroups;
+        }
+        if (!is_array($optGroups)) {
+            throw new \InvalidArgumentException(hsc('Argument must be an associative array of label => [options]!'));
+        }
+        $this->optGroups = array();
+        foreach ($optGroups as $label => $options) {
+            $this->addOptGroup($label, $options);
+        }
+        return $this;
+    }
+
+    /**
+     * Get or set the options of the Dropdown
+     *
+     * Options can be given as associative array (value => label) or as an
+     * indexd array (label = value) or as an array of arrays. In the latter
+     * case an element has to look as follows:
+     * option-value => array (
+     *                 'label' => option-label,
+     *                 'attrs' => array (
+     *                                    attr-key => attr-value, ...
+     *                                  )
+     *                 )
+     *
+     * @param null|array $options
+     * @return $this|array
+     */
+    public function options($options = null) {
+        if ($options === null) {
+            return $this->optGroups['']->options();
+        }
+        $this->optGroups[''] = new OptGroup(null, $options);
+        return $this;
+    }
+
+    /**
+     * Gets or sets an attribute
+     *
+     * When no $value is given, the current content of the attribute is returned.
+     * An empty string is returned for unset attributes.
+     *
+     * When a $value is given, the content is set to that value and the Element
+     * itself is returned for easy chaining
+     *
+     * @param string $name Name of the attribute to access
+     * @param null|string $value New value to set
+     * @return string|$this
+     */
+    public function attr($name, $value = null) {
+        if(strtolower($name) == 'multiple') {
+            throw new \InvalidArgumentException('Sorry, the dropdown element does not support the "multiple" attribute');
+        }
+        return parent::attr($name, $value);
+    }
+
+    /**
+     * Get or set the current value
+     *
+     * When setting a value that is not defined in the options, the value is ignored
+     * and the first option's value is selected instead
+     *
+     * @param null|string $value The value to set
+     * @return $this|string
+     */
+    public function val($value = null) {
+        if($value === null) return $this->value;
+
+        $value_exists = $this->setValueInOptGroups($value);
+
+        if($value_exists) {
+            $this->value = $value;
+        } else {
+            // unknown value set, select first option instead
+            $this->value = $this->getFirstOption();
+            $this->setValueInOptGroups($this->value);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Returns the first options as it will be rendered in HTML
+     *
+     * @return string
+     */
+    protected function getFirstOption() {
+        $options = $this->options();
+        if (!empty($options)) {
+            $keys = array_keys($options);
+            return (string) array_shift($keys);
+        }
+        foreach ($this->optGroups as $optGroup) {
+            $options = $optGroup->options();
+            if (!empty($options)) {
+                $keys = array_keys($options);
+                return (string) array_shift($keys);
+            }
+        }
+    }
+
+    /**
+     * Set the value in the OptGroups, including the optgroup for the options without optgroup.
+     *
+     * @param string $value
+     * @return bool
+     */
+    protected function setValueInOptGroups($value) {
+        $value_exists = false;
+        /** @var OptGroup $optGroup */
+        foreach ($this->optGroups as $optGroup) {
+            $value_exists = $optGroup->storeValue($value) || $value_exists;
+            if ($value_exists) {
+                $value = null;
+            }
+        }
+        return $value_exists;
+    }
+
+    /**
+     * Create the HTML for the select it self
+     *
+     * @return string
+     */
+    protected function mainElementHTML() {
+        if($this->useInput) $this->prefillInput();
+
+        $html = '<select ' . buildAttributes($this->attrs()) . '>';
+        $html = array_reduce($this->optGroups, function($html, OptGroup $optGroup) {return $html . $optGroup->toHTML();}, $html);
+        $html .= '</select>';
+
+        return $html;
+    }
+
+}
diff --git a/wiki/inc/Form/Element.php b/wiki/inc/Form/Element.php
new file mode 100644
index 0000000..a357882
--- /dev/null
+++ b/wiki/inc/Form/Element.php
@@ -0,0 +1,151 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class Element
+ *
+ * The basic building block of a form
+ *
+ * @package dokuwiki\Form
+ */
+abstract class Element {
+
+    /**
+     * @var array the attributes of this element
+     */
+    protected $attributes = array();
+
+    /**
+     * @var string The type of this element
+     */
+    protected $type;
+
+    /**
+     * @param string $type The type of this element
+     * @param array $attributes
+     */
+    public function __construct($type, $attributes = array()) {
+        $this->type = $type;
+        $this->attributes = $attributes;
+    }
+
+    /**
+     * Type of this element
+     *
+     * @return string
+     */
+    public function getType() {
+        return $this->type;
+    }
+
+    /**
+     * Gets or sets an attribute
+     *
+     * When no $value is given, the current content of the attribute is returned.
+     * An empty string is returned for unset attributes.
+     *
+     * When a $value is given, the content is set to that value and the Element
+     * itself is returned for easy chaining
+     *
+     * @param string $name Name of the attribute to access
+     * @param null|string $value New value to set
+     * @return string|$this
+     */
+    public function attr($name, $value = null) {
+        // set
+        if($value !== null) {
+            $this->attributes[$name] = $value;
+            return $this;
+        }
+
+        // get
+        if(isset($this->attributes[$name])) {
+            return $this->attributes[$name];
+        } else {
+            return '';
+        }
+    }
+
+    /**
+     * Removes the given attribute if it exists
+     *
+     * @param string $name
+     * @return $this
+     */
+    public function rmattr($name) {
+        if(isset($this->attributes[$name])) {
+            unset($this->attributes[$name]);
+        }
+        return $this;
+    }
+
+    /**
+     * Gets or adds a all given attributes at once
+     *
+     * @param array|null $attributes
+     * @return array|$this
+     */
+    public function attrs($attributes = null) {
+        // set
+        if($attributes) {
+            foreach((array) $attributes as $key => $val) {
+                $this->attr($key, $val);
+            }
+            return $this;
+        }
+        // get
+        return $this->attributes;
+    }
+
+    /**
+     * Adds a class to the class attribute
+     *
+     * This is the preferred method of setting the element's class
+     *
+     * @param string $class the new class to add
+     * @return $this
+     */
+    public function addClass($class) {
+        $classes = explode(' ', $this->attr('class'));
+        $classes[] = $class;
+        $classes = array_unique($classes);
+        $classes = array_filter($classes);
+        $this->attr('class', join(' ', $classes));
+        return $this;
+    }
+
+    /**
+     * Get or set the element's ID
+     *
+     * This is the preferred way of setting the element's ID
+     *
+     * @param null|string $id
+     * @return string|$this
+     */
+    public function id($id = null) {
+        if(strpos($id, '__') === false) {
+            throw new \InvalidArgumentException('IDs in DokuWiki have to contain two subsequent underscores');
+        }
+
+        return $this->attr('id', $id);
+    }
+
+    /**
+     * Get or set the element's value
+     *
+     * This is the preferred way of setting the element's value
+     *
+     * @param null|string $value
+     * @return string|$this
+     */
+    public function val($value = null) {
+        return $this->attr('value', $value);
+    }
+
+    /**
+     * The HTML representation of this element
+     *
+     * @return string
+     */
+    abstract public function toHTML();
+}
diff --git a/wiki/inc/Form/FieldsetCloseElement.php b/wiki/inc/Form/FieldsetCloseElement.php
new file mode 100644
index 0000000..8f26717
--- /dev/null
+++ b/wiki/inc/Form/FieldsetCloseElement.php
@@ -0,0 +1,30 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class FieldsetCloseElement
+ *
+ * Closes an open Fieldset
+ *
+ * @package dokuwiki\Form
+ */
+class FieldsetCloseElement extends TagCloseElement {
+
+    /**
+     * @param array $attributes
+     */
+    public function __construct($attributes = array()) {
+        parent::__construct('', $attributes);
+        $this->type = 'fieldsetclose';
+    }
+
+
+    /**
+     * The HTML representation of this element
+     *
+     * @return string
+     */
+    public function toHTML() {
+        return '</fieldset>';
+    }
+}
diff --git a/wiki/inc/Form/FieldsetOpenElement.php b/wiki/inc/Form/FieldsetOpenElement.php
new file mode 100644
index 0000000..a7de461
--- /dev/null
+++ b/wiki/inc/Form/FieldsetOpenElement.php
@@ -0,0 +1,36 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class FieldsetOpenElement
+ *
+ * Opens a Fieldset with an optional legend
+ *
+ * @package dokuwiki\Form
+ */
+class FieldsetOpenElement extends TagOpenElement {
+
+    /**
+     * @param string $legend
+     * @param array $attributes
+     */
+    public function __construct($legend='', $attributes = array()) {
+        // this is a bit messy and we just do it for the nicer class hierarchy
+        // the parent would expect the tag in $value but we're storing the
+        // legend there, so we have to set the type manually
+        parent::__construct($legend, $attributes);
+        $this->type = 'fieldsetopen';
+    }
+
+    /**
+     * The HTML representation of this element
+     *
+     * @return string
+     */
+    public function toHTML() {
+        $html = '<fieldset '.buildAttributes($this->attrs()).'>';
+        $legend = $this->val();
+        if($legend) $html .= DOKU_LF.'<legend>'.hsc($legend).'</legend>';
+        return $html;
+    }
+}
diff --git a/wiki/inc/Form/Form.php b/wiki/inc/Form/Form.php
new file mode 100644
index 0000000..92bbd30
--- /dev/null
+++ b/wiki/inc/Form/Form.php
@@ -0,0 +1,456 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class Form
+ *
+ * Represents the whole Form. This is what you work on, and add Elements to
+ *
+ * @package dokuwiki\Form
+ */
+class Form extends Element {
+
+    /**
+     * @var array name value pairs for hidden values
+     */
+    protected $hidden = array();
+
+    /**
+     * @var Element[] the elements of the form
+     */
+    protected $elements = array();
+
+    /**
+     * Creates a new, empty form with some default attributes
+     *
+     * @param array $attributes
+     * @param bool  $unsafe     if true, then the security token is ommited
+     */
+    public function __construct($attributes = array(), $unsafe = false) {
+        global $ID;
+
+        parent::__construct('form', $attributes);
+
+        // use the current URL as default action
+        if(!$this->attr('action')) {
+            $get = $_GET;
+            if(isset($get['id'])) unset($get['id']);
+            $self = wl($ID, $get, false, '&'); //attributes are escaped later
+            $this->attr('action', $self);
+        }
+
+        // post is default
+        if(!$this->attr('method')) {
+            $this->attr('method', 'post');
+        }
+
+        // we like UTF-8
+        if(!$this->attr('accept-charset')) {
+            $this->attr('accept-charset', 'utf-8');
+        }
+
+        // add the security token by default
+        if (!$unsafe) {
+            $this->setHiddenField('sectok', getSecurityToken());
+        }
+
+        // identify this as a new form based form in HTML
+        $this->addClass('doku_form');
+    }
+
+    /**
+     * Sets a hidden field
+     *
+     * @param string $name
+     * @param string $value
+     * @return $this
+     */
+    public function setHiddenField($name, $value) {
+        $this->hidden[$name] = $value;
+        return $this;
+    }
+
+    #region element query function
+
+    /**
+     * Returns the numbers of elements in the form
+     *
+     * @return int
+     */
+    public function elementCount() {
+        return count($this->elements);
+    }
+
+    /**
+     * Get the position of the element in the form or false if it is not in the form
+     *
+     * Warning: This function may return Boolean FALSE, but may also return a non-Boolean value which evaluates to FALSE. Please read the section on Booleans for more information. Use the === operator for testing the return value of this function.
+     *
+     * @param Element $element
+     *
+     * @return false|int
+     */
+    public function getElementPosition(Element $element)
+    {
+        return array_search($element, $this->elements, true);
+    }
+
+    /**
+     * Returns a reference to the element at a position.
+     * A position out-of-bounds will return either the
+     * first (underflow) or last (overflow) element.
+     *
+     * @param int $pos
+     * @return Element
+     */
+    public function getElementAt($pos) {
+        if($pos < 0) $pos = count($this->elements) + $pos;
+        if($pos < 0) $pos = 0;
+        if($pos >= count($this->elements)) $pos = count($this->elements) - 1;
+        return $this->elements[$pos];
+    }
+
+    /**
+     * Gets the position of the first of a type of element
+     *
+     * @param string $type Element type to look for.
+     * @param int $offset search from this position onward
+     * @return false|int position of element if found, otherwise false
+     */
+    public function findPositionByType($type, $offset = 0) {
+        $len = $this->elementCount();
+        for($pos = $offset; $pos < $len; $pos++) {
+            if($this->elements[$pos]->getType() == $type) {
+                return $pos;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Gets the position of the first element matching the attribute
+     *
+     * @param string $name Name of the attribute
+     * @param string $value Value the attribute should have
+     * @param int $offset search from this position onward
+     * @return false|int position of element if found, otherwise false
+     */
+    public function findPositionByAttribute($name, $value, $offset = 0) {
+        $len = $this->elementCount();
+        for($pos = $offset; $pos < $len; $pos++) {
+            if($this->elements[$pos]->attr($name) == $value) {
+                return $pos;
+            }
+        }
+        return false;
+    }
+
+    #endregion
+
+    #region Element positioning functions
+
+    /**
+     * Adds or inserts an element to the form
+     *
+     * @param Element $element
+     * @param int $pos 0-based position in the form, -1 for at the end
+     * @return Element
+     */
+    public function addElement(Element $element, $pos = -1) {
+        if(is_a($element, '\dokuwiki\Form\Form')) throw new \InvalidArgumentException('You can\'t add a form to a form');
+        if($pos < 0) {
+            $this->elements[] = $element;
+        } else {
+            array_splice($this->elements, $pos, 0, array($element));
+        }
+        return $element;
+    }
+
+    /**
+     * Replaces an existing element with a new one
+     *
+     * @param Element $element the new element
+     * @param int $pos 0-based position of the element to replace
+     */
+    public function replaceElement(Element $element, $pos) {
+        if(is_a($element, '\dokuwiki\Form\Form')) throw new \InvalidArgumentException('You can\'t add a form to a form');
+        array_splice($this->elements, $pos, 1, array($element));
+    }
+
+    /**
+     * Remove an element from the form completely
+     *
+     * @param int $pos 0-based position of the element to remove
+     */
+    public function removeElement($pos) {
+        array_splice($this->elements, $pos, 1);
+    }
+
+    #endregion
+
+    #region Element adding functions
+
+    /**
+     * Adds a text input field
+     *
+     * @param string $name
+     * @param string $label
+     * @param int $pos
+     * @return InputElement
+     */
+    public function addTextInput($name, $label = '', $pos = -1) {
+        return $this->addElement(new InputElement('text', $name, $label), $pos);
+    }
+
+    /**
+     * Adds a password input field
+     *
+     * @param string $name
+     * @param string $label
+     * @param int $pos
+     * @return InputElement
+     */
+    public function addPasswordInput($name, $label = '', $pos = -1) {
+        return $this->addElement(new InputElement('password', $name, $label), $pos);
+    }
+
+    /**
+     * Adds a radio button field
+     *
+     * @param string $name
+     * @param string $label
+     * @param int $pos
+     * @return CheckableElement
+     */
+    public function addRadioButton($name, $label = '', $pos = -1) {
+        return $this->addElement(new CheckableElement('radio', $name, $label), $pos);
+    }
+
+    /**
+     * Adds a checkbox field
+     *
+     * @param string $name
+     * @param string $label
+     * @param int $pos
+     * @return CheckableElement
+     */
+    public function addCheckbox($name, $label = '', $pos = -1) {
+        return $this->addElement(new CheckableElement('checkbox', $name, $label), $pos);
+    }
+
+    /**
+     * Adds a dropdown field
+     *
+     * @param string $name
+     * @param array $options
+     * @param string $label
+     * @param int $pos
+     * @return DropdownElement
+     */
+    public function addDropdown($name, $options, $label = '', $pos = -1) {
+        return $this->addElement(new DropdownElement($name, $options, $label), $pos);
+    }
+
+    /**
+     * Adds a textarea field
+     *
+     * @param string $name
+     * @param string $label
+     * @param int $pos
+     * @return TextareaElement
+     */
+    public function addTextarea($name, $label = '', $pos = -1) {
+        return $this->addElement(new TextareaElement($name, $label), $pos);
+    }
+
+    /**
+     * Adds a simple button, escapes the content for you
+     *
+     * @param string $name
+     * @param string $content
+     * @param int $pos
+     * @return Element
+     */
+    public function addButton($name, $content, $pos = -1) {
+        return $this->addElement(new ButtonElement($name, hsc($content)), $pos);
+    }
+
+    /**
+     * Adds a simple button, allows HTML for content
+     *
+     * @param string $name
+     * @param string $html
+     * @param int $pos
+     * @return Element
+     */
+    public function addButtonHTML($name, $html, $pos = -1) {
+        return $this->addElement(new ButtonElement($name, $html), $pos);
+    }
+
+    /**
+     * Adds a label referencing another input element, escapes the label for you
+     *
+     * @param string $label
+     * @param string $for
+     * @param int $pos
+     * @return Element
+     */
+    public function addLabel($label, $for='', $pos = -1) {
+        return $this->addLabelHTML(hsc($label), $for, $pos);
+    }
+
+    /**
+     * Adds a label referencing another input element, allows HTML for content
+     *
+     * @param string $content
+     * @param string|Element $for
+     * @param int $pos
+     * @return Element
+     */
+    public function addLabelHTML($content, $for='', $pos = -1) {
+        $element = new LabelElement(hsc($content));
+
+        if(is_a($for, '\dokuwiki\Form\Element')) {
+            /** @var Element $for */
+            $for = $for->id();
+        }
+        $for = (string) $for;
+        if($for !== '') {
+            $element->attr('for', $for);
+        }
+
+        return $this->addElement($element, $pos);
+    }
+
+    /**
+     * Add fixed HTML to the form
+     *
+     * @param string $html
+     * @param int $pos
+     * @return HTMLElement
+     */
+    public function addHTML($html, $pos = -1) {
+        return $this->addElement(new HTMLElement($html), $pos);
+    }
+
+    /**
+     * Add a closed HTML tag to the form
+     *
+     * @param string $tag
+     * @param int $pos
+     * @return TagElement
+     */
+    public function addTag($tag, $pos = -1) {
+        return $this->addElement(new TagElement($tag), $pos);
+    }
+
+    /**
+     * Add an open HTML tag to the form
+     *
+     * Be sure to close it again!
+     *
+     * @param string $tag
+     * @param int $pos
+     * @return TagOpenElement
+     */
+    public function addTagOpen($tag, $pos = -1) {
+        return $this->addElement(new TagOpenElement($tag), $pos);
+    }
+
+    /**
+     * Add a closing HTML tag to the form
+     *
+     * Be sure it had been opened before
+     *
+     * @param string $tag
+     * @param int $pos
+     * @return TagCloseElement
+     */
+    public function addTagClose($tag, $pos = -1) {
+        return $this->addElement(new TagCloseElement($tag), $pos);
+    }
+
+    /**
+     * Open a Fieldset
+     *
+     * @param string $legend
+     * @param int $pos
+     * @return FieldsetOpenElement
+     */
+    public function addFieldsetOpen($legend = '', $pos = -1) {
+        return $this->addElement(new FieldsetOpenElement($legend), $pos);
+    }
+
+    /**
+     * Close a fieldset
+     *
+     * @param int $pos
+     * @return TagCloseElement
+     */
+    public function addFieldsetClose($pos = -1) {
+        return $this->addElement(new FieldsetCloseElement(), $pos);
+    }
+
+    #endregion
+
+    /**
+     * Adjust the elements so that fieldset open and closes are matching
+     */
+    protected function balanceFieldsets() {
+        $lastclose = 0;
+        $isopen = false;
+        $len = count($this->elements);
+
+        for($pos = 0; $pos < $len; $pos++) {
+            $type = $this->elements[$pos]->getType();
+            if($type == 'fieldsetopen') {
+                if($isopen) {
+                    //close previous fieldset
+                    $this->addFieldsetClose($pos);
+                    $lastclose = $pos + 1;
+                    $pos++;
+                    $len++;
+                }
+                $isopen = true;
+            } else if($type == 'fieldsetclose') {
+                if(!$isopen) {
+                    // make sure there was a fieldsetopen
+                    // either right after the last close or at the begining
+                    $this->addFieldsetOpen('', $lastclose);
+                    $len++;
+                    $pos++;
+                }
+                $lastclose = $pos;
+                $isopen = false;
+            }
+        }
+
+        // close open fieldset at the end
+        if($isopen) {
+            $this->addFieldsetClose();
+        }
+    }
+
+    /**
+     * The HTML representation of the whole form
+     *
+     * @return string
+     */
+    public function toHTML() {
+        $this->balanceFieldsets();
+
+        $html = '<form ' . buildAttributes($this->attrs()) . '>';
+
+        foreach($this->hidden as $name => $value) {
+            $html .= '<input type="hidden" name="' . $name . '" value="' . formText($value) . '" />';
+        }
+
+        foreach($this->elements as $element) {
+            $html .= $element->toHTML();
+        }
+
+        $html .= '</form>';
+
+        return $html;
+    }
+}
diff --git a/wiki/inc/Form/HTMLElement.php b/wiki/inc/Form/HTMLElement.php
new file mode 100644
index 0000000..591cf47
--- /dev/null
+++ b/wiki/inc/Form/HTMLElement.php
@@ -0,0 +1,29 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class HTMLElement
+ *
+ * Holds arbitrary HTML that is added as is to the Form
+ *
+ * @package dokuwiki\Form
+ */
+class HTMLElement extends ValueElement {
+
+
+    /**
+     * @param string $html
+     */
+    public function __construct($html) {
+        parent::__construct('html', $html);
+    }
+
+    /**
+     * The HTML representation of this element
+     *
+     * @return string
+     */
+    public function toHTML() {
+        return $this->val();
+    }
+}
diff --git a/wiki/inc/Form/InputElement.php b/wiki/inc/Form/InputElement.php
new file mode 100644
index 0000000..0242b61
--- /dev/null
+++ b/wiki/inc/Form/InputElement.php
@@ -0,0 +1,159 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class InputElement
+ *
+ * Base class for all input elements. Uses a wrapping label when label
+ * text is given.
+ *
+ * @todo figure out how to make wrapping or related label configurable
+ * @package dokuwiki\Form
+ */
+class InputElement extends Element {
+    /**
+     * @var LabelElement
+     */
+    protected $label = null;
+
+    /**
+     * @var bool if the element should reflect posted values
+     */
+    protected $useInput = true;
+
+    /**
+     * @param string $type The type of this element
+     * @param string $name The name of this form element
+     * @param string $label The label text for this element (will be autoescaped)
+     */
+    public function __construct($type, $name, $label = '') {
+        parent::__construct($type, array('name' => $name));
+        $this->attr('name', $name);
+        $this->attr('type', $type);
+        if($label) $this->label = new LabelElement($label);
+    }
+
+    /**
+     * Returns the label element if there's one set
+     *
+     * @return LabelElement|null
+     */
+    public function getLabel() {
+        return $this->label;
+    }
+
+    /**
+     * Should the user sent input be used to initialize the input field
+     *
+     * The default is true. Any set values will be overwritten by the INPUT
+     * provided values.
+     *
+     * @param bool $useinput
+     * @return $this
+     */
+    public function useInput($useinput) {
+        $this->useInput = (bool) $useinput;
+        return $this;
+    }
+
+    /**
+     * Get or set the element's ID
+     *
+     * @param null|string $id
+     * @return string|$this
+     */
+    public function id($id = null) {
+        if($this->label) $this->label->attr('for', $id);
+        return parent::id($id);
+    }
+
+    /**
+     * Adds a class to the class attribute
+     *
+     * This is the preferred method of setting the element's class
+     *
+     * @param string $class the new class to add
+     * @return $this
+     */
+    public function addClass($class) {
+        if($this->label) $this->label->addClass($class);
+        return parent::addClass($class);
+    }
+
+    /**
+     * Figures out how to access the value for this field from INPUT data
+     *
+     * The element's name could have been given as a simple string ('foo')
+     * or in array notation ('foo[bar]').
+     *
+     * Note: this function only handles one level of arrays. If your data
+     * is nested deeper, you should call useInput(false) and set the
+     * correct value yourself
+     *
+     * @return array name and array key (null if not an array)
+     */
+    protected function getInputName() {
+        $name = $this->attr('name');
+        parse_str("$name=1", $parsed);
+
+        $name = array_keys($parsed);
+        $name = array_shift($name);
+
+        if(is_array($parsed[$name])) {
+            $key = array_keys($parsed[$name]);
+            $key = array_shift($key);
+        } else {
+            $key = null;
+        }
+
+        return array($name, $key);
+    }
+
+    /**
+     * Handles the useInput flag and set the value attribute accordingly
+     */
+    protected function prefillInput() {
+        global $INPUT;
+
+        list($name, $key) = $this->getInputName();
+        if(!$INPUT->has($name)) return;
+
+        if($key === null) {
+            $value = $INPUT->str($name);
+        } else {
+            $value = $INPUT->arr($name);
+            if(isset($value[$key])) {
+                $value = $value[$key];
+            } else {
+                $value = '';
+            }
+        }
+        $this->val($value);
+    }
+
+    /**
+     * The HTML representation of this element
+     *
+     * @return string
+     */
+    protected function mainElementHTML() {
+        if($this->useInput) $this->prefillInput();
+        return '<input ' . buildAttributes($this->attrs()) . ' />';
+    }
+
+    /**
+     * The HTML representation of this element wrapped in a label
+     *
+     * @return string
+     */
+    public function toHTML() {
+        if($this->label) {
+            return '<label ' . buildAttributes($this->label->attrs()) . '>' . DOKU_LF .
+            '<span>' . hsc($this->label->val()) . '</span>' . DOKU_LF .
+            $this->mainElementHTML() . DOKU_LF .
+            '</label>';
+        } else {
+            return $this->mainElementHTML();
+        }
+    }
+}
diff --git a/wiki/inc/Form/LabelElement.php b/wiki/inc/Form/LabelElement.php
new file mode 100644
index 0000000..9c8d542
--- /dev/null
+++ b/wiki/inc/Form/LabelElement.php
@@ -0,0 +1,27 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class Label
+ * @package dokuwiki\Form
+ */
+class LabelElement extends ValueElement {
+
+    /**
+     * Creates a new Label
+     *
+     * @param string $label This is is raw HTML and will not be escaped
+     */
+    public function __construct($label) {
+        parent::__construct('label', $label);
+    }
+
+    /**
+     * The HTML representation of this element
+     *
+     * @return string
+     */
+    public function toHTML() {
+        return '<label ' . buildAttributes($this->attrs()) . '>' . $this->val() . '</label>';
+    }
+}
diff --git a/wiki/inc/Form/LegacyForm.php b/wiki/inc/Form/LegacyForm.php
new file mode 100644
index 0000000..1b47ba2
--- /dev/null
+++ b/wiki/inc/Form/LegacyForm.php
@@ -0,0 +1,181 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class LegacyForm
+ *
+ * Provides a compatibility layer to the old Doku_Form API
+ *
+ * This can be used to work with the modern API on forms provided by old events for
+ * example. When you start new forms, just use Form\Form
+ *
+ * @package dokuwiki\Form
+ */
+class LegacyForm extends Form {
+
+    /**
+     * Creates a new modern form from an old legacy Doku_Form
+     *
+     * @param \Doku_Form $oldform
+     */
+    public function __construct(\Doku_Form $oldform) {
+        parent::__construct($oldform->params);
+
+        $this->hidden = $oldform->_hidden;
+
+        foreach($oldform->_content as $element) {
+            list($ctl, $attr) = $this->parseLegacyAttr($element);
+
+            if(is_array($element)) {
+                switch($ctl['elem']) {
+                    case 'wikitext':
+                        $this->addTextarea('wikitext')
+                             ->attrs($attr)
+                             ->id('wiki__text')
+                             ->val($ctl['text'])
+                             ->addClass($ctl['class']);
+                        break;
+                    case 'textfield':
+                        $this->addTextInput($ctl['name'], $ctl['text'])
+                             ->attrs($attr)
+                             ->id($ctl['id'])
+                             ->addClass($ctl['class']);
+                        break;
+                    case 'passwordfield':
+                        $this->addPasswordInput($ctl['name'], $ctl['text'])
+                             ->attrs($attr)
+                             ->id($ctl['id'])
+                             ->addClass($ctl['class']);
+                        break;
+                    case 'checkboxfield':
+                        $this->addCheckbox($ctl['name'], $ctl['text'])
+                             ->attrs($attr)
+                             ->id($ctl['id'])
+                             ->addClass($ctl['class']);
+                        break;
+                    case 'radiofield':
+                        $this->addRadioButton($ctl['name'], $ctl['text'])
+                             ->attrs($attr)
+                             ->id($ctl['id'])
+                             ->addClass($ctl['class']);
+                        break;
+                    case 'tag':
+                        $this->addTag($ctl['tag'])
+                             ->attrs($attr)
+                             ->attr('name', $ctl['name'])
+                             ->id($ctl['id'])
+                             ->addClass($ctl['class']);
+                        break;
+                    case 'opentag':
+                        $this->addTagOpen($ctl['tag'])
+                             ->attrs($attr)
+                             ->attr('name', $ctl['name'])
+                             ->id($ctl['id'])
+                             ->addClass($ctl['class']);
+                        break;
+                    case 'closetag':
+                        $this->addTagClose($ctl['tag']);
+                        break;
+                    case 'openfieldset':
+                        $this->addFieldsetOpen($ctl['legend'])
+                            ->attrs($attr)
+                            ->attr('name', $ctl['name'])
+                            ->id($ctl['id'])
+                            ->addClass($ctl['class']);
+                        break;
+                    case 'closefieldset':
+                        $this->addFieldsetClose();
+                        break;
+                    case 'button':
+                    case 'field':
+                    case 'fieldright':
+                    case 'filefield':
+                    case 'menufield':
+                    case 'listboxfield':
+                        throw new \UnexpectedValueException('Unsupported legacy field ' . $ctl['elem']);
+                        break;
+                    default:
+                        throw new \UnexpectedValueException('Unknown legacy field ' . $ctl['elem']);
+
+                }
+            } else {
+                $this->addHTML($element);
+            }
+        }
+
+    }
+
+    /**
+     * Parses out what is the elements attributes and what is control info
+     *
+     * @param array $legacy
+     * @return array
+     */
+    protected function parseLegacyAttr($legacy) {
+        $attributes = array();
+        $control = array();
+
+        foreach($legacy as $key => $val) {
+            if($key{0} == '_') {
+                $control[substr($key, 1)] = $val;
+            } elseif($key == 'name') {
+                $control[$key] = $val;
+            } elseif($key == 'id') {
+                $control[$key] = $val;
+            } else {
+                $attributes[$key] = $val;
+            }
+        }
+
+        return array($control, $attributes);
+    }
+
+    /**
+     * Translates our types to the legacy types
+     *
+     * @param string $type
+     * @return string
+     */
+    protected function legacyType($type) {
+        static $types = array(
+            'text' => 'textfield',
+            'password' => 'passwordfield',
+            'checkbox' => 'checkboxfield',
+            'radio' => 'radiofield',
+            'tagopen' => 'opentag',
+            'tagclose' => 'closetag',
+            'fieldsetopen' => 'openfieldset',
+            'fieldsetclose' => 'closefieldset',
+        );
+        if(isset($types[$type])) return $types[$type];
+        return $type;
+    }
+
+    /**
+     * Creates an old legacy form from this modern form's data
+     *
+     * @return \Doku_Form
+     */
+    public function toLegacy() {
+        $this->balanceFieldsets();
+
+        $legacy = new \Doku_Form($this->attrs());
+        $legacy->_hidden = $this->hidden;
+        foreach($this->elements as $element) {
+            if(is_a($element, 'dokuwiki\Form\HTMLElement')) {
+                $legacy->_content[] = $element->toHTML();
+            } elseif(is_a($element, 'dokuwiki\Form\InputElement')) {
+                /** @var InputElement $element */
+                $data = $element->attrs();
+                $data['_elem'] = $this->legacyType($element->getType());
+                $label = $element->getLabel();
+                if($label) {
+                    $data['_class'] = $label->attr('class');
+                }
+                $legacy->_content[] = $data;
+            }
+        }
+
+        return $legacy;
+    }
+}
diff --git a/wiki/inc/Form/OptGroup.php b/wiki/inc/Form/OptGroup.php
new file mode 100644
index 0000000..791f0b3
--- /dev/null
+++ b/wiki/inc/Form/OptGroup.php
@@ -0,0 +1,100 @@
+<?php
+
+namespace dokuwiki\Form;
+
+
+class OptGroup extends Element {
+    protected $options = array();
+    protected $value;
+
+    /**
+     * @param string $label The label text for this element (will be autoescaped)
+     * @param array  $options The available options
+     */
+    public function __construct($label, $options) {
+        parent::__construct('optGroup', array('label' => $label));
+        $this->options($options);
+    }
+
+    /**
+     * Store the given value so it can be used during rendering
+     *
+     * This is intended to be only called from within @see DropdownElement::val()
+     *
+     * @param string $value
+     * @return bool true if an option with the given value exists, false otherwise
+     */
+    public function storeValue($value) {
+        $this->value = $value;
+        return isset($this->options[$value]);
+    }
+
+    /**
+     * Get or set the options of the optgroup
+     *
+     * Options can be given as associative array (value => label) or as an
+     * indexd array (label = value) or as an array of arrays. In the latter
+     * case an element has to look as follows:
+     * option-value => array (
+     *                 'label' => option-label,
+     *                 'attrs' => array (
+     *                                    attr-key => attr-value, ...
+     *                                  )
+     *                 )
+     *
+     * @param null|array $options
+     * @return $this|array
+     */
+    public function options($options = null) {
+        if($options === null) return $this->options;
+        if(!is_array($options)) throw new \InvalidArgumentException('Options have to be an array');
+        $this->options = array();
+        foreach($options as $key => $val) {
+            if (is_array($val)) {
+                if (!key_exists('label', $val)) throw new \InvalidArgumentException('If option is given as array, it has to have a "label"-key!');
+                if (key_exists('attrs', $val) && is_array($val['attrs']) && key_exists('selected', $val['attrs'])) {
+                    throw new \InvalidArgumentException('Please use function "DropdownElement::val()" to set the selected option');
+                }
+                $this->options[$key] = $val;
+            } elseif(is_int($key)) {
+                $this->options[$val] = array('label' => (string) $val);
+            } else {
+                $this->options[$key] = array('label' => (string) $val);
+            }
+        }
+        return $this;
+    }
+
+
+    /**
+     * The HTML representation of this element
+     *
+     * @return string
+     */
+    public function toHTML() {
+        if ($this->attributes['label'] === null) {
+            return $this->renderOptions();
+        }
+        $html = '<optgroup '. buildAttributes($this->attrs()) . '>';
+        $html .= $this->renderOptions();
+        $html .= '</optgroup>';
+        return $html;
+    }
+
+
+    /**
+     * @return string
+     */
+    protected function renderOptions() {
+        $html = '';
+        foreach($this->options as $key => $val) {
+            $selected = ((string)$key === (string)$this->value) ? ' selected="selected"' : '';
+            $attrs = '';
+            if (!empty($val['attrs']) && is_array($val['attrs'])) {
+                $attrs = buildAttributes($val['attrs']);
+            }
+            $html .= '<option' . $selected . ' value="' . hsc($key) . '" '.$attrs.'>' . hsc($val['label']) . '</option>';
+        }
+        return $html;
+    }
+}
diff --git a/wiki/inc/Form/TagCloseElement.php b/wiki/inc/Form/TagCloseElement.php
new file mode 100644
index 0000000..b6bf753
--- /dev/null
+++ b/wiki/inc/Form/TagCloseElement.php
@@ -0,0 +1,88 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class TagCloseElement
+ *
+ * Creates an HTML close tag. You have to make sure it has been opened
+ * before or this will produce invalid HTML
+ *
+ * @package dokuwiki\Form
+ */
+class TagCloseElement extends ValueElement {
+
+    /**
+     * @param string $tag
+     * @param array $attributes
+     */
+    public function __construct($tag, $attributes = array()) {
+        parent::__construct('tagclose', $tag, $attributes);
+    }
+
+    /**
+     * do not call this
+     *
+     * @param string $class
+     * @return void
+     * @throws \BadMethodCallException
+     */
+    public function addClass($class) {
+        throw new \BadMethodCallException('You can\t add classes to closing tag');
+    }
+
+    /**
+     * do not call this
+     *
+     * @param null|string $id
+     * @return string
+     * @throws \BadMethodCallException
+     */
+    public function id($id = null) {
+        if ($id === null) {
+            return '';
+        } else {
+            throw new \BadMethodCallException('You can\t add ID to closing tag');
+        }
+    }
+
+    /**
+     * do not call this
+     *
+     * @param string $name
+     * @param null|string $value
+     * @return string
+     * @throws \BadMethodCallException
+     */
+    public function attr($name, $value = null) {
+        if ($value === null) {
+            return '';
+        } else {
+            throw new \BadMethodCallException('You can\t add attributes to closing tag');
+        }
+    }
+
+    /**
+     * do not call this
+     *
+     * @param array|null $attributes
+     * @return array
+     * @throws \BadMethodCallException
+     */
+    public function attrs($attributes = null) {
+        if ($attributes === null) {
+            return array();
+        } else {
+            throw new \BadMethodCallException('You can\t add attributes to closing tag');
+        }
+    }
+
+    /**
+     * The HTML representation of this element
+     *
+     * @return string
+     */
+    public function toHTML() {
+        return '</'.$this->val().'>';
+    }
+
+}
diff --git a/wiki/inc/Form/TagElement.php b/wiki/inc/Form/TagElement.php
new file mode 100644
index 0000000..ea5144c
--- /dev/null
+++ b/wiki/inc/Form/TagElement.php
@@ -0,0 +1,29 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class TagElement
+ *
+ * Creates a self closing HTML tag
+ *
+ * @package dokuwiki\Form
+ */
+class TagElement extends ValueElement {
+
+    /**
+     * @param string $tag
+     * @param array $attributes
+     */
+    public function __construct($tag, $attributes = array()) {
+        parent::__construct('tag', $tag, $attributes);
+    }
+
+    /**
+     * The HTML representation of this element
+     *
+     * @return string
+     */
+    public function toHTML() {
+        return '<'.$this->val().' '.buildAttributes($this->attrs()).' />';
+    }
+}
diff --git a/wiki/inc/Form/TagOpenElement.php b/wiki/inc/Form/TagOpenElement.php
new file mode 100644
index 0000000..0afe97b
--- /dev/null
+++ b/wiki/inc/Form/TagOpenElement.php
@@ -0,0 +1,30 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class TagOpenElement
+ *
+ * Creates an open HTML tag. You have to make sure you close it
+ * again or this will produce invalid HTML
+ *
+ * @package dokuwiki\Form
+ */
+class TagOpenElement extends ValueElement {
+
+    /**
+     * @param string $tag
+     * @param array $attributes
+     */
+    public function __construct($tag, $attributes = array()) {
+        parent::__construct('tagopen', $tag, $attributes);
+    }
+
+    /**
+     * The HTML representation of this element
+     *
+     * @return string
+     */
+    public function toHTML() {
+        return '<'.$this->val().' '.buildAttributes($this->attrs()).'>';
+    }
+}
diff --git a/wiki/inc/Form/TextareaElement.php b/wiki/inc/Form/TextareaElement.php
new file mode 100644
index 0000000..92741ee
--- /dev/null
+++ b/wiki/inc/Form/TextareaElement.php
@@ -0,0 +1,51 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class TextareaElement
+ * @package dokuwiki\Form
+ */
+class TextareaElement extends InputElement {
+
+    /**
+     * @var string the actual text within the area
+     */
+    protected $text;
+
+    /**
+     * @param string $name The name of this form element
+     * @param string $label The label text for this element
+     */
+    public function __construct($name, $label) {
+        parent::__construct('textarea', $name, $label);
+        $this->attr('dir', 'auto');
+    }
+
+    /**
+     * Get or set the element's value
+     *
+     * This is the preferred way of setting the element's value
+     *
+     * @param null|string $value
+     * @return string|$this
+     */
+    public function val($value = null) {
+        if($value !== null) {
+            $this->text = cleanText($value);
+            return $this;
+        }
+        return $this->text;
+    }
+
+    /**
+     * The HTML representation of this element
+     *
+     * @return string
+     */
+    protected function mainElementHTML() {
+        if($this->useInput) $this->prefillInput();
+        return '<textarea ' . buildAttributes($this->attrs()) . '>' .
+        formText($this->val()) . '</textarea>';
+    }
+
+}
diff --git a/wiki/inc/Form/ValueElement.php b/wiki/inc/Form/ValueElement.php
new file mode 100644
index 0000000..88db167
--- /dev/null
+++ b/wiki/inc/Form/ValueElement.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace dokuwiki\Form;
+
+/**
+ * Class ValueElement
+ *
+ * Just like an Element but it's value is not part of its attributes
+ *
+ * What the value is (tag name, content, etc) is defined by the actual implementations
+ *
+ * @package dokuwiki\Form
+ */
+abstract class ValueElement extends Element {
+
+    /**
+     * @var string holds the element's value
+     */
+    protected $value = '';
+
+    /**
+     * @param string $type
+     * @param string $value
+     * @param array $attributes
+     */
+    public function __construct($type, $value, $attributes = array()) {
+        parent::__construct($type, $attributes);
+        $this->val($value);
+    }
+
+    /**
+     * Get or set the element's value
+     *
+     * @param null|string $value
+     * @return string|$this
+     */
+    public function val($value = null) {
+        if($value !== null) {
+            $this->value = $value;
+            return $this;
+        }
+        return $this->value;
+    }
+
+}