PageRenderTime 59ms CodeModel.GetById 27ms RepoModel.GetById 0ms app.codeStats 1ms

/common/HTML_MetaForm/HTML_FormPersister/lib/HTML/FormPersister.php

https://github.com/rigidus/izverg
PHP | 451 lines | 260 code | 21 blank | 170 comment | 77 complexity | d97e918f1da2dcecdc0e991b81489b0d MD5 | raw file
  1. <?php
  2. /**
  3. * HTML_FormPersister: in-place "human-expectable" form tags post-processing.
  4. * (C) 2005 Dmitry Koterov, http://forum.dklab.ru/users/DmitryKoterov/
  5. *
  6. * This library is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 2.1 of the License, or (at your option) any later version.
  10. * See http://www.gnu.org/copyleft/lesser.html
  11. * Modify HTML-forms adding "value=..." fields to <input> tags according
  12. * to STANDARD PHP $_GET and $_POST variable. Also supported <select> and
  13. * <textarea>.
  14. *
  15. * The simplest example:
  16. *
  17. * <?
  18. * require_once 'HTML/FormPersister.php';
  19. * ob_start(array('HTML_FormPersister', 'ob_formpersisterhandler'));
  20. * ? > <!-- please remove space after "?" while testing -->
  21. * <form>
  22. * <input type="text" name="simple" default="Enter your name">
  23. * <input type="text" name="second[a][b]" default="Something">
  24. * <select name="sel">
  25. * <option value="1">first</option>
  26. * <option value="2">second</option>
  27. * </select>
  28. * <input type="submit">
  29. * </form>
  30. *
  31. * Clicking the submit button, you see that values of text fields and
  32. * selected element in list remain unchanged - the same as you entered before
  33. * submitting the form!
  34. *
  35. * The same method also works with <select multiple>, checkboxes etc. You do
  36. * not need anymore to write "value=..." or "if (...) echo "selected"
  37. * manually in your scripts, nor use dynamic form-field generators confusing
  38. * your HTML designer. Everything is done automatically based on $_GET and
  39. * $_POST arrays.
  40. *
  41. * Form fields parser is based on fast HTML_SemiParser library, which
  42. * performes incomplete HTML parsing searching for only needed tags. On most
  43. * sites (especially XHTML) it is fully acceptable. Parser is fast: if
  44. * there are no one form elements in the page, it returns immediately, don't
  45. * ever think about overhead costs of parsing.
  46. *
  47. * @author Dmitry Koterov
  48. * @version 1.108
  49. * @package HTML
  50. */
  51. require_once 'HTML/SemiParser.php';
  52. class HTML_FormPersister extends HTML_SemiParser
  53. {
  54. /**
  55. * Constructor. Create new FormPersister instance.
  56. */
  57. function HTML_FormPersister()
  58. {
  59. $this->HTML_SemiParser();
  60. }
  61. /**
  62. * Process HTML text.
  63. *
  64. * @param string $st Input HTML text.
  65. * @return HTML text with all substitutions.
  66. */
  67. function process($st)
  68. {
  69. $this->fp_autoindexes = array();
  70. return HTML_SemiParser::process($st);
  71. }
  72. /**
  73. * Static handler for ob_start().
  74. *
  75. * Usage:
  76. * ob_start(array('HTML_FormPersister', 'ob_formpersisterhandler'));
  77. *
  78. * Of course you may not use OB handling but call process() manually
  79. * in your scripts.
  80. *
  81. * @param string $html Input HTML text.
  82. * @return processed output with all form fields modified.
  83. */
  84. function ob_formPersisterHandler($st)
  85. {
  86. $fp =& new HTML_FormPersister();
  87. $r = $fp->process($st);
  88. return $r;
  89. }
  90. /**
  91. * Tag and container callback handlers.
  92. * See usage of HTML_SemiParser.
  93. */
  94. /**
  95. * <FORM> tag handler (add default action attribute).
  96. * See HTML_SemiParser.
  97. */
  98. function tag_form($attr)
  99. {
  100. if (isset($attr['action'])) return;
  101. if (strtolower(@$attr['method']) == 'get') {
  102. $attr['action'] = preg_replace('/\?.*/s', '', $_SERVER['REQUEST_URI']);
  103. } else {
  104. $attr['action'] = $_SERVER['REQUEST_URI'];
  105. }
  106. return $attr;
  107. }
  108. /**
  109. * <INPUT> tag handler.
  110. * See HTML_SemiParser.
  111. */
  112. function tag_input($attr)
  113. {
  114. static $uid = 0;
  115. $orig_attr = $attr;
  116. switch ($type = @strtolower($attr['type'])) {
  117. case 'text': case 'password': case 'hidden': case '':
  118. if (!isset($attr['name'])) return;
  119. if (!isset($attr['value']))
  120. $attr['value'] = $this->getCurValue($attr);
  121. break;
  122. case 'radio':
  123. if (!isset($attr['name'])) return;
  124. if (isset($attr['checked']) || !isset($attr['value'])) return;
  125. if ($attr['value'] == $this->getCurValue($attr)) $attr['checked'] = 'checked';
  126. else unSet($attr['checked']);
  127. break;
  128. case 'checkbox':
  129. if (!isset($attr['name'])) return;
  130. if (isset($attr['checked'])) return;
  131. if (!isset($attr['value'])) $attr['value'] = 'on';
  132. if ($this->getCurValue($attr, true)) $attr['checked'] = 'checked';
  133. break;
  134. case 'image':
  135. case 'submit':
  136. if (isset($attr['confirm'])) {
  137. $attr['onclick'] = 'return confirm("' . $attr['confirm'] . '")';
  138. unSet($attr['confirm']);
  139. }
  140. break;
  141. default:
  142. return;
  143. }
  144. // Handle label pseudo-attribute. Button is placed RIGHTER
  145. // than the text if label text ends with "^". Example:
  146. // <input type=checkbox label="hello"> ==> [x]hello
  147. // <input type=checkbox label="hello^"> ==> hello[x]
  148. if (isset($attr['label'])) {
  149. $text = $attr['label'];
  150. if (!isset($attr['id'])) $attr['id'] = 'FPlab' . ($uid++);
  151. $right = 1;
  152. if ($text[strlen($text)-1] == '^') {
  153. $right = 0;
  154. $text = substr($text, 0, -1);
  155. }
  156. unSet($attr['label']);
  157. $attr[$right? '_right' : '_left'] = '<label for="'.$this->quoteHandler($attr['id']).'">' . $text . '</label>';
  158. }
  159. // We CANNOT return $orig_attr['_orig'] if attributes are not modified,
  160. // because we know nothing about following handlers. They may need
  161. // the parsed attributes, not a plain text.
  162. unset($attr['default']);
  163. return $attr;
  164. }
  165. /**
  166. * <TEXTAREA> tag handler.
  167. * See HTML_SemiParser.
  168. */
  169. function container_textarea($attr)
  170. {
  171. if (trim($attr['_text']) == '') {
  172. $attr['_text'] = $this->quoteHandler($this->getCurValue($attr));
  173. }
  174. unset($attr['default']);
  175. return $attr;
  176. }
  177. /**
  178. * <SELECT> tag handler.
  179. * See HTML_SemiParser.
  180. */
  181. function container_select($attr)
  182. {
  183. if (!isset($attr['name'])) return;
  184. // Multiple lists MUST contain [] in the name.
  185. if (isset($attr['multiple']) && strpos($attr['name'], '[]') === false) {
  186. $attr['name'] .= '[]';
  187. }
  188. $curVal = $this->getCurValue($attr);
  189. $body = "";
  190. // Get some options from variable?
  191. // All the text outside <option>...</option> container are treated as variable name.
  192. // E.g.: <select...> <option>...</option> ... some[global][options] ... <option>...</option> ... </select>
  193. $attr['_text'] = preg_replace_callback('{
  194. (
  195. (?:^ | </option> | </optgroup> | <optgroup[^>]*>)
  196. \s*
  197. )
  198. \$?
  199. ( [^<>\s]+ ) # variable name
  200. (?=
  201. \s*
  202. (?:$ | <option[\s>] | <optgroup[\s>] | </optgroup>)
  203. )
  204. }six',
  205. array(&$this, '_optionsFromVar_callback'),
  206. $attr['_text']
  207. );
  208. // Parse options, fetch its values and save them to array.
  209. // Also determine if we have at least one selected option.
  210. $body = $attr['_text'];
  211. $parts = preg_split("/<option\s*({$this->sp_reTagIn})>/si", $body, -1, PREG_SPLIT_DELIM_CAPTURE);
  212. $hasSelected = 0;
  213. for ($i = 1, $n = count($parts); $i < $n; $i += 2) {
  214. $opt = array();
  215. $this->parseAttrib($parts[$i], $opt);
  216. if (isset($opt['value'])) {
  217. $value = $opt['value'];
  218. } else {
  219. // Option without value: spaces are shrinked (experimented on IE).
  220. $text = preg_replace('{</?(option|optgroup)[^>]*>.*}si', '', $parts[$i + 1]);
  221. $value = trim($text);
  222. $value = preg_replace('/\s\s+/', ' ', $value);
  223. if (strpos($value, '&') !== false) {
  224. $value = strtr($value, $this->trans);
  225. }
  226. }
  227. if (isset($opt['selected'])) $hasSelected++;
  228. $parts[$i] = array($opt, $value);
  229. }
  230. // Modify options list - add selected attribute if needed, but ONLY
  231. // if we do not already have selected options!
  232. if (!$hasSelected) {
  233. foreach ($parts as $i=>$parsed) {
  234. if (!is_array($parsed)) continue;
  235. list ($opt, $value) = $parsed;
  236. if (isset($attr['multiple'])) {
  237. // Inherit some <select> attributes.
  238. if ($this->getCurValue($opt + $attr + array('value'=>$value), true)) { // merge
  239. $opt['selected'] = 'selected';
  240. }
  241. } else {
  242. if ($curVal == $value) {
  243. $opt['selected'] = 'selected';
  244. }
  245. }
  246. $opt['_tagName'] = 'option';
  247. $parts[$i] = $this->makeTag($opt);
  248. }
  249. $body = join('', $parts);
  250. }
  251. $attr['_text'] = $body;
  252. unset($attr['default']);
  253. return $attr;
  254. }
  255. /**
  256. * Other methods.
  257. */
  258. /**
  259. * Create set of <option> tags from array.
  260. */
  261. function makeOptions($options, $curId = false)
  262. {
  263. $body = '';
  264. foreach ($options as $k=>$text) {
  265. if (is_array($text)) {
  266. // option group
  267. $options = '';
  268. foreach ($text as $ko=>$v) {
  269. $opt = array('_tagName'=>'option', 'value'=>$ko, '_text'=>$this->quoteHandler(strval($v)));
  270. if ($curId !== false && strval($curId) === strval($ko)) {
  271. $opt['selected'] = "selected";
  272. }
  273. $options .= HTML_SemiParser::makeTag($opt);
  274. }
  275. $grp = array('_tagName'=>'optgroup', 'label'=>$k, '_text'=>$options);
  276. $body .= HTML_SemiParser::makeTag($grp);
  277. } else {
  278. // single option
  279. $opt = array('_tagName'=>'option', 'value'=>$k, '_text'=>$this->quoteHandler($text));
  280. if ($curId !== false && strval($curId) === strval($k)) {
  281. $opt['selected'] = "selected";
  282. }
  283. $body .= HTML_SemiParser::makeTag($opt);
  284. }
  285. }
  286. return $body;
  287. }
  288. /**
  289. * Value extractor.
  290. *
  291. * Try to find corresponding entry in $_POST, $_GET etc. for tag
  292. * with name attribute $attr['name']. Support complex form names
  293. * like 'fiels[one][two]', 'field[]' etc.
  294. *
  295. * If $isBoolean is set, always return true or false. Used for
  296. * checkboxes and multiple selects (names usually trailed with "[]",
  297. * but may not be trailed too).
  298. *
  299. * @return Current "value" of specified tag.
  300. */
  301. function getCurValue($attr, $isBoolean = false)
  302. {
  303. $name = @$attr['name'];
  304. if ($name === null) return null;
  305. $isArrayLike = false; // boolean AND contain [] in the name
  306. // Handle boolean fields.
  307. if ($isBoolean && false !== ($p = strpos($name, '[]'))) {
  308. $isArrayLike = true;
  309. $name = substr($name, 0, $p) . substr($name, $p + 2);
  310. }
  311. // Search for value in ALL arrays,
  312. // EXCEPT $_REQUEST, because it also holds Cookies!
  313. $fromForm = true;
  314. if (false !== ($v = $this->_deepFetch($_POST, $name, $this->fp_autoindexes[$name]))) $value = $v;
  315. elseif (false !== ($v = $this->_deepFetch($_GET, $name, $this->fp_autoindexes[$name]))) $value = $v;
  316. elseif (isset($attr['default'])) {
  317. $value = $attr['default'];
  318. if ($isBoolean) return $value !== '' && $value !== "0";
  319. // For array fields it is possible to enumerate all the
  320. // values in SCALAR using ';'.
  321. if ($isArrayLike && !is_array($value)) $value = explode(';', $value);
  322. $fromForm = false;
  323. } else {
  324. $value = '';
  325. }
  326. if ($fromForm) {
  327. // Remove slashes on stupid magic_quotes_gpc mode.
  328. // TODO: handle nested arrays too!
  329. if (is_scalar($value) && get_magic_quotes_gpc()) {
  330. $value = stripslashes($value);
  331. }
  332. }
  333. // Return value depending on field type.
  334. $attrValue = strval(isset($attr['value'])? $attr['value'] : 'on');
  335. if ($isArrayLike) {
  336. // Array-like field? If present, return true.
  337. if (!is_array($value)) return false;
  338. return in_array($attrValue, $value);
  339. } else {
  340. if ($isBoolean) {
  341. // Non-array boolean elements must be equal to values to match.
  342. return (bool)@strval($value) === (bool)$attrValue;
  343. } else {
  344. // This is not boolean nor array field. Return it now.
  345. return @strval($value);
  346. }
  347. }
  348. }
  349. /**
  350. * Fetch an element of $arr array using "complex" key $name.
  351. *
  352. * $name can be in form of "zzz[aaa][bbb]",
  353. * it means $arr[zzz][aaa][bbb].
  354. *
  355. * If $name contain auto-indexed parts (e.g. a[b][]), replace
  356. * it by corresponding indexes.
  357. *
  358. * $name may be scalar name or array (already splitted name,
  359. * see _splitMultiArray() method).
  360. *
  361. * @param array &$arr Array to fetch from.
  362. * @param mixed &$name Complex form-field name.
  363. * @param array &$autoindexes Container to hold auto-indexes
  364. * @return found value, or false if $name is not found.
  365. */
  366. function _deepFetch(&$arr, &$name, &$autoindexes) // static
  367. {
  368. if (is_scalar($name) && strpos($name, '[') === false) {
  369. // Fast fetch.
  370. return isset($arr[$name])? $arr[$name] : false;
  371. }
  372. // Else search into deep.
  373. $parts = HTML_FormPersister::_splitMultiArray($name);
  374. $leftPrefix = '';
  375. foreach ($parts as $i=>$k) {
  376. if (!strlen($k)) {
  377. // Perform auto-indexing.
  378. if (!isset($autoindexes[$leftPrefix])) $autoindexes[$leftPrefix] = 0;
  379. $parts[$i] = $k = $autoindexes[$leftPrefix]++;
  380. }
  381. if (!is_array($arr)) {
  382. // Current container is not array.
  383. return false;
  384. }
  385. if (!array_key_exists($k, $arr)) {
  386. // No such element.
  387. return false;
  388. }
  389. $arr = &$arr[$k];
  390. $leftPrefix = strlen($leftPrefix)? $leftPrefix . "[$k]" : $k;
  391. }
  392. if (!is_scalar($name)) {
  393. $name = $parts;
  394. } else {
  395. $name = $leftPrefix;
  396. }
  397. return $arr;
  398. }
  399. /**
  400. * Highly internal function. Must be re-written if some new
  401. * version of would support syntax like "zzz['aaa']['b\'b']" etc.
  402. * For "zzz[aaa][bbb]" returns array(zzz, aaa, bbb).
  403. */
  404. function _splitMultiArray($name) // static
  405. {
  406. if (is_array($name)) return $name;
  407. if (strpos($name, '[') === false) return array($name);
  408. $regs = null;
  409. preg_match_all('/ ( ^[^[]+ | \[ .*? \] ) (?= \[ | $) /xs', $name, $regs);
  410. $arr = array();
  411. foreach ($regs[0] as $s) {
  412. if ($s[0] == '[') $arr[] = substr($s, 1, -1);
  413. else $arr[] = $s;
  414. }
  415. return $arr;
  416. }
  417. /**
  418. * Callback function to replace variables in <select> body by set of options.
  419. */
  420. function _optionsFromVar_callback($p)
  421. {
  422. $dummy = array();
  423. $name = trim($p[2]);
  424. $options = $this->_deepFetch($GLOBALS, $name, $dummy);
  425. if ($options === null || $options === false) return $p[1] . "<option>???</option>";
  426. return $p[1] . $this->makeOptions($options);
  427. }
  428. }
  429. ?>