> */ public static function get_types() { static $types = null; if ( null !== $types ) { return $types; } $types = [ 'text' => [ 'label' => __( 'Text', 'admin-optimizer' ), 'supports_default' => true, 'supports_multiple' => false, 'supports_repeater' => true, 'supports_choices' => false, 'supports_children' => false, 'supports_required' => true, 'supports_disable_empty_save' => true, ], 'textarea' => [ 'label' => __( 'Textarea', 'admin-optimizer' ), 'supports_default' => true, 'supports_multiple' => false, 'supports_repeater' => true, 'supports_choices' => false, 'supports_children' => false, 'supports_required' => true, 'supports_disable_empty_save' => true, ], 'wysiwyg' => [ 'label' => __( 'WYSIWYG', 'admin-optimizer' ), 'supports_default' => true, 'supports_multiple' => false, 'supports_repeater' => true, 'supports_choices' => false, 'supports_children' => false, 'supports_required' => true, 'supports_disable_empty_save' => true, ], 'html_message' => [ 'label' => __( 'HTML Message', 'admin-optimizer' ), 'supports_default' => false, 'supports_multiple' => false, 'supports_repeater' => false, 'supports_choices' => false, 'supports_children' => false, 'supports_required' => false, 'supports_disable_empty_save' => false, ], 'number' => [ 'label' => __( 'Number', 'admin-optimizer' ), 'supports_default' => true, 'supports_multiple' => false, 'supports_repeater' => true, 'supports_choices' => false, 'supports_children' => false, 'supports_required' => true, 'supports_disable_empty_save' => true, ], 'range' => [ 'label' => __( 'Range', 'admin-optimizer' ), 'supports_default' => true, 'supports_multiple' => false, 'supports_repeater' => false, 'supports_choices' => false, 'supports_children' => false, 'supports_required' => true, 'supports_disable_empty_save' => true, ], 'email' => [ 'label' => __( 'Email', 'admin-optimizer' ), 'supports_default' => true, 'supports_multiple' => false, 'supports_repeater' => true, 'supports_choices' => false, 'supports_children' => false, 'supports_required' => true, 'supports_disable_empty_save' => true, ], 'phone' => [ 'label' => __( 'Phone', 'admin-optimizer' ), 'supports_default' => true, 'supports_multiple' => false, 'supports_repeater' => true, 'supports_choices' => false, 'supports_children' => false, 'supports_required' => true, 'supports_disable_empty_save' => true, ], 'url' => [ 'label' => __( 'URL', 'admin-optimizer' ), 'supports_default' => true, 'supports_multiple' => false, 'supports_repeater' => true, 'supports_choices' => false, 'supports_children' => false, 'supports_required' => true, 'supports_disable_empty_save' => true, ], 'password' => [ 'label' => __( 'Password', 'admin-optimizer' ), 'supports_default' => true, 'supports_multiple' => false, 'supports_repeater' => true, 'supports_choices' => false, 'supports_children' => false, 'supports_required' => true, 'supports_disable_empty_save' => true, ], 'select' => [ 'label' => __( 'Select', 'admin-optimizer' ), 'supports_default' => false, 'supports_multiple' => true, 'supports_repeater' => true, 'supports_choices' => true, 'supports_children' => false, 'supports_required' => true, 'supports_disable_empty_save' => true, ], 'radio' => [ 'label' => __( 'Radio', 'admin-optimizer' ), 'supports_default' => false, 'supports_multiple' => false, 'supports_repeater' => true, 'supports_choices' => true, 'supports_children' => false, 'supports_required' => true, 'supports_disable_empty_save' => true, ], 'check' => [ 'label' => __( 'Checkbox', 'admin-optimizer' ), 'supports_default' => false, 'supports_multiple' => true, 'supports_repeater' => false, 'supports_choices' => true, 'supports_children' => false, 'supports_required' => true, 'supports_disable_empty_save' => true, ], 'true_false' => [ 'label' => __( 'True / False', 'admin-optimizer' ), 'supports_default' => false, 'supports_multiple' => false, 'supports_repeater' => false, 'supports_choices' => false, 'supports_children' => false, 'supports_required' => true, 'supports_disable_empty_save' => true, ], 'datepicker' => [ 'label' => __( 'Date', 'admin-optimizer' ), 'supports_default' => true, 'supports_multiple' => false, 'supports_repeater' => true, 'supports_choices' => false, 'supports_children' => false, 'supports_required' => true, 'supports_disable_empty_save' => true, ], 'recaptcha' => [ 'label' => __( 'reCAPTCHA', 'admin-optimizer' ), 'supports_default' => false, 'supports_multiple' => false, 'supports_repeater' => false, 'supports_choices' => false, 'supports_children' => false, 'supports_required' => false, 'supports_disable_empty_save' => false, ], 'turnstile' => [ 'label' => __( 'Turnstile', 'admin-optimizer' ), 'supports_default' => false, 'supports_multiple' => false, 'supports_repeater' => false, 'supports_choices' => false, 'supports_children' => false, 'supports_required' => false, 'supports_disable_empty_save' => false, ], 'time_picker' => [ 'label' => __( 'Time', 'admin-optimizer' ), 'supports_default' => true, 'supports_multiple' => false, 'supports_repeater' => true, 'supports_choices' => false, 'supports_children' => false, 'supports_required' => true, 'supports_disable_empty_save' => true, ], 'datetime_picker' => [ 'label' => __( 'Date Time', 'admin-optimizer' ), 'supports_default' => true, 'supports_multiple' => false, 'supports_repeater' => true, 'supports_choices' => false, 'supports_children' => false, 'supports_required' => true, 'supports_disable_empty_save' => true, ], 'colorpicker' => [ 'label' => __( 'Color', 'admin-optimizer' ), 'supports_default' => true, 'supports_multiple' => false, 'supports_repeater' => true, 'supports_choices' => false, 'supports_children' => false, 'supports_required' => true, 'supports_disable_empty_save' => true, ], 'file' => [ 'label' => __( 'File', 'admin-optimizer' ), 'supports_default' => false, 'supports_multiple' => false, 'supports_repeater' => true, 'supports_choices' => false, 'supports_children' => false, 'supports_required' => true, 'supports_disable_empty_save' => true, ], 'image' => [ 'label' => __( 'Image', 'admin-optimizer' ), 'supports_default' => true, 'supports_multiple' => false, 'supports_repeater' => true, 'supports_choices' => false, 'supports_children' => false, 'supports_required' => true, 'supports_disable_empty_save' => true, ], 'relation' => [ 'label' => __( 'Relationship', 'admin-optimizer' ), 'supports_default' => false, 'supports_multiple' => true, 'supports_repeater' => false, 'supports_choices' => false, 'supports_children' => false, 'supports_required' => true, 'supports_disable_empty_save' => true, ], 'taxonomy' => [ 'label' => __( 'Taxonomy', 'admin-optimizer' ), 'supports_default' => false, 'supports_multiple' => true, 'supports_repeater' => false, 'supports_choices' => false, 'supports_children' => false, 'supports_required' => true, 'supports_disable_empty_save' => true, ], 'user_relation' => [ 'label' => __( 'User Relationship', 'admin-optimizer' ), 'supports_default' => false, 'supports_multiple' => true, 'supports_repeater' => false, 'supports_choices' => false, 'supports_children' => false, 'supports_required' => true, 'supports_disable_empty_save' => true, ], 'group' => [ 'label' => __( 'Group', 'admin-optimizer' ), 'supports_default' => false, 'supports_multiple' => false, 'supports_repeater' => false, 'supports_choices' => false, 'supports_children' => true, 'supports_required' => false, 'supports_disable_empty_save' => false, ], 'row' => [ 'label' => __( 'Row', 'admin-optimizer' ), 'supports_default' => false, 'supports_multiple' => false, 'supports_repeater' => false, 'supports_choices' => false, 'supports_children' => true, 'supports_required' => false, 'supports_disable_empty_save' => false, ], 'tab' => [ 'label' => __( 'Tab', 'admin-optimizer' ), 'supports_default' => false, 'supports_multiple' => false, 'supports_repeater' => false, 'supports_choices' => false, 'supports_children' => true, 'supports_required' => false, 'supports_disable_empty_save' => false, ], ]; return $types; } /** * Get field definition for a specific type. * * @param string $type Field type. * * @return array */ public static function get_type( $type ) { $type = sanitize_key( (string) $type ); $types = self::get_types(); return $types[ $type ] ?? $types['text']; } /** * Get supported field type labels. * * @return array */ public static function get_type_labels() { $labels = []; foreach ( self::get_types() as $type => $config ) { $labels[ $type ] = (string) $config['label']; } return $labels; } /** * Get default field definition. * * @return array */ public static function get_default_definition() { static $defaults = null; if ( null !== $defaults ) { return $defaults; } $defaults = [ 'type' => 'text', 'label' => '', 'name' => '', 'description' => '', 'default' => '', 'placeholder' => '', 'html_message' => '', 'choices' => '', 'yes_label' => __( 'Yes', 'admin-optimizer' ), 'text_input_size' => 'regular', 'multiple' => 0, 'repeater' => 0, 'save_as_array' => 0, 'required' => 0, 'disable_save_empty' => 0, 'range_min' => '0', 'range_max' => '100', 'range_step' => '1', 'number_min' => '', 'number_max' => '', 'number_step' => '1', 'relation_post_type' => [ 'post' ], 'relation_post_status' => [ 'publish' ], 'relation_taxonomy' => [ 'category' ], 'width' => 'full', 'show_admin_column' => 0, 'admin_column_label' => '', 'admin_column_display' => 'auto', 'admin_column_trim_length' => 80, 'children' => [], 'settings' => [], ]; return $defaults; } /** * Normalize and sanitize a field definition. * * @param array $field Raw field definition. * * @return array */ public static function sanitize_definition( array $field ) { $defaults = self::get_default_definition(); $field = array_merge( $defaults, $field ); $type = sanitize_key( (string) $field['type'] ); if ( ! self::has_type( $type ) ) { $type = 'text'; } $type_config = self::get_type( $type ); $name = sanitize_key( (string) $field['name'] ); if ( '' === $name ) { $name = sanitize_key( (string) $field['label'] ); } if ( 'row' === $type ) { $name = ''; } $children = []; if ( ! empty( $type_config['supports_children'] ) && ! empty( $field['children'] ) && is_array( $field['children'] ) ) { foreach ( $field['children'] as $child ) { if ( ! is_array( $child ) ) { continue; } $clean_child = self::sanitize_definition( $child ); if ( 'row' === $clean_child['type'] ) { continue; } if ( '' !== $clean_child['name'] || 'html_message' === $clean_child['type'] ) { $children[] = $clean_child; } } } $definition = [ 'type' => $type, 'label' => sanitize_text_field( (string) $field['label'] ), 'name' => $name, 'description' => sanitize_textarea_field( (string) $field['description'] ), 'default' => ! empty( $type_config['supports_default'] ) && is_scalar( $field['default'] ) ? sanitize_text_field( (string) $field['default'] ) : '', 'placeholder' => is_scalar( $field['placeholder'] ) ? sanitize_text_field( (string) $field['placeholder'] ) : '', 'html_message' => 'html_message' === $type ? wp_kses_post( (string) $field['html_message'] ) : '', 'choices' => ! empty( $type_config['supports_choices'] ) ? sanitize_textarea_field( (string) $field['choices'] ) : '', 'yes_label' => sanitize_text_field( (string) $field['yes_label'] ), 'text_input_size' => self::sanitize_text_input_size( $field['text_input_size'] ), 'multiple' => ! empty( $type_config['supports_multiple'] ) && ! empty( $field['multiple'] ) ? 1 : 0, 'repeater' => ! empty( $type_config['supports_repeater'] ) && ! empty( $field['repeater'] ) ? 1 : 0, 'save_as_array' => 'group' === $type && ! empty( $field['save_as_array'] ) ? 1 : 0, 'required' => ! empty( $type_config['supports_required'] ) && ! empty( $field['required'] ) ? 1 : 0, 'disable_save_empty' => ! empty( $type_config['supports_disable_empty_save'] ) && ! empty( $field['disable_save_empty'] ) ? 1 : 0, 'range_min' => sanitize_text_field( (string) $field['range_min'] ), 'range_max' => sanitize_text_field( (string) $field['range_max'] ), 'range_step' => sanitize_text_field( (string) $field['range_step'] ), 'number_min' => sanitize_text_field( (string) $field['number_min'] ), 'number_max' => sanitize_text_field( (string) $field['number_max'] ), 'number_step' => sanitize_text_field( (string) $field['number_step'] ), 'relation_post_type' => self::sanitize_key_list( $field['relation_post_type'] ?? [], [ 'post' ] ), 'relation_post_status' => self::sanitize_key_list( $field['relation_post_status'] ?? [], [ 'publish' ] ), 'relation_taxonomy' => self::sanitize_key_list( $field['relation_taxonomy'] ?? [], [ 'category' ] ), 'width' => self::sanitize_width( $field['width'] ), 'show_admin_column' => self::supports_admin_column( $type ) && ! empty( $field['show_admin_column'] ) ? 1 : 0, 'admin_column_label' => sanitize_text_field( (string) $field['admin_column_label'] ), 'admin_column_display' => self::sanitize_admin_column_display( $field['admin_column_display'] ), 'admin_column_trim_length' => self::sanitize_admin_column_trim_length( $field['admin_column_trim_length'] ), 'children' => $children, 'settings' => 'row' === $type ? self::sanitize_row_settings( $field['settings'] ?? [] ) : [], ]; if ( '' === $definition['label'] ) { $definition['label'] = $definition['name']; } if ( 'file' === $type ) { $definition['default'] = ''; } return $definition; } /** * Whether a field type can be displayed in post list table columns. * * @param string $type Field type. * * @return bool */ public static function supports_admin_column( $type ) { return in_array( sanitize_key( (string) $type ), [ 'text', 'textarea', 'number', 'range', 'email', 'phone', 'url', 'datepicker', 'time_picker', 'datetime_picker', 'colorpicker', 'select', 'radio', 'check', 'true_false' ], true ); } /** * Sanitize admin column display mode. * * @param mixed $display Raw display mode. * * @return string */ public static function sanitize_admin_column_display( $display ) { $display = sanitize_key( (string) $display ); return in_array( $display, [ 'auto', 'plain', 'trimmed' ], true ) ? $display : 'auto'; } /** * Sanitize admin column trim length. * * @param mixed $length Raw trim length. * * @return int */ public static function sanitize_admin_column_trim_length( $length ) { $length = absint( $length ); if ( $length <= 0 ) { $length = 80; } return max( 20, min( 300, $length ) ); } /** * Sanitize visual row layout settings. * * @param mixed $settings Raw row settings. * * @return array */ private static function sanitize_row_settings( $settings ) { $settings = is_array( $settings ) ? $settings : []; $columns = isset( $settings['columns'] ) ? sanitize_key( (string) $settings['columns'] ) : 'auto'; $gap = isset( $settings['gap'] ) ? sanitize_key( (string) $settings['gap'] ) : 'md'; if ( ! in_array( $columns, [ 'auto', '2', '3', '4' ], true ) ) { $columns = 'auto'; } if ( ! in_array( $gap, [ 'sm', 'md', 'lg' ], true ) ) { $gap = 'md'; } return [ 'columns' => $columns, 'gap' => $gap, 'stack_mobile' => array_key_exists( 'stack_mobile', $settings ) ? ( ! empty( $settings['stack_mobile'] ) ? 1 : 0 ) : 1, ]; } /** * Whether a field type exists. * * @param string $type Field type. * * @return bool */ public static function has_type( $type ) { $type = sanitize_key( (string) $type ); return isset( self::get_types()[ $type ] ); } /** * Whether a field type supports children. * * @param string $type Field type. * * @return bool */ public static function supports_children( $type ) { $type_config = self::get_type( $type ); return ! empty( $type_config['supports_children'] ); } /** * Whether a field type supports choices. * * @param string $type Field type. * * @return bool */ public static function supports_choices( $type ) { $type_config = self::get_type( $type ); return ! empty( $type_config['supports_choices'] ); } /** * Whether a field type supports multiple values. * * @param string $type Field type. * * @return bool */ public static function supports_multiple( $type ) { $type_config = self::get_type( $type ); return ! empty( $type_config['supports_multiple'] ); } /** * Whether a field type supports repeaters. * * @param string $type Field type. * * @return bool */ public static function supports_repeater( $type ) { $type_config = self::get_type( $type ); return ! empty( $type_config['supports_repeater'] ); } /** * Whether a field type supports default values. * * @param string $type Field type. * * @return bool */ public static function supports_default( $type ) { $type_config = self::get_type( $type ); return ! empty( $type_config['supports_default'] ); } /** * Whether a field type supports required validation. * * @param string $type Field type. * * @return bool */ public static function supports_required( $type ) { $type_config = self::get_type( $type ); return ! empty( $type_config['supports_required'] ); } /** * Whether a field type supports disable-save-empty behavior. * * @param string $type Field type. * * @return bool */ public static function supports_disable_empty_save( $type ) { $type_config = self::get_type( $type ); return ! empty( $type_config['supports_disable_empty_save'] ); } /** * Sanitize a runtime value for a field definition. * * @param array $field Field definition. * @param mixed $value Raw runtime value. * * @return mixed */ public static function sanitize_runtime_value( array $field, $value ) { $field_type = sanitize_key( (string) ( $field['type'] ?? 'text' ) ); if ( ! empty( $field['repeater'] ) && self::supports_repeater( $field_type ) ) { $rows = is_array( $value ) ? $value : [ $value ]; $out = []; foreach ( $rows as $row_value ) { $single = self::sanitize_single_runtime_value( array_merge( $field, [ 'repeater' => 0 ] ), $row_value ); if ( self::is_effectively_empty_value( array_merge( $field, [ 'repeater' => 0 ] ), $single ) ) { continue; } $out[] = $single; } return $out; } return self::sanitize_single_runtime_value( $field, $value ); } /** * Sanitize a single runtime value for a field definition. * * @param array $field Field definition. * @param mixed $value Raw runtime value. * * @return mixed */ public static function sanitize_single_runtime_value( array $field, $value ) { $field_type = sanitize_key( (string) ( $field['type'] ?? 'text' ) ); $choices = self::parse_choices( $field['choices'] ?? '' ); switch ( $field_type ) { case 'number': case 'range': return is_numeric( $value ) ? (string) $value : ''; case 'email': return sanitize_email( is_scalar( $value ) ? (string) $value : '' ); case 'phone': return sanitize_text_field( is_scalar( $value ) ? (string) $value : '' ); case 'url': case 'file': case 'image': return esc_url_raw( is_scalar( $value ) ? (string) $value : '' ); case 'textarea': case 'code': return sanitize_textarea_field( is_scalar( $value ) ? (string) $value : '' ); case 'html': case 'wysiwyg': return wp_kses_post( is_scalar( $value ) ? (string) $value : '' ); case 'html_message': case 'recaptcha': case 'turnstile': return ''; case 'check': if ( empty( $choices ) ) { return empty( $value ) ? '0' : '1'; } $values = self::normalize_to_array( $value ); return array_values( array_filter( $values, static function ( $item ) use ( $choices ) { return isset( $choices[ $item ] ); } ) ); case 'true_false': return empty( $value ) ? '0' : '1'; case 'select': if ( ! empty( $field['multiple'] ) ) { $values = self::normalize_to_array( $value ); return array_values( array_filter( $values, static function ( $item ) use ( $choices ) { return isset( $choices[ $item ] ); } ) ); } $value = sanitize_text_field( is_scalar( $value ) ? (string) $value : '' ); return isset( $choices[ $value ] ) ? $value : ''; case 'radio': $value = sanitize_text_field( is_scalar( $value ) ? (string) $value : '' ); return isset( $choices[ $value ] ) ? $value : ''; case 'relation': case 'taxonomy': case 'user_relation': if ( ! empty( $field['multiple'] ) ) { return array_values( array_filter( array_map( 'absint', self::normalize_to_array( $value ) ) ) ); } return (string) absint( is_scalar( $value ) ? $value : 0 ); case 'datepicker': case 'time_picker': case 'datetime_picker': case 'colorpicker': case 'password': case 'text': default: return sanitize_text_field( is_scalar( $value ) ? (string) $value : '' ); } } /** * Determine if a runtime value should be treated as empty. * * @param mixed $value Runtime value. * * @return bool */ public static function is_empty_runtime_value( $value ) { if ( is_array( $value ) ) { return empty( $value ); } if ( null === $value ) { return true; } return '' === (string) $value; } /** * Determine if a field-aware runtime value should be treated as empty. * * @param array $field Field definition. * @param mixed $value Sanitized runtime value. * * @return bool */ public static function is_effectively_empty_value( array $field, $value ) { $type = sanitize_key( (string) ( $field['type'] ?? 'text' ) ); if ( in_array( $type, [ 'check', 'true_false' ], true ) ) { if ( is_array( $value ) ) { return empty( $value ); } return in_array( (string) $value, [ '', '0' ], true ); } return self::is_empty_runtime_value( $value ); } /** * Parse a textarea choices string into value => label pairs. * * @param string $raw_choices Raw choices. * * @return array */ public static function parse_choices( $raw_choices ) { $choices = []; $lines = preg_split( '/\r\n|\r|\n/', (string) $raw_choices ); $lines = is_array( $lines ) ? $lines : []; foreach ( $lines as $line ) { $line = trim( (string) $line ); if ( '' === $line ) { continue; } $parts = array_map( 'trim', explode( '|', $line, 2 ) ); $value = sanitize_text_field( (string) ( $parts[0] ?? '' ) ); $label = sanitize_text_field( (string) ( $parts[1] ?? $value ) ); if ( '' !== $value ) { $choices[ $value ] = '' !== $label ? $label : $value; } } return $choices; } /** * Normalize a runtime value to an array of scalar strings. * * @param mixed $value Raw value. * * @return array */ public static function normalize_to_array( $value ) { $values = is_array( $value ) ? $value : [ $value ]; return array_values( array_filter( array_map( static function ( $item ) { return is_scalar( $item ) ? (string) $item : ''; }, $values ), 'strlen' ) ); } /** * Sanitize a list of keys. * * @param mixed $values List of values. * @param array $fallback Fallback values. * * @return array */ public static function sanitize_key_list( $values, array $fallback = [] ) { if ( ! is_array( $values ) ) { $values = [ $values ]; } $values = array_values( array_unique( array_filter( array_map( 'sanitize_key', $values ) ) ) ); return empty( $values ) ? $fallback : $values; } /** * Sanitize layout width. * * @param mixed $width Raw width. * * @return string */ public static function sanitize_width( $width ) { $width = sanitize_key( (string) $width ); if ( ! in_array( $width, [ 'full', 'three_quarter', 'half', 'quarter' ], true ) ) { return 'full'; } return $width; } /** * Sanitize text input size. * * @param mixed $size Raw size. * * @return string */ public static function sanitize_text_input_size( $size ) { $size = sanitize_key( (string) $size ); if ( ! in_array( $size, [ 'regular', 'large' ], true ) ) { return 'regular'; } return $size; } }