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