HEX
Server: nginx/1.18.0
System: Linux iZj6c1ieg2jrpk1z5tzi19Z 6.3.9-1.el7.elrepo.x86_64 #1 SMP PREEMPT_DYNAMIC Wed Jun 21 22:18:40 EDT 2023 x86_64
User: www (1001)
PHP: 8.2.4
Disabled: passthru,exec,system,putenv,chroot,chgrp,chown,shell_exec,popen,proc_open,pcntl_exec,ini_alter,ini_restore,dl,openlog,syslog,readlink,symlink,popepassthru,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,imap_open,apache_setenv
Upload Files
File: /www/wwwroot/www.cytocare.cn/wp-content/plugins/wpjam-basic/includes/class-wpjam-field.php
<?php
class WPJAM_Attr extends WPJAM_Args{
	public function __toString(){
		return (string)$this->render();
	}

	public function jsonSerialize(){
		return $this->render();
	}

	public function attr($key, ...$args){
		if(!is_array($key)){
			if(!$args){
				return $this->$key;
			}

			$this->$key	= $args[0];
		}else{
			array_walk($key, fn($v, $k)=> $this->$k = $v);
		}

		return $this;
	}

	public function remove_attr($key){
		return $this->delete_arg($key);
	}

	public function val(...$args){
		return $this->attr('value', ...$args);
	}

	public function data(...$args){
		$data	= wpjam_array($this->data);

		if(!$args){
			return array_merge($data, wpjam_array($this->get_args(), fn($k)=> str_starts_with($k, 'data-') ? substr($k, 5) : null));
		}elseif(count($args) == 1 && !is_array($args[0])){
			$k	= 'data-'.$args[0];

			return $this->attr($k) ?? ($data[$k] ?? null);
		}else{
			return $this->attr(wpjam_array((is_array($args[0]) ? $args[0] : [$args[0]=>$args[1]]), fn($k)=> 'data-'.$k));
		}
	}

	protected function class($action='', ...$args){
		$value	= array_filter(wp_parse_list($this->class ?: []));
		$cb		= $action ? ['add'=>'array_merge', 'remove'=>'array_diff', 'toggle'=>'wpjam_toggle'][$action] : '';

		return $cb ? $this->attr('class', $cb($value, wp_parse_list($args[0] ?: []))) : $value;
	}

	public function has_class($name){
		return in_array($name, $this->class());
	}

	public function add_class($name){
		return $this->class('add', $name);
	}

	public function remove_class(...$args){
		return $args ? $this->class('remove', $args[0]) : $this->attr('class', []);
	}

	public function toggle_class($name){
		return $this->class('toggle', $name);
	}

	public function style(...$args){
		$fn		= fn($s)=> $s ? wpjam_array((array)$s, fn($k, $v)=> ($v || is_numeric($v)) ? [null, is_numeric($k) ? $v : $k.':'.$v] : null) : [];
		$style	= $fn($this->style);

		if($args){
			$args	= (count($args) == 1 || is_array($args[0])) ? $args[0] : [$args[0]=>$args[1]];

			return $this->attr('style', [...$style, ...$fn($args)]);
		}

		return wpjam_map($style, fn($v)=> $v ? rtrim($v, ';').';' : '');
	}

	public function render(){
		if($this->pull('__data')){
			return $this->render_data($this->get_args());
		}

		$attr	= wpjam_filter(self::process($this->get_args()), fn($v, $k)=> !str_ends_with($k, '_callback') 
			&& !str_ends_with($k, '_column')
			&& !str_starts_with($k, 'column_')
			&& !str_starts_with($k, '_')
			&& !str_starts_with($k, 'data-')
			&& !in_array($k, ['class', 'style', 'data', 'value']) 
			&& ($v || is_numeric($v)));

		$value	= $this->val();
		$attr	+= isset($value) ? compact('value') : [];
		$class	= array_merge($this->class(), wpjam_slice($attr, ['readonly', 'disabled']));
		$attr	+= wpjam_map(array_filter(['class'=>$class, 'style'=>$this->style()]), fn($v)=> implode(' ', array_unique($v)));

		return implode(wpjam_map($attr, function($v, $k){
			if(!is_scalar($v)){
				trigger_error($k.' '.var_export($v, true));
			}

			return ' '.$k.'="'.esc_attr($v).'"';
		})).$this->render_data($this->data());
	}

	protected function render_data($args){
		return implode(wpjam_map($args, function($v, $k){
			if(is_null($v) || $v === false){
				return '';
			}

			if($k == 'show_if'){
				$v	= wpjam_parse_show_if($v);

				if(!$v){
					return '';
				}
			}

			return ' data-'.$k.'=\''.(is_scalar($v) ? esc_attr($v) : ($k == 'data' ? http_build_query($v) : wpjam_json_encode($v))).'\'';
		}));
	}

	public static function is_bool($attr){
		return in_array($attr, ['allowfullscreen', 'allowpaymentrequest', 'allowusermedia', 'async', 'autofocus', 'autoplay', 'checked', 'controls', 'default', 'defer', 'disabled', 'download', 'formnovalidate', 'hidden', 'ismap', 'itemscope', 'loop', 'multiple', 'muted', 'nomodule', 'novalidate', 'open', 'playsinline', 'readonly', 'required', 'reversed', 'selected', 'typemustmatch']);
	}

	public static function process($attr){
		return wpjam_array($attr, function($k, $v){
			$k	= strtolower(trim($k));

			if(is_numeric($k)){
				$v = strtolower(trim($v));

				return self::is_bool($v) ? [$v, $v] : null;
			}else{
				return self::is_bool($k) ? ($v ? [$k, $k] : null) : [$k, $v];
			}
		});
	}

	public static function create($attr, $type=''){
		$attr	= ($attr && is_string($attr)) ? shortcode_parse_atts($attr) : wpjam_array($attr);
		$attr	+= $type == 'data' ? ['__data'=>true] : [];

		return new WPJAM_Attr($attr);
	}
}

class WPJAM_Tag extends WPJAM_Attr{
	protected $tag		= '';
	protected $text		= '';
	protected $_before	= [];
	protected $_after	= [];
	protected $_prepend	= [];
	protected $_append	= [];

