1<?php
2namespace dokuwiki\Form;
3
4/**
5 * Class Form
6 *
7 * Represents the whole Form. This is what you work on, and add Elements to
8 *
9 * @package dokuwiki\Form
10 */
11class Form extends Element {
12
13    /**
14     * @var array name value pairs for hidden values
15     */
16    protected $hidden = array();
17
18    /**
19     * @var Element[] the elements of the form
20     */
21    protected $elements = array();
22
23    /**
24     * Creates a new, empty form with some default attributes
25     *
26     * @param array $attributes
27     * @param bool  $unsafe     if true, then the security token is ommited
28     */
29    public function __construct($attributes = array(), $unsafe = false) {
30        global $ID;
31
32        parent::__construct('form', $attributes);
33
34        // use the current URL as default action
35        if(!$this->attr('action')) {
36            $get = $_GET;
37            if(isset($get['id'])) unset($get['id']);
38            $self = wl($ID, $get, false, '&'); //attributes are escaped later
39            $this->attr('action', $self);
40        }
41
42        // post is default
43        if(!$this->attr('method')) {
44            $this->attr('method', 'post');
45        }
46
47        // we like UTF-8
48        if(!$this->attr('accept-charset')) {
49            $this->attr('accept-charset', 'utf-8');
50        }
51
52        // add the security token by default
53        if (!$unsafe) {
54            $this->setHiddenField('sectok', getSecurityToken());
55        }
56
57        // identify this as a new form based form in HTML
58        $this->addClass('doku_form');
59    }
60
61    /**
62     * Sets a hidden field
63     *
64     * @param string $name
65     * @param string $value
66     * @return $this
67     */
68    public function setHiddenField($name, $value) {
69        $this->hidden[$name] = $value;
70        return $this;
71    }
72
73    #region element query function
74
75    /**
76     * Returns the numbers of elements in the form
77     *
78     * @return int
79     */
80    public function elementCount() {
81        return count($this->elements);
82    }
83
84    /**
85     * Get the position of the element in the form or false if it is not in the form
86     *
87     * Warning: This function may return Boolean FALSE, but may also return a non-Boolean value which evaluates
88     * to FALSE. Please read the section on Booleans for more information. Use the === operator for testing the
89     * return value of this function.
90     *
91     * @param Element $element
92     *
93     * @return false|int
94     */
95    public function getElementPosition(Element $element)
96    {
97        return array_search($element, $this->elements, true);
98    }
99
100    /**
101     * Returns a reference to the element at a position.
102     * A position out-of-bounds will return either the
103     * first (underflow) or last (overflow) element.
104     *
105     * @param int $pos
106     * @return Element
107     */
108    public function getElementAt($pos) {
109        if($pos < 0) $pos = count($this->elements) + $pos;
110        if($pos < 0) $pos = 0;
111        if($pos >= count($this->elements)) $pos = count($this->elements) - 1;
112        return $this->elements[$pos];
113    }
114
115    /**
116     * Gets the position of the first of a type of element
117     *
118     * @param string $type Element type to look for.
119     * @param int $offset search from this position onward
120     * @return false|int position of element if found, otherwise false
121     */
122    public function findPositionByType($type, $offset = 0) {
123        $len = $this->elementCount();
124        for($pos = $offset; $pos < $len; $pos++) {
125            if($this->elements[$pos]->getType() == $type) {
126                return $pos;
127            }
128        }
129        return false;
130    }
131
132    /**
133     * Gets the position of the first element matching the attribute
134     *
135     * @param string $name Name of the attribute
136     * @param string $value Value the attribute should have
137     * @param int $offset search from this position onward
138     * @return false|int position of element if found, otherwise false
139     */
140    public function findPositionByAttribute($name, $value, $offset = 0) {
141        $len = $this->elementCount();
142        for($pos = $offset; $pos < $len; $pos++) {
143            if($this->elements[$pos]->attr($name) == $value) {
144                return $pos;
145            }
146        }
147        return false;
148    }
149
150    #endregion
151
152    #region Element positioning functions
153
154    /**
155     * Adds or inserts an element to the form
156     *
157     * @param Element $element
158     * @param int $pos 0-based position in the form, -1 for at the end
159     * @return Element
160     */
161    public function addElement(Element $element, $pos = -1) {
162        if(is_a($element, '\dokuwiki\Form\Form')) throw new \InvalidArgumentException(
163            'You can\'t add a form to a form'
164        );
165        if($pos < 0) {
166            $this->elements[] = $element;
167        } else {
168            array_splice($this->elements, $pos, 0, array($element));
169        }
170        return $element;
171    }
172
173    /**
174     * Replaces an existing element with a new one
175     *
176     * @param Element $element the new element
177     * @param int $pos 0-based position of the element to replace
178     */
179    public function replaceElement(Element $element, $pos) {
180        if(is_a($element, '\dokuwiki\Form\Form')) throw new \InvalidArgumentException(
181            'You can\'t add a form to a form'
182        );
183        array_splice($this->elements, $pos, 1, array($element));
184    }
185
186    /**
187     * Remove an element from the form completely
188     *
189     * @param int $pos 0-based position of the element to remove
190     */
191    public function removeElement($pos) {
192        array_splice($this->elements, $pos, 1);
193    }
194
195    #endregion
196
197    #region Element adding functions
198
199    /**
200     * Adds a text input field
201     *
202     * @param string $name
203     * @param string $label
204     * @param int $pos
205     * @return InputElement
206     */
207    public function addTextInput($name, $label = '', $pos = -1) {
208        return $this->addElement(new InputElement('text', $name, $label), $pos);
209    }
210
211    /**
212     * Adds a password input field
213     *
214     * @param string $name
215     * @param string $label
216     * @param int $pos
217     * @return InputElement
218     */
219    public function addPasswordInput($name, $label = '', $pos = -1) {
220        return $this->addElement(new InputElement('password', $name, $label), $pos);
221    }
222
223    /**
224     * Adds a radio button field
225     *
226     * @param string $name
227     * @param string $label
228     * @param int $pos
229     * @return CheckableElement
230     */
231    public function addRadioButton($name, $label = '', $pos = -1) {
232        return $this->addElement(new CheckableElement('radio', $name, $label), $pos);
233    }
234
235    /**
236     * Adds a checkbox field
237     *
238     * @param string $name
239     * @param string $label
240     * @param int $pos
241     * @return CheckableElement
242     */
243    public function addCheckbox($name, $label = '', $pos = -1) {
244        return $this->addElement(new CheckableElement('checkbox', $name, $label), $pos);
245    }
246
247    /**
248     * Adds a dropdown field
249     *
250     * @param string $name
251     * @param array $options
252     * @param string $label
253     * @param int $pos
254     * @return DropdownElement
255     */
256    public function addDropdown($name, $options, $label = '', $pos = -1) {
257        return $this->addElement(new DropdownElement($name, $options, $label), $pos);
258    }
259
260    /**
261     * Adds a textarea field
262     *
263     * @param string $name
264     * @param string $label
265     * @param int $pos
266     * @return TextareaElement
267     */
268    public function addTextarea($name, $label = '', $pos = -1) {
269        return $this->addElement(new TextareaElement($name, $label), $pos);
270    }
271
272    /**
273     * Adds a simple button, escapes the content for you
274     *
275     * @param string $name
276     * @param string $content
277     * @param int $pos
278     * @return Element
279     */
280    public function addButton($name, $content, $pos = -1) {
281        return $this->addElement(new ButtonElement($name, hsc($content)), $pos);
282    }
283
284    /**
285     * Adds a simple button, allows HTML for content
286     *
287     * @param string $name
288     * @param string $html
289     * @param int $pos
290     * @return Element
291     */
292    public function addButtonHTML($name, $html, $pos = -1) {
293        return $this->addElement(new ButtonElement($name, $html), $pos);
294    }
295
296    /**
297     * Adds a label referencing another input element, escapes the label for you
298     *
299     * @param string $label
300     * @param string $for
301     * @param int $pos
302     * @return Element
303     */
304    public function addLabel($label, $for='', $pos = -1) {
305        return $this->addLabelHTML(hsc($label), $for, $pos);
306    }
307
308    /**
309     * Adds a label referencing another input element, allows HTML for content
310     *
311     * @param string $content
312     * @param string|Element $for
313     * @param int $pos
314     * @return Element
315     */
316    public function addLabelHTML($content, $for='', $pos = -1) {
317        $element = new LabelElement(hsc($content));
318
319        if(is_a($for, '\dokuwiki\Form\Element')) {
320            /** @var Element $for */
321            $for = $for->id();
322        }
323        $for = (string) $for;
324        if($for !== '') {
325            $element->attr('for', $for);
326        }
327
328        return $this->addElement($element, $pos);
329    }
330
331    /**
332     * Add fixed HTML to the form
333     *
334     * @param string $html
335     * @param int $pos
336     * @return HTMLElement
337     */
338    public function addHTML($html, $pos = -1) {
339        return $this->addElement(new HTMLElement($html), $pos);
340    }
341
342    /**
343     * Add a closed HTML tag to the form
344     *
345     * @param string $tag
346     * @param int $pos
347     * @return TagElement
348     */
349    public function addTag($tag, $pos = -1) {
350        return $this->addElement(new TagElement($tag), $pos);
351    }
352
353    /**
354     * Add an open HTML tag to the form
355     *
356     * Be sure to close it again!
357     *
358     * @param string $tag
359     * @param int $pos
360     * @return TagOpenElement
361     */
362    public function addTagOpen($tag, $pos = -1) {
363        return $this->addElement(new TagOpenElement($tag), $pos);
364    }
365
366    /**
367     * Add a closing HTML tag to the form
368     *
369     * Be sure it had been opened before
370     *
371     * @param string $tag
372     * @param int $pos
373     * @return TagCloseElement
374     */
375    public function addTagClose($tag, $pos = -1) {
376        return $this->addElement(new TagCloseElement($tag), $pos);
377    }
378
379    /**
380     * Open a Fieldset
381     *
382     * @param string $legend
383     * @param int $pos
384     * @return FieldsetOpenElement
385     */
386    public function addFieldsetOpen($legend = '', $pos = -1) {
387        return $this->addElement(new FieldsetOpenElement($legend), $pos);
388    }
389
390    /**
391     * Close a fieldset
392     *
393     * @param int $pos
394     * @return TagCloseElement
395     */
396    public function addFieldsetClose($pos = -1) {
397        return $this->addElement(new FieldsetCloseElement(), $pos);
398    }
399
400    #endregion
401
402    /**
403     * Adjust the elements so that fieldset open and closes are matching
404     */
405    protected function balanceFieldsets() {
406        $lastclose = 0;
407        $isopen = false;
408        $len = count($this->elements);
409
410        for($pos = 0; $pos < $len; $pos++) {
411            $type = $this->elements[$pos]->getType();
412            if($type == 'fieldsetopen') {
413                if($isopen) {
414                    //close previous fieldset
415                    $this->addFieldsetClose($pos);
416                    $lastclose = $pos + 1;
417                    $pos++;
418                    $len++;
419                }
420                $isopen = true;
421            } else if($type == 'fieldsetclose') {
422                if(!$isopen) {
423                    // make sure there was a fieldsetopen
424                    // either right after the last close or at the begining
425                    $this->addFieldsetOpen('', $lastclose);
426                    $len++;
427                    $pos++;
428                }
429                $lastclose = $pos;
430                $isopen = false;
431            }
432        }
433
434        // close open fieldset at the end
435        if($isopen) {
436            $this->addFieldsetClose();
437        }
438    }
439
440    /**
441     * The HTML representation of the whole form
442     *
443     * @return string
444     */
445    public function toHTML() {
446        $this->balanceFieldsets();
447
448        $html = '<form ' . buildAttributes($this->attrs()) . '>';
449
450        foreach($this->hidden as $name => $value) {
451            $html .= '<input type="hidden" name="' . $name . '" value="' . formText($value) . '" />';
452        }
453
454        foreach($this->elements as $element) {
455            $html .= $element->toHTML();
456        }
457
458        $html .= '</form>';
459
460        return $html;
461    }
462}
463