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