xref: /dokuwiki/inc/Form/Form.php (revision a00078f7c6131eb152c1f51a05a0753843638bd6)
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 to FALSE. Please read the section on Booleans for more information. Use the === operator for testing the return value of this function.
88     *
89     * @param Element $element
90     *
91     * @return false|int
92     */
93    public function getElementPosition(Element $element)
94    {
95        return array_search($element, $this->elements, true);
96    }
97
98    /**
99     * Returns a reference to the element at a position.
100     * A position out-of-bounds will return either the
101     * first (underflow) or last (overflow) element.
102     *
103     * @param int $pos
104     * @return Element
105     */
106    public function getElementAt($pos) {
107        if($pos < 0) $pos = count($this->elements) + $pos;
108        if($pos < 0) $pos = 0;
109        if($pos >= count($this->elements)) $pos = count($this->elements) - 1;
110        return $this->elements[$pos];
111    }
112
113    /**
114     * Gets the position of the first of a type of element
115     *
116     * @param string $type Element type to look for.
117     * @param int $offset search from this position onward
118     * @return false|int position of element if found, otherwise false
119     */
120    public function findPositionByType($type, $offset = 0) {
121        $len = $this->elementCount();
122        for($pos = $offset; $pos < $len; $pos++) {
123            if($this->elements[$pos]->getType() == $type) {
124                return $pos;
125            }
126        }
127        return false;
128    }
129
130    /**
131     * Gets the position of the first element matching the attribute
132     *
133     * @param string $name Name of the attribute
134     * @param string $value Value the attribute should have
135     * @param int $offset search from this position onward
136     * @return false|int position of element if found, otherwise false
137     */
138    public function findPositionByAttribute($name, $value, $offset = 0) {
139        $len = $this->elementCount();
140        for($pos = $offset; $pos < $len; $pos++) {
141            if($this->elements[$pos]->attr($name) == $value) {
142                return $pos;
143            }
144        }
145        return false;
146    }
147
148    #endregion
149
150    #region Element positioning functions
151
152    /**
153     * Adds or inserts an element to the form
154     *
155     * @param Element $element
156     * @param int $pos 0-based position in the form, -1 for at the end
157     * @return Element
158     */
159    public function addElement(Element $element, $pos = -1) {
160        if(is_a($element, '\dokuwiki\Form\Form')) throw new \InvalidArgumentException('You can\'t add a form to a form');
161        if($pos < 0) {
162            $this->elements[] = $element;
163        } else {
164            array_splice($this->elements, $pos, 0, array($element));
165        }
166        return $element;
167    }
168
169    /**
170     * Replaces an existing element with a new one
171     *
172     * @param Element $element the new element
173     * @param int $pos 0-based position of the element to replace
174     */
175    public function replaceElement(Element $element, $pos) {
176        if(is_a($element, '\dokuwiki\Form\Form')) throw new \InvalidArgumentException('You can\'t add a form to a form');
177        array_splice($this->elements, $pos, 1, array($element));
178    }
179
180    /**
181     * Remove an element from the form completely
182     *
183     * @param int $pos 0-based position of the element to remove
184     */
185    public function removeElement($pos) {
186        array_splice($this->elements, $pos, 1);
187    }
188
189    #endregion
190
191    #region Element adding functions
192
193    /**
194     * Adds a text input field
195     *
196     * @param string $name
197     * @param string $label
198     * @param int $pos
199     * @return InputElement
200     */
201    public function addTextInput($name, $label = '', $pos = -1) {
202        return $this->addElement(new InputElement('text', $name, $label), $pos);
203    }
204
205    /**
206     * Adds a password input field
207     *
208     * @param string $name
209     * @param string $label
210     * @param int $pos
211     * @return InputElement
212     */
213    public function addPasswordInput($name, $label = '', $pos = -1) {
214        return $this->addElement(new InputElement('password', $name, $label), $pos);
215    }
216
217    /**
218     * Adds a radio button field
219     *
220     * @param string $name
221     * @param string $label
222     * @param int $pos
223     * @return CheckableElement
224     */
225    public function addRadioButton($name, $label = '', $pos = -1) {
226        return $this->addElement(new CheckableElement('radio', $name, $label), $pos);
227    }
228
229    /**
230     * Adds a checkbox field
231     *
232     * @param string $name
233     * @param string $label
234     * @param int $pos
235     * @return CheckableElement
236     */
237    public function addCheckbox($name, $label = '', $pos = -1) {
238        return $this->addElement(new CheckableElement('checkbox', $name, $label), $pos);
239    }
240
241    /**
242     * Adds a dropdown field
243     *
244     * @param string $name
245     * @param array $options
246     * @param string $label
247     * @param int $pos
248     * @return DropdownElement
249     */
250    public function addDropdown($name, $options, $label = '', $pos = -1) {
251        return $this->addElement(new DropdownElement($name, $options, $label), $pos);
252    }
253
254    /**
255     * Adds a textarea field
256     *
257     * @param string $name
258     * @param string $label
259     * @param int $pos
260     * @return TextareaElement
261     */
262    public function addTextarea($name, $label = '', $pos = -1) {
263        return $this->addElement(new TextareaElement($name, $label), $pos);
264    }
265
266    /**
267     * Adds a simple button, escapes the content for you
268     *
269     * @param string $name
270     * @param string $content
271     * @param int $pos
272     * @return Element
273     */
274    public function addButton($name, $content, $pos = -1) {
275        return $this->addElement(new ButtonElement($name, hsc($content)), $pos);
276    }
277
278    /**
279     * Adds a simple button, allows HTML for content
280     *
281     * @param string $name
282     * @param string $html
283     * @param int $pos
284     * @return Element
285     */
286    public function addButtonHTML($name, $html, $pos = -1) {
287        return $this->addElement(new ButtonElement($name, $html), $pos);
288    }
289
290    /**
291     * Adds a label referencing another input element, escapes the label for you
292     *
293     * @param string $label
294     * @param string $for
295     * @param int $pos
296     * @return Element
297     */
298    public function addLabel($label, $for='', $pos = -1) {
299        return $this->addLabelHTML(hsc($label), $for, $pos);
300    }
301
302    /**
303     * Adds a label referencing another input element, allows HTML for content
304     *
305     * @param string $content
306     * @param string|Element $for
307     * @param int $pos
308     * @return Element
309     */
310    public function addLabelHTML($content, $for='', $pos = -1) {
311        $element = new LabelElement(hsc($content));
312
313        if(is_a($for, '\dokuwiki\Form\Element')) {
314            /** @var Element $for */
315            $for = $for->id();
316        }
317        $for = (string) $for;
318        if($for !== '') {
319            $element->attr('for', $for);
320        }
321
322        return $this->addElement($element, $pos);
323    }
324
325    /**
326     * Add fixed HTML to the form
327     *
328     * @param string $html
329     * @param int $pos
330     * @return HTMLElement
331     */
332    public function addHTML($html, $pos = -1) {
333        return $this->addElement(new HTMLElement($html), $pos);
334    }
335
336    /**
337     * Add a closed HTML tag to the form
338     *
339     * @param string $tag
340     * @param int $pos
341     * @return TagElement
342     */
343    public function addTag($tag, $pos = -1) {
344        return $this->addElement(new TagElement($tag), $pos);
345    }
346
347    /**
348     * Add an open HTML tag to the form
349     *
350     * Be sure to close it again!
351     *
352     * @param string $tag
353     * @param int $pos
354     * @return TagOpenElement
355     */
356    public function addTagOpen($tag, $pos = -1) {
357        return $this->addElement(new TagOpenElement($tag), $pos);
358    }
359
360    /**
361     * Add a closing HTML tag to the form
362     *
363     * Be sure it had been opened before
364     *
365     * @param string $tag
366     * @param int $pos
367     * @return TagCloseElement
368     */
369    public function addTagClose($tag, $pos = -1) {
370        return $this->addElement(new TagCloseElement($tag), $pos);
371    }
372
373    /**
374     * Open a Fieldset
375     *
376     * @param string $legend
377     * @param int $pos
378     * @return FieldsetOpenElement
379     */
380    public function addFieldsetOpen($legend = '', $pos = -1) {
381        return $this->addElement(new FieldsetOpenElement($legend), $pos);
382    }
383
384    /**
385     * Close a fieldset
386     *
387     * @param int $pos
388     * @return TagCloseElement
389     */
390    public function addFieldsetClose($pos = -1) {
391        return $this->addElement(new FieldsetCloseElement(), $pos);
392    }
393
394    #endregion
395
396    /**
397     * Adjust the elements so that fieldset open and closes are matching
398     */
399    protected function balanceFieldsets() {
400        $lastclose = 0;
401        $isopen = false;
402        $len = count($this->elements);
403
404        for($pos = 0; $pos < $len; $pos++) {
405            $type = $this->elements[$pos]->getType();
406            if($type == 'fieldsetopen') {
407                if($isopen) {
408                    //close previous fieldset
409                    $this->addFieldsetClose($pos);
410                    $lastclose = $pos + 1;
411                    $pos++;
412                    $len++;
413                }
414                $isopen = true;
415            } else if($type == 'fieldsetclose') {
416                if(!$isopen) {
417                    // make sure there was a fieldsetopen
418                    // either right after the last close or at the begining
419                    $this->addFieldsetOpen('', $lastclose);
420                    $len++;
421                    $pos++;
422                }
423                $lastclose = $pos;
424                $isopen = false;
425            }
426        }
427
428        // close open fieldset at the end
429        if($isopen) {
430            $this->addFieldsetClose();
431        }
432    }
433
434    /**
435     * The HTML representation of the whole form
436     *
437     * @return string
438     */
439    public function toHTML() {
440        $this->balanceFieldsets();
441
442        $html = '<form ' . buildAttributes($this->attrs()) . '>';
443
444        foreach($this->hidden as $name => $value) {
445            $html .= '<input type="hidden" name="' . $name . '" value="' . formText($value) . '" />';
446        }
447
448        foreach($this->elements as $element) {
449            $html .= $element->toHTML();
450        }
451
452        $html .= '</form>';
453
454        return $html;
455    }
456}
457