	public function __construct($tag='', $attr=[], $text=''){
		$this->init($tag, $attr, $text);
	}

	public function __call($method, $args){
		if(in_array($method, ['text', 'tag', 'before', 'after', 'prepend', 'append'])){
			if($args){
				if(count($args) > 1){
					$value	= is_array($args[1])? new self(...$args) : new self($args[1], ($args[2] ?? []), $args[0]);
				}else{
					$value	= $args[0];

					if(is_array($value)){
						array_map(fn($v)=> $this->$method(...(is_array($v) ? $v : [$v])), $value);

						return $this;
					}
				}

				if($value || in_array($method, ['text', 'tag'])){
					if($method == 'text'){
						$this->text	= (string)$value;
					}elseif($method == 'tag'){
						$this->tag	= $value;
					}else{
						$cb	= 'array_'.(in_array($method, ['before', 'prepend']) ? 'unshift' : 'push');

						$cb($this->{'_'.$method}, $value);
					}
				}

				return $this;
			}

			return in_array($method, ['text', 'tag']) ? $this->$method : $this->{'_'.$method};
		}elseif(in_array($method, ['insert_before', 'insert_after', 'append_to', 'prepend_to'])){
			$args[0]->{str_replace(['insert_', '_to'], '', $method)}($this);

			return $this;
		}

		trigger_error($method);
	}

	public function is($tag){
		return $this->tag == $tag;
	}

	public function init($tag, $attr, $text){
		$this->empty();

		$this->tag	= $tag;
		$this->args	= ($attr && (wp_is_numeric_array($attr) || !is_array($attr))) ? ['class'=>$attr] : $attr;

		if($text && is_array($text)){
			$this->text(...$text);
		}elseif($text || is_numeric($text)){
			$this->text	= $text;
		}

		return $this;
	}

	public function render(){
		if($this->tag == 'a'){
			$this->href		??= 'javascript:;';
		}elseif($this->tag == 'img'){
			$this->title	??= $this->alt;
		}

		$render	= fn($k)=> $this->{'_'.$k} ? implode($this->{'_'.$k}) : '';
		$single	= $this->is_single($this->tag);
		$result	= $this->tag ? '<'.$this->tag.parent::render().($single ? ' />' : '>') : '';
		$result	.= !$single ? $render('prepend').(string)$this->text.$render('append') : '';
		$result	.= (!$single && $this->tag) ? '</'.$this->tag.'>' : '';

		return $render('before').$result.$render('after');
	}

	public function wrap($tag, ...$args){
		if(!$tag){
			return $this;
		}

		if(str_contains($tag, '></')){
			if(!preg_match('/<(\w+)([^>]+)>/', ($args ? sprintf($tag, ...$args) : $tag), $matches)){
				return $this;
			}

			$tag	= $matches[1];
			$attr	= shortcode_parse_atts($matches[2]);
		}else{
			$attr	= $args[0] ?? [];
		}

		return $this->init($tag, $attr, clone($this));
	}

	public function empty(){
		$this->_before	= $this->_after = $this->_prepend = $this->_append = [];
		$this->text		= '';

		return $this;
	}

