znt/lib/formdom/formdom.class.php

478 lines
15 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* The formdom class file of ZenTaoPMS.
*
* @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
* @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
* @author Zhu Jinyong<zhujinyong@easycorp.ltd>
* @package form
* @link https://www.zentao.net
*/
/**
* 解析form dom为数组数据。
* Parse Form dom to array data.
*/
class formdom
{
/**
* Errors
*
* @var array
* @access private
*/
private $errors = array();
/**
* Options
*
* @var array
* @access private
*/
private $options = array(
'include_disabled' => false,
'include_empty_checkboxes' => false,
'default_radio_value' => null,
'debug' => false
);
/**
* Constructor
*
* @param array $options
* @access public
* @return void
*/
public function __construct($options = array())
{
$this->options = array_merge($this->options, $options);
}
/**
* Parse html dom
*
* @param string $html
* @param string $formSelector
* @access public
* @return array
*/
public function parse($html, $formSelector = null)
{
$this->errors = array();
if(empty($html))
{
$this->errors[] = "HTML is empty";
return array();
}
$dom = new DOMDocument();
libxml_use_internal_errors(true);
@$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
libxml_clear_errors();
$xpath = new DOMXPath($dom);
/* 查找表单 */
$form = null;
if($formSelector)
{
// 通过 id 查找
$forms = $xpath->query("//form[@id='$formSelector']");
if($forms->length > 0)
{
$form = $forms->item(0);
}
else
{
/* 通过 name 查找 */
$forms = $xpath->query("//form[@name='$formSelector']");
if($forms->length > 0)
{
$form = $forms->item(0);
}
}
} else {
/* 获取第一个表单 */
$forms = $xpath->query("//form");
if ($forms->length > 0) {
$form = $forms->item(0);
}
}
if(!$form)
{
$this->errors[] = "Form not found";
return array();
}
/* 提取表单数据 */
$result = $this->extractFormData($xpath, $form);
return $result;
}
/**
* Extract form data
*
* @param object $xpath
* @param object $form
* @access private
* @return array
*/
private function extractFormData($xpath, $form)
{
$data = [];
/* 处理所有表单元素 */
$this->processInputs($xpath, $form, $data);
$this->processSelects($xpath, $form, $data);
$this->processTextareas($xpath, $form, $data);
$this->processZinPickers($xpath, $form, $data);
$this->processZinEditors($xpath, $form, $data);
return $data;
}
/**
* Process inputs
*
* @param object $xpath
* @param object $form
* @param array $data
* @access private
* @return void
*/
private function processInputs($xpath, $form, &$data)
{
// 查找所有 input 元素(包括嵌套的)
$inputs = $xpath->query(".//input[@name]", $form);
$radioGroups = array();
foreach($inputs as $input)
{
$name = $input->getAttribute('name');
$type = strtolower($input->getAttribute('type') ?: 'text');
$value = $input->getAttribute('value');
$disabled = $input->hasAttribute('disabled');
/* 跳过禁用的元素(除非配置允许) */
if(!$this->options['include_disabled'] && $disabled) continue;
switch($type)
{
case 'checkbox':
if($input->hasAttribute('checked'))
{
$this->addValue($data, $name, $value ?: 'on');
}
elseif($this->options['include_empty_checkboxes'])
{
$this->addValue($data, $name, '');
}
break;
case 'radio':
$baseName = $this->getBaseName($name);
if(!isset($radioGroups[$baseName]))
{
$radioGroups[$baseName] = array(
'selected' => null,
'hasSelection' => false
);
}
if($input->hasAttribute('checked'))
{
$radioGroups[$baseName]['selected'] = $value;
$radioGroups[$baseName]['hasSelection'] = true;
}
break;
case 'file':
$this->addValue($data, $name, '');
break;
case 'submit':
case 'button':
case 'image':
case 'reset':
break;
default:
/* 包括 hidden, text, password, email 等所有其他类型 */
$this->addValue($data, $name, $value ?: '');
break;
}
}
/* 处理 radio 组 */
foreach ($radioGroups as $name => $group) {
if ($group['hasSelection']) {
$data[$name] = $group['selected'];
} elseif ($this->options['default_radio_value'] !== null) {
$data[$name] = $this->options['default_radio_value'];
}
}
}
/**
* Process selects
*
* @param object $xpath
* @param object $form
* @param array $data
* @access private
* @return void
*/
private function processSelects($xpath, $form, &$data)
{
$selects = $xpath->query(".//select[@name]", $form);
foreach($selects as $select)
{
if(!$this->options['include_disabled'] && $select->hasAttribute('disabled')) continue;
$name = $select->getAttribute('name');
$multiple = $select->hasAttribute('multiple');
$options = $xpath->query(".//option", $select);
if($multiple)
{
$selectedValues = [];
foreach($options as $option)
{
if($option->hasAttribute('selected'))
{
$value = $option->hasAttribute('value') ? $option->getAttribute('value') : trim($option->textContent);
$selectedValues[] = $value;
}
}
/* 对于多选,即使没有选中项,也要处理默认值 */
foreach($options as $option)
{
$value = $option->hasAttribute('value') ? $option->getAttribute('value') : trim($option->textContent);
if(!empty($value))
{
$selectedValues[] = $value;
break; // 只取第一个有值的选项作为默认
}
}
if(!empty($selectedValues))
{
foreach($selectedValues as $val) $this->addValue($data, $name, $val);
}
}
else
{
$selectedValue = null;
$firstValue = null;
foreach($options as $index => $option)
{
$value = $option->hasAttribute('value')
? $option->getAttribute('value')
: trim($option->textContent);
if($index === 0) $firstValue = $value;
if($option->hasAttribute('selected'))
{
$selectedValue = $value;
break;
}
}
$finalValue = $selectedValue !== null ? $selectedValue : $firstValue;
if($finalValue !== null) $this->addValue($data, $name, $finalValue);
}
}
}
/**
* Process textarea
*
* @param object $xpath
* @param object $form
* @param array $data
* @access private
* @return void
*/
private function processTextareas($xpath, $form, &$data)
{
$textareas = $xpath->query(".//textarea[@name]", $form);
foreach($textareas as $textarea)
{
if(!$this->options['include_disabled'] && $textarea->hasAttribute('disabled')) continue;
$name = $textarea->getAttribute('name');
$value = $textarea->textContent;
$this->addValue($data, $name, $value);
}
}
/**
* Process zin pickers and date pickers.
*
* @param object $xpath
* @param object $form
* @param array $data
* @access private
* @return void
*/
private function processZinPickers($xpath, $form, &$data)
{
/* 查找所有包含 zui-create-picker 属性的元素(通常是 div.picker-box */
$pickers = $xpath->query(".//*[@zui-create]", $form);
foreach ($pickers as $picker) {
/* 1. 跳过禁用的picker如果配置不允许 */
$pickerDisabled = $picker->hasAttribute('disabled') || $picker->getAttribute('disabled') === 'true';
if(!$this->options['include_disabled'] && $pickerDisabled) continue;
/* 2. 获取 zui-create-picker 属性的JSON值 */
if($picker->hasAttribute('zui-create-picker'))
{
$pickerConfig = $picker->getAttribute('zui-create-picker');
}
elseif($picker->hasAttribute('zui-create-datepicker'))
{
$pickerConfig = $picker->getAttribute('zui-create-datepicker');
}
if(empty($pickerConfig)) continue;
/* 3. 提取name关键用正则匹配name字段*/
$name = null;
if(preg_match('/"name"\s*:\s*"([^"]+)"/i', $pickerConfig, $nameMatches))
{
$name = $nameMatches[1]; // 捕获引号中的name值
}
elseif(preg_match("/'name'\s*:\s*'([^']+)'/i", $pickerConfig, $nameMatches))
{
$name = $nameMatches[1]; // 兼容单引号
}
if(empty($name)) continue;
/* 4. 提取defaultValue核心兼容JS函数和非JSON格式*/
$defaultValue = '';
/* 正则规则:匹配 "defaultValue": 值 或 'defaultValue': 值 */
/* 支持的值类型:字符串(单/双引号、数字、true/false/null */
$valuePattern = '/"defaultValue"\s*:\s*(?:"([^"]+)"|\'([^\']+)\'|(\d+\.?\d*)|(true|false|null)|\[([^\]]+)\])/i';
if(preg_match($valuePattern, $pickerConfig, $valueMatches))
{
/* 匹配优先级:数组 > 双引号字符串 > 单引号字符串 > 数字 > 布尔/null */
$defaultValue = !empty($valueMatches[5]) ? $valueMatches[5] // 优先级1数组内容
: (!empty($valueMatches[1]) ? $valueMatches[1] // 优先级2双引号字符串
: (!empty($valueMatches[2]) ? $valueMatches[2] // 优先级3单引号字符串
: (!empty($valueMatches[3]) || $valueMatches[3] === '0' ? $valueMatches[3] // 优先级4数字
: $valueMatches[4] ?? null))); // 优先级5布尔/null
/* 转换类型(如"true"→true"123"→123*/
if ($defaultValue === 'true') $defaultValue = true;
elseif ($defaultValue === 'false') $defaultValue = false;
elseif ($defaultValue === 'null') $defaultValue = null;
elseif (is_numeric($defaultValue)) $defaultValue = (float)$defaultValue;
}
/* 5. 写入数据 */
$this->addValue($data, $name, $defaultValue);
}
}
/**
* Process zin editors.
*
* @param object $xpath
* @param object $form
* @param array $data
* @access private
* @return void
*/
private function processZinEditors($xpath, $form, &$data)
{
$editors = $xpath->query("//zen-editor", $form);
foreach($editors as $editor)
{
$name = $editor->getAttribute('name');
$value = '';
$contentNode = $xpath->query(".//article[@slot='content']", $editor)->item(0);
if($contentNode)
{
/* 获取内部HTML内容 */
$dom = $contentNode->ownerDocument;
foreach($contentNode->childNodes as $child) $value .= $dom->saveHTML($child);
}
$this->addValue($data, $name, $value);
}
}
/**
* Add value to data.
*
* @param array $data
* @param string $name
* @param mixed $value
* @access private
* @return void
*/
private function addValue(&$data, $name, $value)
{
/* 使用 parse_str 来处理名称 */
if($value === null) $value = '';
parse_str($name . '=' . urlencode($value), $temp);
/* 合并到数据中 */
foreach($temp as $key => $val)
{
if(!isset($data[$key]))
{
$data[$key] = $val;
}
elseif(is_array($data[$key]) && is_array($val))
{
/* 递归合并数组 */
foreach($val as $v)
{
if(!in_array($v, $data[$key])) $data[$key][] = $v;
}
}
else
{
/* 覆盖(处理重复的非数组字段) */
$data[$key] = $val;
}
}
}
/**
* Get base name of field.
*
* @param string $name
* @access private
* @return string
*/
private function getBaseName($name)
{
if(preg_match('/^([^\[]+)/', $name, $matches)) return $matches[1];
return $name;
}
/**
* Get errors.
*
* @access public
* @return array
*/
public function getErrors()
{
return $this->errors;
}
}