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