	public static function is_single($tag){
		return $tag && in_array($tag, ['area', 'base', 'basefont', 'br', 'col', 'command', 'embed', 'frame', 'hr', 'img', 'input', 'isindex', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
	}
}

class WPJAM_Field extends WPJAM_Attr{
	protected function __construct($args){
		$this->args		= $args;
		$this->names	= $this->parse_names();
		$this->options	= $this->parse_options();

		$this->_data_type	= wpjam_get_data_type_object($this);

		if($this->pattern){
			$this->attr(wpjam_get_item('pattern', $this->pattern) ?: []);
		}
	}

	public function __get($key){
		$value	= parent::__get($key);

		if(!is_null($value)){
			return $value;
		}

		if($key == 'wrap_tag'){
			if(($this->is('mu-fields') || $this->is('fieldset', true)) && $this->fields && array_all($this->fields, fn($v)=> empty($v['title']))){
				return $this->$key = 'fieldset';
			}
		}elseif($key == 'show_in_rest'){
			return $this->_editable;
		}elseif($key == '_editable'){
			return !$this->disabled && !$this->readonly && !$this->is('view');
		}elseif($key == '_title'){
			return $this->title.'「'.$this->key.'」';
		}elseif($key == '_fields'){
			return $this->$key = WPJAM_Fields::create($this->fields, $this);
		}elseif($key == '_item'){
			if($this->is('mu-fields')){
				return $this->_fields;
			}

			$args	= wpjam_except($this->get_args(), ['required', 'show_in_rest']);
			$type	= $this->is('mu-text') ? $this->item_type : substr($this->type, 3);

			return $this->$key = self::create(array_merge($args, ['type'=>$type]));
		}elseif($key == '_options'){
			return wpjam_flatten($this->options, 'options', function($item, $opt){
				if(!is_array($item)){
					return $item;
				}

				if(isset($item['options'])){
					return;
				}

				if($k = array_find(['title', 'label', 'image'], fn($k)=> isset($item[$k]))){
					$v	= $item[$k];

					return empty($item['alias']) ? $v : array_replace([$opt=>$v], array_fill_keys(wp_parse_list($item['alias']), $v));
				}
			});
		}
	}

	public function __set($key, $value){
		parent::__set($key, $value);

		if($key == 'names'){
			$this->_name	= wpjam_at($value, -1);
			$this->name		= array_shift($value).($value ? '['.implode('][', $value).']' : '');
		}
	}

	public function __call($method, $args){
		if(str_contains($method, '_by_')){
			[$method, $type]	= explode('_by', $method);

			return $this->$type ? wpjam_try([$this->$type, $method], ...$args) : array_shift($args);
		}

		trigger_error($method);
	}

	public function is($type, ...$args){
		if($type === 'decimal'){
			$step	= ($args ? $args[0] : $this->step) ?: '';

			return $step == 'any' || strpos($step, '.');
		}

		$type	= wp_parse_list($type);
		$strict	= $args ? $args[0] : false;

		if(in_array('mu', $type) && str_starts_with($this->type, 'mu-')){
			return true;
		}

		return in_array($this->type, $type, $strict) || (!$strict && in_array($this->type, wpjam_slice(['fieldset'=>'fields', 'view'=>'hr'], $type)));
	}

	public function get_schema(){
		return $this->_schema ??= $this->parse_schema();
	}

	public function set_schema($schema){
		return $this->attr('_schema', $schema);
	}

	protected function call_schema($action, $value){
		$schema	= $this->get_schema();

		if(!$schema){
			return $value;
		}

		if($action == 'sanitize'){
			if($schema['type'] == 'string'){
				$value	= (string)$value;
			}
		}else{
			if($this->pattern && !rest_validate_json_schema_pattern($this->pattern, $value)){
				wpjam_throw('rest_invalid_pattern', wpjam_join(' ', [$this->_title, $this->custom_validity]));
			}
		}

		return wpjam_try('rest_'.$action.'_value_from_schema', $value, $schema, $this->_title);
	}

	protected function parse_schema(...$args){
		if($args){
			$schema	= $args[0];
		}else{
			$schema	= ['type'=>'string'];

			if($this->is('mu')){
				$schema	= $this->get_schema_by_item();
				$schema	= ['type'=>'array', 'items'=>($this->is('mu-fields') ? ['type'=>'object', 'properties'=>$schema] : $schema)];
			}elseif($this->is('email')){
				$schema['format']	= 'email';
			}elseif($this->is('color')){
				if(!$this->data('alpha-enabled')){
					$schema['format']	= 'hex-color';
				}
			}elseif($this->is('url, image, file, img')){
				if($this->is('img') && $this->item_type != 'url'){
					$schema['type']	= 'integer';
				}else{
					$schema['format']	= 'uri';
				}
			}elseif($this->is('number, range')){
				$schema['type']	= $this->is('decimal') ? 'number' : 'integer';

				if($schema['type'] == 'integer' && $this->step > 1){
					$schema['multipleOf']	= $this->step;
				}
			}elseif($this->is('radio, select, checkbox')){
				if($this->is('checkbox') && !$this->options){
					$schema['type']	= 'boolean';
				}else{
					$schema	+= $this->custom_input ? [] : ['enum'=>array_keys($this->_options)];
					$schema	= $this->is('checkbox') ? ['type'=>'array', 'items'=>$schema] : $schema;
				}
			}

			if(in_array($schema['type'], ['number', 'integer'])){
				$map	= [
					'minimum'	=> 'min',
					'maximum'	=> 'max',
					'pattern'	=> 'pattern',
				];
			}elseif($schema['type'] == 'string'){
				$map	= [
					'minLength'	=> 'minlength',
					'maxLength'	=> 'maxlength',
					'pattern'	=> 'pattern'
				];
			}elseif($schema['type'] == 'array'){
				$map	= [
					'maxItems'		=> 'max_items',
					'minItems'		=> 'min_items',
					'uniqueItems'	=> 'unique_items',
				];
			}

			$schema	+= wpjam_array($map ?? [], fn($k, $v)=> isset($this->$v) ? [$k, $this->$v] : null);
			$rest	= $this->show_in_rest;

			if($rest && is_array($rest)){
				if(isset($rest['schema']) && is_array($rest['schema'])){
					$schema	= wpjam_merge($schema, $rest['schema']);
				}

				if(!empty($rest['type'])){
					$key	= $schema['type'] == 'array' && $rest['type'] != 'array' ? 'items.type' : 'type';
					$schema	= wpjam_set($schema, $key, $rest['type']);
				}
			}

			if($this->required && !$this->show_if){	// todo 以后可能要改成 callback
				$schema['required']	= true;
			}
		}

		$schema	= wpjam_except($schema, wpjam_filter(['object'=>'properties', 'array'=>'items'], fn($v, $k)=> isset($schema[$v]) && $k != $schema['type']));
		$parse	= [
			'enum'			=> fn($value)=> array_map(fn($v)=> rest_sanitize_value_from_schema($v, $schema), $value),
			'properties'	=> fn($value)=> array_map([$this, 'parse_schema'], $value),
			'items'			=> fn($value)=> $this->parse_schema($value),
		];

		if($k = array_find_key($parse, fn($v, $k)=> isset($schema[$k]))){
			$schema[$k]	= $parse[$k]($schema[$k]);
		}

		return $schema;
	}

	protected function parse_options(){
		$options	= $this->call_property('options') ?? $this->options;

		return $this->is('select') ? array_replace(wpjam_array(['all', 'none'], fn($k, $v)=> ($v = $this->pull('show_option_'.$v, false)) === false ? null : [$this->pull('option_'.$v.'_value', ''), $v]), $options) : $options;
	}

	protected function parse_names($prepend=null){
		$fn	= fn($v)=> $v ? ((str_contains($v, '[') && preg_match_all('/\[?([^\[\]]+)\]*/', $v, $m)) ? $m[1] : [$v]) : [];

		return array_merge($fn($prepend ?? $this->pull('prepend_name')), $fn($this->name));
	}

	protected function parse_show_if(...$args){
		if($args = wpjam_parse_show_if($args ? $args[0] : $this->show_if)){
			if(isset($args['compare']) || !isset($args['query_arg'])){
				$args	+= ['value'=>true];
			}

			if($this->_creator && $this->_creator->get_by_fields($args['key'])){
				$args['key']	= $this->_prefix.$args['key'].$this->_suffix;
			}

			return $args;
		}
	}

	public function show_if($values){
		$args	= $this->parse_show_if();

		return ($args && empty($args['external'])) ? wpjam_if($values, $args) : true;
	}

	public function validate($value, $for=''){
		$code	= $for ?: 'value';
		$value	??= $this->default;

		if($for == 'parameter' && $this->required && is_null($value)){
			wpjam_throw('missing_'.$code, '缺少参数:'.$this->key);
		}

		if($this->validate_callback){
			$result	= wpjam_try($this->validate_callback, $value);

			if($result === false){
				wpjam_throw('invalid_'.$code, [$this->key]);
			}
		}

		$value	= $this->try('validate_value', $value);

		if(!$this->is('fieldset') || $this->_data_type){
			if($for == 'parameter'){
				if(!is_null($value)){
					$value	= $this->call_schema('sanitize', $value);
				}
			}else{
				if($this->required && !$value && !is_numeric($value)){
					wpjam_throw($code.'_required', [$this->_title]);
				}

				$value	= $this->before_schema($value);

				if($value || is_array($value) || is_numeric($value)){	// 空值只需 required 验证
					$this->call_schema('validate', $value);
				}
			}
		}

		if($this->sanitize_callback){
			return wpjam_try($this->sanitize_callback, ($value ?? ''));
		}

		return $value;
	}

	public function validate_value($value){
		if($this->is('mu')){
			$value	= is_array($value) ? wpjam_filter($value, fn($v)=> $v || is_numeric($v), true) : ($value ? wpjam_json_decode($value) : []);
			$value	= (!$value || is_wp_error($value)) ? [] : array_values($value);

			return array_map([$this, 'validate_value_by_item'], $value);
		}elseif($this->custom_input){
			return $this->call_custom('validate', $value);
		}

		return $this->validate_value_by_data_type($value, $this);
	}

	protected function before_schema($value, $schema=null){
		if(is_null($schema)){
			$schema	= $this->get_schema();

			if(is_null($value) && $schema && !empty($schema['required'])){
				$value	= false;
			}
		}

		if($schema){
			$cb	= [
				'array'		=> ['is_array', fn($value)=> wpjam_map($value, fn($v)=> $this->before_schema($v, $schema['items']))],
				'object'	=> ['is_array', fn($value)=> wpjam_map($value, fn($v, $k)=> $this->before_schema($v, ($schema['properties'][$k] ?? '')))],
				'integer'	=> ['is_numeric', 'intval'],
				'number'	=> ['is_numeric', 'floatval'],
				'string'	=> ['is_scalar', 'strval'],
				'null'		=> [fn($v)=> !$v && !is_numeric($v), fn()=>null],
				'boolean'	=> [fn($v)=> is_scalar($v) || is_null($v), 'rest_sanitize_boolean']
			][$schema['type']] ?? '';

			if($cb && $cb[0]($value)){
				return $cb[1]($value);
			}
		}

		return $value;
	}

	public function pack($value){
		return wpjam_set([], $this->names, $value);
	}

	public function unpack($data){
		return wpjam_get($data, $this->names);
	}

	public function value_callback($args=[]){
		$value	= null;

		if($args && (!$this->is('view') || is_null($this->value))){
			if($this->value_callback){
				$value	= wpjam_value_callback($this->value_callback, $this->_name, wpjam_get($args, 'id'));
			}else{
				$name	= $this->names[0];

				if(!empty($args['data']) && isset($args['data'][$name])){
					$value	= $args['data'][$name];
				}else{
					$id		= wpjam_get($args, 'id');

					if(!empty($args['value_callback'])){
						$value	= wpjam_value_callback($args['value_callback'], $name, $id);
					}

					if($id && !empty($args['meta_type']) && is_null($value)){
						$value	= wpjam_get_metadata($args['meta_type'], $id, $name);
					}
				}

				$value	= (is_wp_error($value) || is_null($value)) ? null : $this->unpack([$name=>$value]);
			}
		}

		return $value ?? $this->value;
	}

	public function prepare($args){
		return $this->prepare_value($this->call_schema('sanitize', $this->value_callback($args)));
	}

	public function prepare_value($value){
		if($this->is('mu')){
			return array_map([$this, 'prepare_value_by_item'], $value);
		}elseif($this->is('img, image, file')){
			return wpjam_get_thumbnail($value, $this->size);
		}elseif($this->custom_input){
			return $this->call_custom('prepare', $value);
		}

		return $this->prepare_value_by_data_type($value, $this);
	}

	protected function call_custom($action, $value){
		$values	= array_map('strval', array_keys($this->_options));
		$input	= $this->custom_input;
		$field	= $this->_custom ??= self::create((is_array($input) ? $input : [])+[
			'title'			=> is_string($input) ? $input : '其他',
			'placeholder'	=> '请输入其他选项',
			'id'			=> $this->id.'__custom_input',
			'key'			=> $this->key.'__custom_input',
			'type'			=> 'text',
			'class'			=> '',
			'required'		=> true,
			'show_if'		=> [$this->key, '__custom'],
		]);

		if($this->is('checkbox')){
			$value	= array_diff(($value ?: []), ['__custom']);
			$diff	= array_diff($value, $values);

			if($diff){
				$field->val(reset($diff));

				if($action == 'validate'){
					if(count($diff) > 1){
						wpjam_throw('too_many_custom_value', $field->_title.'只能传递一个其他选项值');
					}

					$field->set_schema($this->get_schema()['items'])->validate(reset($diff));
				}
			}
		}else{
			if(isset($value) && !in_array($value, $values)){
				$field->val($value);

				if($action == 'validate'){
					$field->set_schema($this->get_schema())->validate($value);
				}
			}
		}

		if($action == 'render'){
			$this->value	= isset($field->value) ? ($this->is('checkbox') ? [...$value, '__custom'] : '__custom') : $value;
			$this->options	+= ['__custom'=>$field->title];

			return $field->attr(['title'=>'', 'name'=>$this->name])->wrap();
		}

		return $value;
	}

	public function wrap($tag='', $args=[]){
		if(is_object($tag)){
			$wrap	= $this->wrap_tag ?: (($this->is('fieldset') && !$this->group && !$args) ? '' : 'div');

			$tag->wrap($wrap, $args)->add_class($this->group ? 'field-group' : '');

			if($wrap == 'fieldset'){
				if($this->title){
					$tag->prepend('legend', ['screen-reader-text'], $this->title);
				}
			
				if($this->summary){
					$tag->before([$this->summary, 'strong'], 'summary')->wrap('details');
				}
			}

			return $tag;
		}

		$creator	= $this->_creator;

		if($creator){
			if($creator->is('mu-fields') || $creator->propertied){
				$prepend	= $creator->name;
				$prefix		= $this->_prefix = $creator->key.'__';
				$suffix		= '';

				if($creator->is('mu-fields')){
					$prepend	.= '['.$args['i'].']';
					$suffix		= $this->_suffix = '__'.$args['i'];

					if(isset($args['v'][$this->name])){
						$this->value	= $args['v'][$this->name];
					}
				}

				$this->names	= $this->parse_names($prepend);
				$this->id		= $prefix.$this->id.$suffix;
				$this->key		= $prefix.$this->key.$suffix;
			}

			if($creator->label === true){
				$args	+= ['label'=>true];
			}
		}

		$show_if	= $this->parse_show_if();

		$class	= [$this->disabled, $this->readonly, ($this->is('hidden') ? 'hidden' : '')];
		$wrap	= wpjam_tag($tag, $class)->add_class($this->pull('wrap_class'))->add_class(wpjam_get($args, 'wrap_class'));
		$field	= $this->render($args);

		if($this->buttons){
			$this->after	= wpjam_join(' ', [$this->after, implode(' ', wpjam_map($this->buttons, [self::class, 'create']))]);
		}

		foreach(array_filter(wpjam_pick($this, ['before', 'after'])) as $k => $v){
			$action	= $field->is('div') ? ($k == 'before' ? 'prepend' : 'append') : $k;

			$field->$action($k == 'before' ? $v.'&nbsp;' : '&nbsp;'.$v);
		}

		if($this->is('fieldset')){
			$_args	= array_filter(wpjam_pick($this, ['class', 'style'])+['data'=>$this->data()]);
			$_args	= $_args ? wpjam_set($_args, 'data.key', $this->key) : [];

			$this->wrap($field, $_args);

			$wrap->after("\n");
		}else{
			if(($this->label || $this->before || $this->after || !empty($args['label'])) && !$field->is('div')){
				$this->label($field);
			}
		}

		$title	= $this->title ? $this->label() : '';
		$desc	= $this->description ? wpjam_tag('p', ['description'], $this->description) : '';

		$field->after($desc);

		if($creator && !$creator->is('fields')){
			if($creator->wrap_tag == 'fieldset'){
				if($title || $desc || ($show_if && ($this->is('fields') || !is_null($field->data('query_title'))))){
					$field->before($title ? ['<br />', $title] : null)->wrap('div', ['inline']);
				}

				$title	= null;
			}else{
				$wrap->add_class('sub-field');

				if($title){
					$title->add_class('sub-field-label');
					$field->wrap('div', ['sub-field-detail']);
				}
			}
		}

		if($tag){
			$wrap->attr('id', $tag.'_'.$this->id)->data('show_if', $show_if);
		}else{
			$field->add_class($class)->data('show_if', $show_if);
		}

		if($tag == 'tr'){
			$field->wrap('td', $title ? [] : ['colspan'=>2]);

			if($title){
				$title->wrap('th', ['scope'=>'row']);
			}
		}elseif($tag == 'p'){
			if($title){
				$title->after('<br />');
			}
		}

		return $wrap->append([$title, $field]);
	}

	public function render($args=[]){
		$this->value	= $this->value_callback($args);
		$this->class	??= $this->is('text, password, url, email, image, file, mu-image, mu-file') ? 'regular-text' : null;

		if($this->render){
			return wpjam_wrap($this->call_property('render', $args));
		}elseif($this->is('fieldset')){
			return $this->render_by_fields($args);
		}elseif($this->is('mu')){
			$value	= $this->value ?: [];
			$value	= is_array($value) ? array_values(wpjam_filter($value, fn($v)=> $v || is_numeric($v), true)) : [$value];
			$wrap	= wpjam_tag('div', ['id'=>$this->id, 'data-max_items'=>$this->max_items]);
		
			if($this->is('mu-img, mu-image, mu-file')){
				if(!current_user_can('upload_files')){
					$this->disabled	= 'disabled';
				}

				$data['button_text']	= '选择'.($this->is('mu-file') ? '文件' : '图片').'[多选]';
			}else{
				$data['button_text']	= $this->button_text ?: '添加'.(wpjam_between(mb_strwidth($this->title ?: ''), 4, 8) ? $this->title : '选项');
			}

			if($this->is('mu-fields')){
				$append	= wpjam_map($value+['${i}'=>[]], fn($v, $i)=> $this->wrap($this->render_by_fields(['i'=>$i, 'v'=>$v])->wrap($v ? '' : 'template'), ['mu-item']));
			}else{
				$args	= ['id'=>'', 'name'=>$this->name.'[]', 'value'=>null];
				$data	+= ['value'=>&$value];

				if($this->is('mu-img')){
					$this->direction	= 'row';

					$value	= array_map(fn($v)=> ['value'=>$v, 'url'=> wpjam_at(explode('?', wpjam_get_thumbnail($v)), 0)] , $value);
					$data	+= ['thumb_args'=> wpjam_get_thumbnail_args([200, 200]), 'item_type'=>$this->item_type];
					$append	= $this->input($args+['type'=>'hidden']);
				}elseif($this->is('mu-image, mu-file')){
					$data	+= ['item_type'=> $this->is('mu-image') ? 'image' : $this->item_type];
					$append	= $this->input($args+['type'=>'url']);
				}elseif($this->is('mu-select, mu-text')){
					if($this->is('mu-select')){
						$this->type			= 'mu-text';
						$this->item_type	= 'select';
					}else{
						$this->item_type	??= 'text';
					}

					if($this->item_type == 'text' && $this->direction == 'row'){
						$this->class	??= 'medium-text';
					}

					$value	= $this->_data_type ? array_map(fn($v)=> ['value'=>$v, 'label'=>$this->query_label_by_data_type($v, $this)], $value) : $value;
					$append	= $this->attr_by_item($args)->render();
				}
			}

			return $wrap->append($append)->data($data)->add_class(['mu', $this->type, ($this->sortable !== false ? 'sortable' : ''), 'direction-'.($this->direction ?: 'column')]);
		}elseif($this->is('radio, select, checkbox')){
			if($this->is('checkbox') && !$this->options){
				$this->label	??= $this->pull('description');

				return $this->input(['value'=>1])->data('value', $this->value)->after($this->label);
			}

			if($this->is('checkbox')){
				$this->name	.= '[]';

				if($this->pull('required')){
					$this->min_items	??= 1;
				}
			}

			$custom	= $this->custom_input ? $this->call_custom('render', $this->value) : '';
			$items	= $this->render_options($this->options);

			if($this->is('select')){
				return $this->tag('select')->data('value', $this->value)->append($items)->after($custom ? '&emsp;'.$custom : '');
			}

			$sep	= $this->sep ?: '';
			$dir	= $this->direction ?: ($sep ? '' : 'row');

			return  wpjam_tag('fieldset', ['id'=>$this->id.'_options', 'class'=>'checkable'], implode($sep, array_filter([...array_values($items), $custom])))->data(wpjam_pick($this, ['max_items', 'min_items', 'value']))->add_class($dir ? 'direction-'.$dir : '');
		}elseif($this->is('textarea')){
			return $this->textarea();
		}else{
			return $this->input()->data(['class'=>$this->class]+($this->_data_type ? ['label'=>$this->query_label_by_data_type($this->value, $this)] : []));
		}
	}

	protected function render_options($options){
		return wpjam_map($options, function($label, $opt){
			$attr	= $data = $class = [];

			if(is_array($label)){
				$arr	= $label;
				$label	= wpjam_pull($arr, ['label', 'title']);
				$label	= $label ? reset($label) : '';
				$image	= wpjam_pull($arr, 'image');

				if($image){
					$image	= is_array($image) ? array_slice($image, 0, 2) : [$image];
					$label	= implode(array_map(fn($i)=> wpjam_tag('img', ['src'=>$i, 'alt'=>$label]), $image)).$label;
					$class	= ['image-'.$this->type];
				}

				foreach($arr as $k => $v){
					if(is_numeric($k)){
						if(self::is_bool($v)){
							$attr[$v]	= $v;
						}
					}elseif(self::is_bool($k)){
						if($v){
							$attr[$k]	= $k;
						}
					}elseif($k == 'show_if'){
						$data[$k]	= $this->parse_show_if($v);
					}elseif($k == 'alias'){
						$data[$k]	= wp_parse_list($v);
					}elseif($k == 'class'){
						$class	= [...$class, ...wp_parse_list($v)];
					}elseif($k == 'description'){
						$this->description	.= $v ? wpjam_wrap($v, 'span', ['data-show_if'=>$this->parse_show_if([$this->key, '=', $opt])]) : '';
					}elseif($k == 'options'){
						if($this->is('select')){
							$attr	+= ['label'=>$label];
							$label	= $this->render_options($v ?: []);
						}
					}elseif(!is_array($v)){
						$data[$k]	= $v;
					}
				}
			}

			if($this->is('select')){
				$opt	= (isset($data['alias']) && isset($this->value) && in_array($this->value, $data['alias'])) ? $this->value : $opt;
				$args	= is_array($label) ? ['optgroup', $attr] : ['option', $attr+['value'=>$opt]];
				$tag	= wpjam_tag(...$args)->append($label);
			}else{
				$attr	= ['id'=>$this->id.'_'.$opt, 'value'=>$opt]+$attr;
				$tag 	= $this->input($attr)->after($label)->wrap('label', ['for'=>$attr['id']]);
			}

			return $tag->data($data)->add_class($class);
		});
	}

	protected function label($tag=''){
		return wpjam_wrap($tag ?: $this->title, 'label', $this->is('view, mu, fieldset, img, uploader, radio') || ($this->is('checkbox') && $this->options) ? [] : ['for'=> $this->id]);
	}

	protected function tag($tag='input', $attr=[]){
		$tag	= wpjam_tag($tag, $this->get_args())->attr($attr)->add_class('field-key field-key-'.$this->key);
		$data	= $tag->pull(['key', 'data_type', 'query_args', 'custom_validity']);
		$tag	= $tag->data($data)->remove_attr(['default', 'options', 'title', 'names', 'label', 'render', 'before', 'after', 'description', 'item_type', 'max_items', 'min_items', 'unique_items', 'direction', 'group', 'buttons', 'button_text', 'size', 'filterable', 'post_type', 'taxonomy', 'sep', 'fields', 'parse_required', 'show_if', 'show_in_rest', 'column', 'custom_input']);

		return $tag->is('input') ? $tag : $tag->remove_attr(['type', 'value']);
	}

	protected function input($attr=[]){
		$tag	= $this->tag('input', $attr);

		if(in_array($tag->type, ['number', 'url', 'tel', 'email', 'search'])){
			$tag->inputmode	??= $tag->type == 'number' ? ($this->is('decimal', $tag->step) ? 'decimal' : 'numeric') : $tag->type;
		}

		return $tag;
	}

	protected function textarea(){
		return $this->tag('textarea')->append(esc_textarea($this->value ?: ''));
	}

	public static function add_pattern($key, $args){
		wpjam_add_item('pattern', $key, $args);
	}

	public static function parse($field){
		if(is_string($field)){
			return ['type'=>'view', 'value'=>$field, 'options'=>[], 'wrap_tag'=>''];
		}

		$field	= self::process($field);

		$field['options']	= wpjam_get($field, 'options') ?: [];
		$field['type']		= $type = wpjam_get($field, 'type') ?: array_find(['options'=>'select', 'label'=>'checkbox', 'fields'=>'fieldset'], fn($v, $k)=> !empty($field[$k])) ?: 'text';

		if($type == 'fields' && wpjam_pull($field, 'fields_type') == 'size'){
			$type	= 'size';

			$field['propertied']	= false;
		}

		if($type == 'size'){
			$field['type']			= 'fields';
			$field['propertied']	??= true;
			$field['fields']		= wpjam_array(wpjam_merge([
				'width'		=> ['type'=>'number',	'class'=>'small-text'],
				'x'			=> '✖️',
				'height'	=> ['type'=>'number',	'class'=>'small-text']
			], ($field['fields'] ?? [])), fn($k, $v)=> is_array($v) && !empty($v['key']) ? $v['key'] : $k);
		}elseif(in_array($type, ['fieldset', 'fields'])){
			$field['propertied']	??= !empty($field['data_type']) ? true : wpjam_pull($field, 'fieldset_type') == 'array';	
		}

		return $field;
	}

	public static function create($field, $key=''){
		$field	= self::parse($field);
		$field	= ($key && !is_numeric($key) ? ['key'=>$key] : [])+$field;

		if(empty($field['key']) || is_numeric($field['key'])){
			trigger_error('Field 的 key 不能为'.(empty($field['key']) ? '空' : '纯数字「'.$field['key'].'」'));
			return;
		}

		$field	= wpjam_fill(['id', 'name'], fn($k)=> wpjam_get($field, $k) ?: $field['key'])+$field;
		$field	+= array_filter(['max_items'=> wpjam_pull($field, 'total')]);
		$type	= $field['type'];

		if($type == 'color'){	
			$field	+= ['label'=>true, 'data-button_text'=> wpjam_pull($field, 'button_text'), 'data-alpha-enabled'=>wpjam_pull($field, 'alpha')];
		}elseif($type == 'timestamp'){
			$field['sanitize_callback']	= fn($value)=> $value ? wpjam_strtotime($value) : 0;
		}elseif($type == 'editor'){
			$field['render']	??= function(){
				$this->id	= 'editor_'.$this->id;

				if(user_can_richedit()){
					if(!wp_doing_ajax()){
						return wpjam_ob_get_contents('wp_editor', ($this->value ?: ''), $this->id, ['textarea_name'=>$this->name]);
					}

					$this->data('editor', ['tinymce'=>true, 'quicktags'=>true, 'mediaButtons'=>current_user_can('upload_files')]);
				}

				return $this->textarea();
			};
		}elseif(in_array($type, ['img', 'image', 'file'])){
			$field['render']	??= function(){
				if(!current_user_can('upload_files')){
					$this->disabled	= 'disabled';
				}

				$size	= array_filter(wpjam_slice(wpjam_parse_size($this->size), ['width', 'height']));

				if(count($size) == 2){
					$this->description	??= '建议尺寸:'.implode('x', $size);
				}

				if($this->is('img')){
					$size	= wpjam_parse_size($this->size ?: '600x0', [600, 600]);
					$data	= ['thumb_args'=> wpjam_get_thumbnail_args($size)];
					$src	= $this->value ? wpjam_get_thumbnail($this->value, $size) : '';
					$tag	= $this->input(['type'=>'hidden'])->before('img', array_filter(['src'=>$src]+wpjam_map($size, fn($v)=> (int)($v/2))));
				}else{
					$tag	= $this->input(['type'=>'url']);
				}

				return $tag->wrap('div', ['wpjam-'.$this->type])->data(($data ?? [])+[
					'item_type'		=> $this->is('image') ? 'image' : $this->item_type,
					'media_button'	=> $this->button_text ?: '选择'.($this->is('file') ? '文件' : '图片')
				]);	
			};
		}elseif($type == 'uploader'){
			$field['render']	??= function(){
				$mime_types	= $this->pull('mime_types') ?: ['title'=>'图片', 'extensions'=>'jpeg,jpg,gif,png'];
				$plupload	= [
					'browse_button'		=> 'plupload_button__'.$this->key,
					'button_text'		=> $this->button_text ?: __('Select Files'),
					'container'			=> 'plupload_container__'.$this->key,
					'file_data_name'	=> $this->key,
					'filters'			=> [
						'mime_types'	=> wp_is_numeric_array($mime_types) ? $mime_types : [$mime_types],
						'max_file_size'	=> (wp_max_upload_size() ?: 0).'b'
					],
					'multipart_params'	=> [
						'_ajax_nonce'	=> wp_create_nonce('upload-'.$this->key),
						'action'		=> 'wpjam-upload',
						'file_name'		=> $this->key,
					]
				]+(($this->pull('drap_drop') && !wp_is_mobile()) ? [
					'drop_element'	=> 'plupload_drag_drop__'.$this->key,
					'drop_info'		=> [__('Drop files to upload'), _x('or', 'Uploader: Drop files here - or - Select Files')]
				] : []);

				return $this->input(['type'=>'hidden'])->wrap('div', ['plupload'])->data(['key'=>$this->key, 'plupload'=>$plupload]);
			};
		}elseif($type == 'view'){
			$field['render']	??= function(){
				$value	= (string)$this->value;
				$wrap	= $value != strip_tags($value);
				$tag	= $this->wrap_tag ?? (!$this->show_if && $wrap ? '' : 'span');
				$value	= $this->options && !$wrap ? (array_find($this->_options, fn($v, $k)=> $value ? $k == $value : !$k) ?? $value) : $value;

				return $tag ? wpjam_tag($tag, ['field-key field-key-'.$this->key], $value)->data('value', $this->value) : $value;
			};
		}elseif($type == 'hr'){
			$field['render']	= fn()=> wpjam_tag('hr');
		}

		return new WPJAM_Field($field);
	}

	public static function ajax_upload($data){
		if(!check_ajax_referer('upload-'.$data['file_name'], false, false)){
			wp_die('invalid_nonce');
		}

		return wpjam_upload($data['file_name']);
	}
}

class WPJAM_Fields extends WPJAM_Attr{
	private $fields		= [];
	private $creator	= null;

	private function __construct($fields, $creator=null){
		$this->fields	= $fields ?: [];
		$this->creator	= $creator;
	}

	public function	__call($method, $args){
		$data	= [];

		foreach($this->fields as $field){
			if((in_array($method, ['get_defaults', 'get_show_if_values']) && !$field->_editable)
				|| ($method == 'prepare' && !$field->show_in_rest)
			){
				continue;
			}

			if($field->is('fieldset') && !$field->_data_type){
				$value	= $field->try($method.'_by_fields', ...$args);
			}else{
				$name	= $field->_name;

				if($method == 'prepare'){
					$value	= $field->pack($field->prepare(...$args));
				}elseif($method == 'get_defaults'){
					$value	= $field->pack($field->value);
				}elseif($method == 'get_show_if_values'){ // show_if 基于key,且propertied的fieldset的key是 {$key}__{$sub_key}
					$item	= wpjam_catch([$field, 'validate'], $field->unpack(...$args));
					$value	= [$field->key => is_wp_error($item) ? null : $item];
				}elseif($method == 'get_schema'){
					$value	= array_filter([$name => $field->get_schema()]);
				}elseif(in_array($method, ['prepare_value', 'validate_value'])){
					$value	= isset($args[0][$name]) ? [$name => $field->try($method, $args[0][$name])] : [];
				}else{
					$value	= $field->try($method, ...$args);
				}
			}

			$data	= wpjam_merge($data, $value);
		}

		return $data;
	}

	public function	__invoke($args=[]){
		return $this->render($args);
	}

	public function get($key){
		return $this->fields[$key] ?? null;
	}

	public function validate($values=null, $for=''){
		$data	= [];
		$values	??= wpjam_get_post_parameter();

		[$if_values, $if_show]	= ($this->creator && $this->creator->_if) ? $this->creator->_if : [$this->get_show_if_values($values), true];

		foreach($this->fields as $field){
			if(!$field->_editable){
				continue;
			}

			$value	= $values;
			$show	= $if_show ? $field->show_if($if_values) : false;

			if($field->is('fieldset')){
				$field->_if	= [$if_values, $show];
				$value		= $field->propertied ? $field->chain($value)->unpack()->validate_by_fields($for)->pack()->value() : $field->validate_by_fields($value, $for);
			}

			if(!$field->is('fieldset') || ($show && $field->propertied)){
				if(!$show){
					$if_values[$field->key] = null;	// 第一次获取的值都是经过 json schema validate 的,可能存在 show_if 的字段在后面
				}

				$value	= $show ? $field->chain($value)->unpack()->validate($for)->pack()->value() : $field->pack(null);
			}

			$data	= wpjam_merge($data, $value);
		}

		return $data;
	}

	public function render($args=[], $to_string=false){
		$fields		= $groups = [];
		$creator	= $this->creator;

		if($creator){
			$type	= '';
			$wrap	= $creator->is('fields') ? '' : $creator->wrap_tag;
			$tag	= is_null($wrap) ? 'div' : '';
			$sep	= $creator->sep ??= ($wrap == 'fieldset' ? ($creator->group ? '' : '<br />') : '')."\n";
			$group	= reset($this->fields)->group;

			if($creator->is('fieldset') && $creator->propertied && is_array($creator->value)){
				$args['data']	= $creator->pack($creator->value);
			}
		}else{
			$type	= wpjam_pull($args, 'fields_type', 'table');
			$tag	= wpjam_pull($args, 'wrap_tag', (['table'=>'tr', 'list'=>'li'][$type] ?? $type));
			$sep	= "\n";
		}

		foreach($this->fields as $field){
			if($creator && $field->group != $group){
				[$groups[], $fields, $group]	= [[$group, $fields], [], $field->group];
			}

			$fields[]	= $field->sandbox(fn()=> $this->wrap($tag, $args));
		}

		if($groups){
			$fields	= array_merge(...array_map(fn($g)=> ($g[0] && count($g[1]) > 1) ? [wpjam_tag('div', ['field-group'], implode("\n", $g[1]))] : $g[1], [...$groups, [$group, $fields]]));
		}

		$fields	= wpjam_wrap(implode($sep, array_filter($fields)));

		if($type == 'table'){
			$fields->wrap('tbody')->wrap('table', ['cellspacing'=>0, 'class'=>'form-table']);
		}elseif($type == 'list'){
			$fields->wrap('ul');
		}

		return $to_string ? (string)$fields : $fields;
	}

	public function get_parameter($method='POST', $merge=true){
		$data		= wpjam_get_parameter('', [], $method);
		$validated	= $this->validate($data, 'parameter');

		return $merge ? array_merge($data, $validated) : $validated;
	}

	public static function create($fields, $creator=null){
		$args	= [];

		if($creator){
			$sink	= wpjam_pick($creator, ['readonly', 'disabled']);

			if($creator->is('mu-fields') || $creator->propertied){
				if(!$fields){
					wp_die($creator->_title.'fields不能为空');
				}
			}else{
				$args['prefix']	= $creator->prefix === true ? $creator->key : $creator->prefix;
			}
		}

		foreach(self::parse($fields, $args) as $key => $field){
			if(wpjam_get($field, 'show_admin_column') === 'only'){
				continue;
			}

			$object	= WPJAM_Field::create($field, $key);

			if(!$object){
				continue;
			}

			if($creator){
				if($creator->is('mu-fields') || $creator->propertied){
					if(count($object->names) > 1 || ($object->is('fieldset', true) && !$object->data_type) || $object->is('mu-fields')){
						trigger_error($creator->_title.'子字段不允许'.(count($object->names) > 1 ? '[]模式' : $object->type).':'.$object->name);

						continue;
					}
				}else{
					$object->show_in_rest	??= $creator->show_in_rest;
				}

				$object->attr('_creator', $creator)->attr($sink);
			}

			$objects[$key]	= $object;
		}

		return new self($objects ?? [], $creator);
	}

	public static function parse($fields, $args=[]){
		$parsed	= [];
		$fields	= (array)($fields ?: []);
		$length	= count($fields);

		for($i=0; $i<$length; $i++){
			$key	= array_keys($fields)[$i];
			$field	= WPJAM_Field::parse($fields[$key]);

			if(!empty($args['prefix'])){
				if($field['type'] == 'fields' && !$field['propertied']){	// 向下传递
					$field	= array_merge($field, ['prefix'=>$args['prefix']]);
				}else{
					$key	= wpjam_join('_', [$args['prefix'], $key]);
				}
			}

			$parsed[$key]	= $field;

			if(in_array($field['type'], ['fieldset', 'fields'])){
				$subs	= (!empty($args['flat']) && !$field['propertied']) ? $field['fields'] : [];
			}elseif($field['type'] == 'checkbox' && !$field['options']){
				$subs	= wpjam_map((wpjam_pull($field, 'fields') ?: []), fn($v)=> $v+(isset($v['show_if']) ? [] : ['show_if'=>[$key, '=', 1]]));
			}else{
				$subs	= wpjam_flatten($field['options'], 'options', fn($item, $opt)=> (is_array($item) && isset($item['fields'])) ? wpjam_map($item['fields'], fn($v)=> $v+(isset($v['show_if']) ? [] : ['show_if'=>[$key, '=', $opt]])) : null);
			}

			if($subs){
				$fields	= wpjam_add_at($fields, $i+1, $subs);
				$length	= count($fields);
			}
		}

		return $parsed;
	}
}