formdom.class.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. <?php
  2. /**
  3. * The formdom class file of ZenTaoPMS.
  4. *
  5. * @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
  6. * @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
  7. * @author Zhu Jinyong<zhujinyong@easycorp.ltd>
  8. * @package form
  9. * @link https://www.zentao.net
  10. */
  11. /**
  12. * 解析form dom为数组数据。
  13. * Parse Form dom to array data.
  14. */
  15. class formdom
  16. {
  17. /**
  18. * Errors
  19. *
  20. * @var array
  21. * @access private
  22. */
  23. private $errors = array();
  24. /**
  25. * Options
  26. *
  27. * @var array
  28. * @access private
  29. */
  30. private $options = array(
  31. 'include_disabled' => false,
  32. 'include_empty_checkboxes' => false,
  33. 'default_radio_value' => null,
  34. 'debug' => false
  35. );
  36. /**
  37. * Constructor
  38. *
  39. * @param array $options
  40. * @access public
  41. * @return void
  42. */
  43. public function __construct($options = array())
  44. {
  45. $this->options = array_merge($this->options, $options);
  46. }
  47. /**
  48. * Parse html dom
  49. *
  50. * @param string $html
  51. * @param string $formSelector
  52. * @access public
  53. * @return array
  54. */
  55. public function parse($html, $formSelector = null)
  56. {
  57. $this->errors = array();
  58. if(empty($html))
  59. {
  60. $this->errors[] = "HTML is empty";
  61. return array();
  62. }
  63. $dom = new DOMDocument();
  64. libxml_use_internal_errors(true);
  65. @$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
  66. libxml_clear_errors();
  67. $xpath = new DOMXPath($dom);
  68. /* 查找表单 */
  69. $form = null;
  70. if($formSelector)
  71. {
  72. // 通过 id 查找
  73. $forms = $xpath->query("//form[@id='$formSelector']");
  74. if($forms->length > 0)
  75. {
  76. $form = $forms->item(0);
  77. }
  78. else
  79. {
  80. /* 通过 name 查找 */
  81. $forms = $xpath->query("//form[@name='$formSelector']");
  82. if($forms->length > 0)
  83. {
  84. $form = $forms->item(0);
  85. }
  86. }
  87. } else {
  88. /* 获取第一个表单 */
  89. $forms = $xpath->query("//form");
  90. if ($forms->length > 0) {
  91. $form = $forms->item(0);
  92. }
  93. }
  94. if(!$form)
  95. {
  96. $this->errors[] = "Form not found";
  97. return array();
  98. }
  99. /* 提取表单数据 */
  100. $result = $this->extractFormData($xpath, $form);
  101. return $result;
  102. }
  103. /**
  104. * Extract form data
  105. *
  106. * @param object $xpath
  107. * @param object $form
  108. * @access private
  109. * @return array
  110. */
  111. private function extractFormData($xpath, $form)
  112. {
  113. $data = [];
  114. /* 处理所有表单元素 */
  115. $this->processInputs($xpath, $form, $data);
  116. $this->processSelects($xpath, $form, $data);
  117. $this->processTextareas($xpath, $form, $data);
  118. $this->processZinPickers($xpath, $form, $data);
  119. $this->processZinEditors($xpath, $form, $data);
  120. return $data;
  121. }
  122. /**
  123. * Process inputs
  124. *
  125. * @param object $xpath
  126. * @param object $form
  127. * @param array $data
  128. * @access private
  129. * @return void
  130. */
  131. private function processInputs($xpath, $form, &$data)
  132. {
  133. // 查找所有 input 元素(包括嵌套的)
  134. $inputs = $xpath->query(".//input[@name]", $form);
  135. $radioGroups = array();
  136. foreach($inputs as $input)
  137. {
  138. $name = $input->getAttribute('name');
  139. $type = strtolower($input->getAttribute('type') ?: 'text');
  140. $value = $input->getAttribute('value');
  141. $disabled = $input->hasAttribute('disabled');
  142. /* 跳过禁用的元素(除非配置允许) */
  143. if(!$this->options['include_disabled'] && $disabled) continue;
  144. switch($type)
  145. {
  146. case 'checkbox':
  147. if($input->hasAttribute('checked'))
  148. {
  149. $this->addValue($data, $name, $value ?: 'on');
  150. }
  151. elseif($this->options['include_empty_checkboxes'])
  152. {
  153. $this->addValue($data, $name, '');
  154. }
  155. break;
  156. case 'radio':
  157. $baseName = $this->getBaseName($name);
  158. if(!isset($radioGroups[$baseName]))
  159. {
  160. $radioGroups[$baseName] = array(
  161. 'selected' => null,
  162. 'hasSelection' => false
  163. );
  164. }
  165. if($input->hasAttribute('checked'))
  166. {
  167. $radioGroups[$baseName]['selected'] = $value;
  168. $radioGroups[$baseName]['hasSelection'] = true;
  169. }
  170. break;
  171. case 'file':
  172. $this->addValue($data, $name, '');
  173. break;
  174. case 'submit':
  175. case 'button':
  176. case 'image':
  177. case 'reset':
  178. break;
  179. default:
  180. /* 包括 hidden, text, password, email 等所有其他类型 */
  181. $this->addValue($data, $name, $value ?: '');
  182. break;
  183. }
  184. }
  185. /* 处理 radio 组 */
  186. foreach ($radioGroups as $name => $group) {
  187. if ($group['hasSelection']) {
  188. $data[$name] = $group['selected'];
  189. } elseif ($this->options['default_radio_value'] !== null) {
  190. $data[$name] = $this->options['default_radio_value'];
  191. }
  192. }
  193. }
  194. /**
  195. * Process selects
  196. *
  197. * @param object $xpath
  198. * @param object $form
  199. * @param array $data
  200. * @access private
  201. * @return void
  202. */
  203. private function processSelects($xpath, $form, &$data)
  204. {
  205. $selects = $xpath->query(".//select[@name]", $form);
  206. foreach($selects as $select)
  207. {
  208. if(!$this->options['include_disabled'] && $select->hasAttribute('disabled')) continue;
  209. $name = $select->getAttribute('name');
  210. $multiple = $select->hasAttribute('multiple');
  211. $options = $xpath->query(".//option", $select);
  212. if($multiple)
  213. {
  214. $selectedValues = [];
  215. foreach($options as $option)
  216. {
  217. if($option->hasAttribute('selected'))
  218. {
  219. $value = $option->hasAttribute('value') ? $option->getAttribute('value') : trim($option->textContent);
  220. $selectedValues[] = $value;
  221. }
  222. }
  223. /* 对于多选,即使没有选中项,也要处理默认值 */
  224. foreach($options as $option)
  225. {
  226. $value = $option->hasAttribute('value') ? $option->getAttribute('value') : trim($option->textContent);
  227. if(!empty($value))
  228. {
  229. $selectedValues[] = $value;
  230. break; // 只取第一个有值的选项作为默认
  231. }
  232. }
  233. if(!empty($selectedValues))
  234. {
  235. foreach($selectedValues as $val) $this->addValue($data, $name, $val);
  236. }
  237. }
  238. else
  239. {
  240. $selectedValue = null;
  241. $firstValue = null;
  242. foreach($options as $index => $option)
  243. {
  244. $value = $option->hasAttribute('value')
  245. ? $option->getAttribute('value')
  246. : trim($option->textContent);
  247. if($index === 0) $firstValue = $value;
  248. if($option->hasAttribute('selected'))
  249. {
  250. $selectedValue = $value;
  251. break;
  252. }
  253. }
  254. $finalValue = $selectedValue !== null ? $selectedValue : $firstValue;
  255. if($finalValue !== null) $this->addValue($data, $name, $finalValue);
  256. }
  257. }
  258. }
  259. /**
  260. * Process textarea
  261. *
  262. * @param object $xpath
  263. * @param object $form
  264. * @param array $data
  265. * @access private
  266. * @return void
  267. */
  268. private function processTextareas($xpath, $form, &$data)
  269. {
  270. $textareas = $xpath->query(".//textarea[@name]", $form);
  271. foreach($textareas as $textarea)
  272. {
  273. if(!$this->options['include_disabled'] && $textarea->hasAttribute('disabled')) continue;
  274. $name = $textarea->getAttribute('name');
  275. $value = $textarea->textContent;
  276. $this->addValue($data, $name, $value);
  277. }
  278. }
  279. /**
  280. * Process zin pickers and date pickers.
  281. *
  282. * @param object $xpath
  283. * @param object $form
  284. * @param array $data
  285. * @access private
  286. * @return void
  287. */
  288. private function processZinPickers($xpath, $form, &$data)
  289. {
  290. /* 查找所有包含 zui-create-picker 属性的元素(通常是 div.picker-box) */
  291. $pickers = $xpath->query(".//*[@zui-create]", $form);
  292. foreach ($pickers as $picker) {
  293. /* 1. 跳过禁用的picker(如果配置不允许) */
  294. $pickerDisabled = $picker->hasAttribute('disabled') || $picker->getAttribute('disabled') === 'true';
  295. if(!$this->options['include_disabled'] && $pickerDisabled) continue;
  296. /* 2. 获取 zui-create-picker 属性的JSON值 */
  297. if($picker->hasAttribute('zui-create-picker'))
  298. {
  299. $pickerConfig = $picker->getAttribute('zui-create-picker');
  300. }
  301. elseif($picker->hasAttribute('zui-create-datepicker'))
  302. {
  303. $pickerConfig = $picker->getAttribute('zui-create-datepicker');
  304. }
  305. if(empty($pickerConfig)) continue;
  306. /* 3. 提取name(关键:用正则匹配name字段)*/
  307. $name = null;
  308. if(preg_match('/"name"\s*:\s*"([^"]+)"/i', $pickerConfig, $nameMatches))
  309. {
  310. $name = $nameMatches[1]; // 捕获引号中的name值
  311. }
  312. elseif(preg_match("/'name'\s*:\s*'([^']+)'/i", $pickerConfig, $nameMatches))
  313. {
  314. $name = $nameMatches[1]; // 兼容单引号
  315. }
  316. if(empty($name)) continue;
  317. /* 4. 提取defaultValue(核心:兼容JS函数和非JSON格式)*/
  318. $defaultValue = '';
  319. /* 正则规则:匹配 "defaultValue": 值 或 'defaultValue': 值 */
  320. /* 支持的值类型:字符串(单/双引号)、数字、true/false/null */
  321. $valuePattern = '/"defaultValue"\s*:\s*(?:"([^"]+)"|\'([^\']+)\'|(\d+\.?\d*)|(true|false|null)|\[([^\]]+)\])/i';
  322. if(preg_match($valuePattern, $pickerConfig, $valueMatches))
  323. {
  324. /* 匹配优先级:数组 > 双引号字符串 > 单引号字符串 > 数字 > 布尔/null */
  325. $defaultValue = !empty($valueMatches[5]) ? $valueMatches[5] // 优先级1:数组内容
  326. : (!empty($valueMatches[1]) ? $valueMatches[1] // 优先级2:双引号字符串
  327. : (!empty($valueMatches[2]) ? $valueMatches[2] // 优先级3:单引号字符串
  328. : (!empty($valueMatches[3]) || $valueMatches[3] === '0' ? $valueMatches[3] // 优先级4:数字
  329. : $valueMatches[4] ?? null))); // 优先级5:布尔/null
  330. /* 转换类型(如"true"→true,"123"→123)*/
  331. if ($defaultValue === 'true') $defaultValue = true;
  332. elseif ($defaultValue === 'false') $defaultValue = false;
  333. elseif ($defaultValue === 'null') $defaultValue = null;
  334. elseif (is_numeric($defaultValue)) $defaultValue = (float)$defaultValue;
  335. }
  336. /* 5. 写入数据 */
  337. $this->addValue($data, $name, $defaultValue);
  338. }
  339. }
  340. /**
  341. * Process zin editors.
  342. *
  343. * @param object $xpath
  344. * @param object $form
  345. * @param array $data
  346. * @access private
  347. * @return void
  348. */
  349. private function processZinEditors($xpath, $form, &$data)
  350. {
  351. $editors = $xpath->query("//zen-editor", $form);
  352. foreach($editors as $editor)
  353. {
  354. $name = $editor->getAttribute('name');
  355. $value = '';
  356. $contentNode = $xpath->query(".//article[@slot='content']", $editor)->item(0);
  357. if($contentNode)
  358. {
  359. /* 获取内部HTML内容 */
  360. $dom = $contentNode->ownerDocument;
  361. foreach($contentNode->childNodes as $child) $value .= $dom->saveHTML($child);
  362. }
  363. $this->addValue($data, $name, $value);
  364. }
  365. }
  366. /**
  367. * Add value to data.
  368. *
  369. * @param array $data
  370. * @param string $name
  371. * @param mixed $value
  372. * @access private
  373. * @return void
  374. */
  375. private function addValue(&$data, $name, $value)
  376. {
  377. /* 使用 parse_str 来处理名称 */
  378. if($value === null) $value = '';
  379. parse_str($name . '=' . urlencode($value), $temp);
  380. /* 合并到数据中 */
  381. foreach($temp as $key => $val)
  382. {
  383. if(!isset($data[$key]))
  384. {
  385. $data[$key] = $val;
  386. }
  387. elseif(is_array($data[$key]) && is_array($val))
  388. {
  389. /* 递归合并数组 */
  390. foreach($val as $v)
  391. {
  392. if(!in_array($v, $data[$key])) $data[$key][] = $v;
  393. }
  394. }
  395. else
  396. {
  397. /* 覆盖(处理重复的非数组字段) */
  398. $data[$key] = $val;
  399. }
  400. }
  401. }
  402. /**
  403. * Get base name of field.
  404. *
  405. * @param string $name
  406. * @access private
  407. * @return string
  408. */
  409. private function getBaseName($name)
  410. {
  411. if(preg_match('/^([^\[]+)/', $name, $matches)) return $matches[1];
  412. return $name;
  413. }
  414. /**
  415. * Get errors.
  416. *
  417. * @access public
  418. * @return array
  419. */
  420. public function getErrors()
  421. {
  422. return $this->errors;
  423. }
  424. }