478 lines
15 KiB
PHP
478 lines
15 KiB
PHP
<?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;
|
||
}
|
||
}
|