', '<' ); public static function boot() { add_filter( 'customfields_validate_field_schema', array( __CLASS__, 'validate_field' ), 5, 3 ); add_filter( 'customfields_validate_group_schema', array( __CLASS__, 'validate_group' ), 5, 2 ); add_filter( 'customfields_field_is_active', array( __CLASS__, 'is_active' ), 10, 4 ); add_action( 'customfields_after_render_field', array( __CLASS__, 'render' ), 10, 3 ); add_action( 'customfields_field_editor_settings', array( __CLASS__, 'editor' ), 5, 2 ); add_action( 'admin_enqueue_scripts', array( __CLASS__, 'assets' ) ); } public static function assets() { $screen = get_current_screen(); if ( ! $screen || ! ( in_array( $screen->base, array( 'post', 'profile', 'user-edit', 'edit-tags', 'term' ), true ) || false !== strpos( $screen->id, 'customfields' ) ) ) { return; } wp_enqueue_script( 'customfields-conditions', CUSTOMFIELDS_URL . 'assets/conditions.js', array('wp-i18n'), CUSTOMFIELDS_VERSION, true ); wp_set_script_translations('customfields-conditions', 'customfields-com', CUSTOMFIELDS_PATH . 'languages'); if ( current_user_can( 'upload_files' ) ) { wp_enqueue_media(); } } public static function validate_field( $field, $raw, $depth = 0 ) { if ( is_wp_error( $field ) ) { return $field; } $rules = $raw['conditional_logic'] ?? array(); if ( is_string( $rules ) ) { $rules = '' === trim( $rules ) ? array() : json_decode( $rules, true ); } if ( ! is_array( $rules ) || count( $rules ) > 20 ) { return new \WP_Error( 'conditions', __( 'Use at most 20 visibility rules.', 'customfields-com' ) ); } $field['conditional_logic'] = array(); foreach ( $rules as $rule ) { if ( ! is_array( $rule ) || ! isset( $rule['field'], $rule['operator'] ) || ! is_string( $rule['field'] ) || ! preg_match( '/^[a-z][a-z0-9_]{0,63}$/D', $rule['field'] ) || ! in_array( $rule['operator'], self::OPERATORS, true ) || ! is_scalar( $rule['value'] ?? '' ) || strlen( (string) ( $rule['value'] ?? '' ) ) > 1000 ) { return new \WP_Error( 'conditions', __( 'Each visibility rule needs a field name, a supported operator, and a simple value.', 'customfields-com' ) ); } if ( $rule['field'] === $field['name'] ) { return new \WP_Error( 'conditions_self', __( 'A field cannot control its own visibility.', 'customfields-com' ) ); } if ( in_array( $rule['operator'], array( '>', '<' ), true ) && ( ! is_numeric( $rule['value'] ?? '' ) || ! is_finite( (float) $rule['value'] ) ) ) { return new \WP_Error( 'conditions_number', __( 'Greater/less than rules require a finite numeric value.', 'customfields-com' ) ); } $field['conditional_logic'][] = array( 'field' => $rule['field'], 'operator' => $rule['operator'], 'value' => (string) ( $rule['value'] ?? '' ) ); } $relation = $raw['condition_relation'] ?? 'all'; if ( ! in_array( $relation, array( 'all', 'any' ), true ) ) { return new \WP_Error( 'conditions_relation', __( 'Visibility must match all or any rules.', 'customfields-com' ) ); } $field['condition_relation'] = $relation; return $field; } public static function validate_group( $group, $raw ) { if ( is_wp_error( $group ) ) { return $group; } $valid = self::validate_fields( $group['fields'] ); return is_wp_error( $valid ) ? $valid : $group; } public static function validate_fields( $fields ) { $by_name = array_column( $fields, null, 'name' ); foreach ( $fields as $field ) { foreach ( $field['conditional_logic'] ?? array() as $rule ) { if ( ! isset( $by_name[$rule['field']] ) || ! in_array( $by_name[$rule['field']]['type'], Fields::CORE_TYPES, true ) ) { return new \WP_Error( 'conditions_reference', __( 'Visibility rules must reference a basic field in the same group.', 'customfields-com' ) ); } } } $visited = array(); foreach ( $fields as $field ) { if ( self::cycle( $field['name'], $by_name, $visited ) ) { return new \WP_Error( 'conditions_cycle', __( 'Visibility rules cannot form a dependency cycle.', 'customfields-com' ) ); } } return true; } private static function cycle( $name, $fields, &$visited ) { if ( isset( $visited[$name] ) ) { return 1 === $visited[$name]; } $visited[$name] = 1; foreach ( $fields[$name]['conditional_logic'] ?? array() as $rule ) { if ( self::cycle( $rule['field'], $fields, $visited ) ) { return true; } } $visited[$name] = 2; return false; } /** Resolve controllers first so visibility follows sanitized or retained values, never discarded input. */ public static function resolve_values( $fields, $submitted, $stored, $validator, $object_id = 0 ) { $by_name = array_column( $fields, null, 'name' ); $values = $submitted; $updates = array(); $resolved = array(); $visiting = array(); $errors = new \WP_Error(); $resolve = function ( $name ) use ( &$resolve, &$values, &$updates, &$resolved, &$visiting, $by_name, $stored, $validator, $object_id, $errors ) { if ( isset( $resolved[$name] ) ) { return; } if ( ! isset( $by_name[$name] ) ) { unset( $values[$name] ); return; } if ( isset( $visiting[$name] ) ) { $errors->add( $name, __( 'Visibility rules contain an invalid dependency.', 'customfields-com' ) ); unset( $values[$name] ); return; } $visiting[$name] = true; $field = $by_name[$name]; foreach ( $field['conditional_logic'] ?? array() as $rule ) { $resolve( $rule['field'] ); } $condition_field = $field; $condition_field['_customfields_controller_types'] = array_column( $by_name, 'type', 'name' ); if ( ! apply_filters( 'customfields_field_is_active', true, $condition_field, $values, $object_id ) ) { $values[$name] = array_key_exists( $name, $stored ) ? $stored[$name] : ( $field['default'] ?? '' ); } else { $clean = $validator( $field ); if ( is_wp_error( $clean ) ) { $errors->add( $name, $field['label'] . ': ' . $clean->get_error_message() ); unset( $values[$name] ); } else { $updates[$name] = $clean; $values[$name] = $clean; } } unset( $visiting[$name] ); $resolved[$name] = true; }; foreach ( array_keys( $by_name ) as $name ) { $resolve( $name ); } if ( $errors->has_errors() ) { return $errors; } $ordered = array(); foreach ( array_keys( $by_name ) as $name ) { if ( array_key_exists( $name, $updates ) ) { $ordered[$name] = $updates[$name]; } } return $ordered; } public static function is_active( $active, $field, $values, $object_id = 0, $controller_types = array() ) { if ( ! $active || empty( $field['conditional_logic'] ) || ! is_array( $values ) ) { return $active; } $controller_types = $field['_customfields_controller_types'] ?? $controller_types; // Missing or malformed controller values leave fields visible: uncertainty never suppresses validation. foreach ( $field['conditional_logic'] as $rule ) { if ( ! array_key_exists( $rule['field'], $values ) || ( ! is_scalar( $values[$rule['field']] ) && ! is_array( $values[$rule['field']] ) ) ) { return true; } if ( is_array( $values[$rule['field']] ) ) { foreach ( $values[$rule['field']] as $key => $v ) { if ( '__present' !== $key && ( ! ctype_digit( (string) $key ) || ! is_scalar( $v ) ) ) { return true; } } } } $any = 'any' === ( $field['condition_relation'] ?? 'all' ); foreach ( $field['conditional_logic'] as $rule ) { $value = $values[$rule['field']]; $numeric = 'number' === ( $controller_types[$rule['field']] ?? '' ) || is_float( $value ); $number = $numeric && is_scalar( $value ) && '' !== (string) $value && is_numeric( $value ) && is_finite( (float) $value ) ? (float) $value : null; if ( null !== $number ) { $value = $number; } if ( is_array( $value ) ) { unset( $value['__present'] ); $value = array_map( 'strval', array_values( $value ) ); } else { $value = is_bool( $value ) ? ( $value ? '1' : '0' ) : (string) $value; } $empty = array() === $value || '' === $value; $equal = is_array( $value ) ? in_array( $rule['value'], $value, true ) : $value === $rule['value']; if ( null !== $number && is_numeric( $rule['value'] ) && is_finite( (float) $rule['value'] ) ) { $equal = $number === (float) $rule['value']; } switch ( $rule['operator'] ) { case '!=': $match = ! $equal; break; case 'empty': $match = $empty; break; case '!empty': $match = ! $empty; break; case 'contains': $match = is_array( $value ) ? $equal : false !== strpos( $value, $rule['value'] ); break; case '>': case '<': $match = ! is_array( $value ) && is_numeric( $value ) && is_finite( (float) $value ) && ( '>' === $rule['operator'] ? (float) $value > (float) $rule['value'] : (float) $value < (float) $rule['value'] ); break; default: $match = $equal; } if ( $any && $match ) { return true; } if ( ! $any && ! $match ) { return false; } } return ! $any; } public static function render( $field, $value, $input_name ) { if ( in_array( $field['type'], Fields::CORE_TYPES, true ) ) { // Options recovery can render unsaved input; its condition baseline must remain stored data. if ( 'customfields_pro_values[' . $field['name'] . ']' === $input_name ) { $options = get_option( 'customfields_pro_values', array() ); $value = is_array( $options ) && array_key_exists( $field['name'], $options ) ? $options[$field['name']] : ( $field['default'] ?? '' ); } echo ''; } if ( ! empty( $field['conditional_logic'] ) ) { echo ''; } } /** Return only the permitted basic-field values from this completed options write. */ public static function option_values( $groups, $values ) { $saved = array(); foreach ( $groups as $group ) { foreach ( $group['fields'] as $field ) { if ( in_array( $field['type'], Fields::CORE_TYPES, true ) && array_key_exists( $field['name'], $values ) ) { $saved[$field['name']] = $values[$field['name']]; } } } return $saved; } public static function editor( $field, $prefix ) { echo '
' . esc_html__( 'Conditional visibility', 'customfields-com' ) . '

'; foreach ( $field['conditional_logic'] ?? array() as $i => $rule ) { self::rule_editor( $rule, $prefix . '[conditional_logic][' . $i . ']' ); } echo '

' . esc_html__( 'Use field names from this group. Hidden fields retain stored values. Visibility does not control access.', 'customfields-com' ) . '

'; } private static function rule_editor( $rule, $prefix ) { echo '
'; } }