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