PageRenderTime 75ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/QFW/lib/HTML/FormPersister.php

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