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