PageRenderTime 567ms CodeModel.GetById 34ms RepoModel.GetById 66ms app.codeStats 0ms

/berry/lib/html/formpersister.php

http://goodgirl.googlecode.com/
PHP | 450 lines | 256 code | 22 blank | 172 comment | 75 complexity | bf9e34b02524f4b2d7cd7b8c2009184b 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.102
  49. * @package HTML
  50. */
  51. //include '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 'submit':
  135. if (isset($attr['confirm'])) {
  136. $attr['onclick'] = 'return confirm("' . $attr['confirm'] . '")';
  137. unSet($attr['confirm']);
  138. }
  139. break;
  140. default:
  141. return;
  142. }
  143. // Handle label pseudo-attribute. Button is placed RIGHTER
  144. // than the text if label text ends with "^". Example:
  145. // <input type=checkbox label="hello"> ==> [x]hello
  146. // <input type=checkbox label="hello^"> ==> hello[x]
  147. if (isset($attr['label'])) {
  148. $text = $attr['label'];
  149. if (!isset($attr['id'])) $attr['id'] = 'FPlab' . ($uid++);
  150. $right = 1;
  151. if ($text[strlen($text)-1] == '^') {
  152. $right = 0;
  153. $text = substr($text, 0, -1);
  154. }
  155. unSet($attr['label']);
  156. $attr[$right? '_right' : '_left'] = '<label for="'.htmlspecialchars($attr['id']).'">' . $text . '</label>';
  157. }
  158. // We CANNOT return $orig_attr['_orig'] if attributes are not modified,
  159. // because we know nothing about following handlers. They may need
  160. // the parsed attributes, not a plain text.
  161. unset($attr['default']);
  162. return $attr;
  163. }
  164. /**
  165. * <TEXTAREA> tag handler.
  166. * See HTML_SemiParser.
  167. */
  168. function container_textarea($attr)
  169. {
  170. if (trim($attr['_text']) == '') {
  171. $attr['_text'] = htmlspecialchars($this->getCurValue($attr));
  172. }
  173. unset($attr['default']);
  174. return $attr;
  175. }
  176. /**
  177. * <SELECT> tag handler.
  178. * See HTML_SemiParser.
  179. */
  180. function container_select($attr)
  181. {
  182. if (!isset($attr['name'])) return;
  183. // Multiple lists MUST contain [] in the name.
  184. if (isset($attr['multiple']) && strpos($attr['name'], '[]') === false) {
  185. $attr['name'] .= '[]';
  186. }
  187. $curVal = $this->getCurValue($attr);
  188. $body = "";
  189. // Get some options from variable?
  190. // All the text outside <option>...</option> container are treated as variable name.
  191. // E.g.: <select...> <option>...</option> ... some[global][options] ... <option>...</option> ... </select>
  192. $attr['_text'] = preg_replace_callback('{
  193. (
  194. (?:^ | </option> | </optgroup> | <optgroup[^>]*>)
  195. \s*
  196. )
  197. \$?
  198. ( [^<>\s]+ ) # variable name
  199. (?=
  200. \s*
  201. (?:$ | <option[\s>] | <optgroup[\s>] | </optgroup>)
  202. )
  203. }six',
  204. array(&$this, '_optionsFromVar_callback'),
  205. $attr['_text']
  206. );
  207. // Parse options, fetch its values and save them to array.
  208. // Also determine if we have at least one selected option.
  209. $body = $attr['_text'];
  210. $parts = preg_split("/<option\s*({$this->sp_reTagIn})>/si", $body, -1, PREG_SPLIT_DELIM_CAPTURE);
  211. $hasSelected = 0;
  212. for ($i = 1, $n = count($parts); $i < $n; $i += 2) {
  213. $opt = array();
  214. $this->parseAttrib($parts[$i], $opt);
  215. if (isset($opt['value'])) {
  216. $value = $opt['value'];
  217. } else {
  218. // Option without value: spaces are shrinked (experimented on IE).
  219. $text = preg_replace('{</?(option|optgroup)[^>]*>.*}si', '', $parts[$i + 1]);
  220. $value = trim($text);
  221. $value = preg_replace('/\s\s+/', ' ', $value);
  222. if (strpos($value, '&') !== false) {
  223. $value = strtr($value, $this->trans);
  224. }
  225. }
  226. if (isset($opt['selected'])) $hasSelected++;
  227. $parts[$i] = array($opt, $value);
  228. }
  229. // Modify options list - add selected attribute if needed, but ONLY
  230. // if we do not already have selected options!
  231. if (!$hasSelected) {
  232. foreach ($parts as $i=>$parsed) {
  233. if (!is_array($parsed)) continue;
  234. list ($opt, $value) = $parsed;
  235. if (isset($attr['multiple'])) {
  236. // Inherit some <select> attributes.
  237. if ($this->getCurValue($opt + $attr + array('value'=>$value), true)) { // merge
  238. $opt['selected'] = 'selected';
  239. }
  240. } else {
  241. if ($curVal == $value) {
  242. $opt['selected'] = 'selected';
  243. }
  244. }
  245. $opt['_tagName'] = 'option';
  246. $parts[$i] = $this->makeTag($opt);
  247. }
  248. $body = join('', $parts);
  249. }
  250. $attr['_text'] = $body;
  251. unset($attr['default']);
  252. return $attr;
  253. }
  254. /**
  255. * Other methods.
  256. */
  257. /**
  258. * Create set of <option> tags from array.
  259. */
  260. function makeOptions($options, $curId = false)
  261. {
  262. $body = '';
  263. foreach ($options as $k=>$text) {
  264. if (is_array($text)) {
  265. // option group
  266. $options = '';
  267. foreach ($text as $ko=>$v) {
  268. $opt = array('_tagName'=>'option', 'value'=>$ko, '_text'=>htmlspecialchars(strval($v)));
  269. if ($curId !== false && strval($curId) === strval($ko)) {
  270. $opt['selected'] = "selected";
  271. }
  272. $options .= HTML_SemiParser::makeTag($opt);
  273. }
  274. $grp = array('_tagName'=>'optgroup', 'label'=>$k, '_text'=>$options);
  275. $body .= HTML_SemiParser::makeTag($grp);
  276. } else {
  277. // single option
  278. $opt = array('_tagName'=>'option', 'value'=>$k, '_text'=>$text);
  279. if ($curId !== false && strval($curId) === strval($k)) {
  280. $opt['selected'] = "selected";
  281. }
  282. $body .= HTML_SemiParser::makeTag($opt);
  283. }
  284. }
  285. return $body;
  286. }
  287. /**
  288. * Value extractor.
  289. *
  290. * Try to find corresponding entry in $_POST, $_GET etc. for tag
  291. * with name attribute $attr['name']. Support complex form names
  292. * like 'fiels[one][two]', 'field[]' etc.
  293. *
  294. * If $isBoolean is set, always return true or false. Used for
  295. * checkboxes and multiple selects (names usually trailed with "[]",
  296. * but may not be trailed too).
  297. *
  298. * @return Current "value" of specified tag.
  299. */
  300. function getCurValue($attr, $isBoolean = false)
  301. {
  302. $name = $attr['name'];
  303. $isArrayLike = false; // boolean AND contain [] in the name
  304. // Handle boolean fields.
  305. if ($isBoolean && false !== ($p = strpos($name, '[]'))) {
  306. $isArrayLike = true;
  307. $name = substr($name, 0, $p) . substr($name, $p + 2);
  308. }
  309. // Search for value in ALL arrays,
  310. // EXCEPT $_REQUEST, because it also holds Cookies!
  311. $fromForm = true;
  312. if (false !== ($v = $this->_deepFetch($_POST, $name, $this->fp_autoindexes[$name]))) $value = $v;
  313. elseif (false !== ($v = $this->_deepFetch($_GET, $name, $this->fp_autoindexes[$name]))) $value = $v;
  314. elseif (isset($attr['default'])) {
  315. $value = $attr['default'];
  316. if ($isBoolean) return $value !== '' && $value !== "0";
  317. // For array fields it is possible to enumerate all the
  318. // values in SCALAR using ';'.
  319. if ($isArrayLike && !is_array($value)) $value = explode(';', $value);
  320. $fromForm = false;
  321. } else {
  322. $value = '';
  323. }
  324. if ($fromForm) {
  325. // Remove slashes on stupid magic_quotes_gpc mode.
  326. // TODO: handle nested arrays too!
  327. if (is_scalar($value) && ini_get('magic_quotes_gpc')) {
  328. $value = stripslashes($value);
  329. }
  330. }
  331. // Return value depending on field type.
  332. $attrValue = strval(isset($attr['value'])? $attr['value'] : 'on');
  333. if ($isArrayLike) {
  334. // Array-like field? If present, return true.
  335. if (!is_array($value)) return false;
  336. return in_array($attrValue, $value);
  337. } else {
  338. if ($isBoolean) {
  339. // Non-array boolean elements must be equal to values to match.
  340. return (bool)@strval($value) === (bool)$attrValue;
  341. } else {
  342. // This is not boolean nor array field. Return it now.
  343. return @strval($value);
  344. }
  345. }
  346. }
  347. /**
  348. * Fetch an element of $arr array using "complex" key $name.
  349. *
  350. * $name can be in form of "zzz[aaa][bbb]",
  351. * it means $arr[zzz][aaa][bbb].
  352. *
  353. * If $name contain auto-indexed parts (e.g. a[b][]), replace
  354. * it by corresponding indexes.
  355. *
  356. * $name may be scalar name or array (already splitted name,
  357. * see _splitMultiArray() method).
  358. *
  359. * @param array &$arr Array to fetch from.
  360. * @param mixed &$name Complex form-field name.
  361. * @param array &$autoindexes Container to hold auto-indexes
  362. * @return found value, or false if $name is not found.
  363. */
  364. function _deepFetch(&$arr, &$name, &$autoindexes) // static
  365. {
  366. if (is_scalar($name) && strpos($name, '[') === false) {
  367. // Fast fetch.
  368. return isset($arr[$name])? $arr[$name] : false;
  369. }
  370. // Else search into deep.
  371. $parts = HTML_FormPersister::_splitMultiArray($name);
  372. $leftPrefix = '';
  373. foreach ($parts as $i=>$k) {
  374. if (!strlen($k)) {
  375. // Perform auto-indexing.
  376. if (!isset($autoindexes[$leftPrefix])) $autoindexes[$leftPrefix] = 0;
  377. $parts[$i] = $k = $autoindexes[$leftPrefix]++;
  378. }
  379. if (!is_array($arr)) {
  380. // Current container is not array.
  381. return false;
  382. }
  383. if (!array_key_exists($k, $arr)) {
  384. // No such element.
  385. return false;
  386. }
  387. $arr = &$arr[$k];
  388. $leftPrefix = strlen($leftPrefix)? $leftPrefix . "[$k]" : $k;
  389. }
  390. if (!is_scalar($name)) {
  391. $name = $parts;
  392. } else {
  393. $name = $leftPrefix;
  394. }
  395. return $arr;
  396. }
  397. /**
  398. * Highly internal function. Must be re-written if some new
  399. * version of would support syntax like "zzz['aaa']['b\'b']" etc.
  400. * For "zzz[aaa][bbb]" returns array(zzz, aaa, bbb).
  401. */
  402. function _splitMultiArray($name) // static
  403. {
  404. if (is_array($name)) return $name;
  405. if (strpos($name, '[') === false) return array($name);
  406. $regs = null;
  407. preg_match_all('/ ( ^[^[]+ | \[ .*? \] ) (?= \[ | $) /xs', $name, $regs);
  408. $arr = array();
  409. foreach ($regs[0] as $s) {
  410. if ($s[0] == '[') $arr[] = substr($s, 1, -1);
  411. else $arr[] = $s;
  412. }
  413. return $arr;
  414. }
  415. /**
  416. * Callback function to replace variables in <select> body by set of options.
  417. */
  418. function _optionsFromVar_callback($p)
  419. {
  420. $dummy = array();
  421. $name = trim($p[2]);
  422. $options = $this->_deepFetch($GLOBALS, $name, $dummy);
  423. if ($options === null || $options === false) return $p[1] . "<option>???</option>";
  424. return $p[1] . $this->makeOptions($options);
  425. }
  426. }
  427. ?>