Form.php 13 KB

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