1<?php 2 3namespace dokuwiki\Form; 4 5use 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 */ 14class 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