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