'; KCONV_Pro_Discovery::hint( 'logic', true ); echo ''; } }, 46 ); } public static function comparison_schema( $public ) { // Older WordPress releases normalize escaped numeric entities in a different order. $public['condition_text_legacy_entities'] = '< A' === sanitize_text_field( '< &#65;' ); return $public; } public static function has_fields( $form ) { foreach ( $form['fields'] as $field ) { if ( ! empty( $field['condition'] ) ) { return true; } } return false; } public static function sanitize( $schema, $raw ) { if ( is_wp_error( $schema ) ) { return $schema; } $previous = array(); foreach ( $schema['fields'] as $index => &$field ) { $condition = isset( $raw['fields'][ $index ]['condition'] ) ? $raw['fields'][ $index ]['condition'] : null; if ( null !== $condition ) { if ( 'hidden' === $field['type'] || ! is_array( $condition ) || ! isset( $condition['source'], $condition['operator'], $condition['value'] ) || ! is_string( $condition['source'] ) || ! is_string( $condition['operator'] ) || ! is_string( $condition['value'] ) || KCONV_Field_Rules::length( $condition['value'] ) > 200 || ! isset( $previous[ $condition['source'] ] ) ) { return new WP_Error( 'kconv_condition', __( 'Each basic condition needs an earlier source field, a comparison and a value of at most 200 characters.', 'killerconversions' ) ); } $source = $previous[ $condition['source'] ]; $type = $source['type']; $operators = 'checkbox_group' === $type ? array( 'contains', 'not_contains' ) : array( 'equals', 'not_equals' ); $value = wp_check_invalid_utf8( trim( $condition['value'] ) ); if ( in_array( $type, array( 'file', 'calculated' ), true ) || ! in_array( $condition['operator'], $operators, true ) ) { return new WP_Error( 'kconv_condition', __( 'Choose a supported source and basic comparison. Files and calculated totals cannot control conditions.', 'killerconversions' ) ); } if ( in_array( $type, array( 'select', 'radio', 'checkbox_group' ), true ) && ( '' !== $value || 'checkbox_group' === $type ) && ! in_array( $value, $source['options'], true ) ) { return new WP_Error( 'kconv_condition', __( 'The condition value must match an option in the source field.', 'killerconversions' ) ); } if ( 'checkbox' === $type && ! in_array( $value, array( '', '1' ), true ) ) { return new WP_Error( 'kconv_condition', __( 'Checkbox conditions use 1 for checked or an empty value for unchecked.', 'killerconversions' ) ); } if ( '' !== $value && ( ( 'number' === $type && ( ! is_numeric( $value ) || ! is_finite( (float) $value ) ) ) || ( 'date' === $type && ! KCONV_Core::valid_date( $value ) ) || ( 'time' === $type && ! preg_match( '/^(?:[01]\d|2[0-3]):[0-5]\d$/D', $value ) ) ) ) { return new WP_Error( 'kconv_condition', __( 'Use a valid number, date or time for this condition.', 'killerconversions' ) ); } $field['condition'] = array( 'source' => $source['id'], 'operator' => $condition['operator'], 'value' => $value ); } $previous[ $field['id'] ] = $field; } unset( $field ); return $schema; } /** Exact decimal order for the bounded numeric strings accepted by form questions. */ public static function compare_numbers( $left, $right ) { $parts = array(); foreach ( array( $left, $right ) as $value ) { if ( ! is_scalar( $value ) || is_bool( $value ) ) { return null; } $value = trim( (string) $value ); if ( strlen( $value ) > 1000 || ! preg_match( '/^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/D', $value ) || ! is_finite( (float) $value ) ) { return null; } $negative = '-' === substr( $value, 0, 1 ); $scientific = preg_split( '/[eE]/', ltrim( $value, '+-' ), 2 ); $decimal = explode( '.', $scientific[0], 2 ); $fraction = $decimal[1] ?? ''; $digits = ltrim( $decimal[0] . $fraction, '0' ); $parts[] = array( 'negative' => $negative && '' !== $digits, 'zero' => '' === $digits, 'position' => self::shift_exponent( $scientific[1] ?? '0', strlen( $digits ) - strlen( $fraction ) ), 'digits' => rtrim( $digits, '0' ) ); } if ( $parts[0]['negative'] !== $parts[1]['negative'] ) { return $parts[0]['negative'] ? -1 : 1; } if ( $parts[0]['zero'] || $parts[1]['zero'] ) { return $parts[0]['zero'] === $parts[1]['zero'] ? 0 : ( $parts[0]['zero'] ? -1 : 1 ); } $comparison = self::compare_exponents( $parts[0]['position'], $parts[1]['position'] ); if ( 0 === $comparison ) { $length = max( strlen( $parts[0]['digits'] ), strlen( $parts[1]['digits'] ) ); $comparison = strcmp( str_pad( $parts[0]['digits'], $length, '0' ), str_pad( $parts[1]['digits'], $length, '0' ) ); } return ( $parts[0]['negative'] ? -$comparison : $comparison ) <=> 0; } private static function compare_exponents( $left, $right ) { $negative_left = '-' === substr( $left, 0, 1 ); $negative_right = '-' === substr( $right, 0, 1 ); if ( $negative_left !== $negative_right ) { return $negative_left ? -1 : 1; } $left = ltrim( $left, '-' ); $right = ltrim( $right, '-' ); $comparison = strlen( $left ) <=> strlen( $right ); if ( 0 === $comparison ) { $comparison = strcmp( $left, $right ) <=> 0; } return $negative_left ? -$comparison : $comparison; } /** Add a small decimal-point shift to an exponent without expanding its power of ten. */ private static function shift_exponent( $value, $shift ) { $negative = '-' === substr( $value, 0, 1 ); $digits = ltrim( ltrim( $value, '+-' ), '0' ); if ( '' === $digits ) { $digits = '0'; $negative = false; } $other = (string) abs( $shift ); $subtract = $negative !== ( $shift < 0 ); if ( $subtract ) { $comparison = strlen( $digits ) <=> strlen( $other ); if ( 0 === $comparison ) { $comparison = strcmp( $digits, $other ); } if ( $comparison < 0 ) { $swap = $digits; $digits = $other; $other = $swap; $negative = $shift < 0; } } $result = ''; $carry = 0; $a = strlen( $digits ) - 1; $b = strlen( $other ) - 1; while ( $a >= 0 || $b >= 0 ) { $one = $a >= 0 ? ord( $digits[ $a-- ] ) - 48 : 0; $two = $b >= 0 ? ord( $other[ $b-- ] ) - 48 : 0; $digit = $subtract ? $one - $two - $carry : $one + $two + $carry; $carry = $subtract ? ( $digit < 0 ? 1 : 0 ) : ( $digit >= 10 ? 1 : 0 ); $result = (string) ( ( $digit + 10 ) % 10 ) . $result; } if ( ! $subtract && $carry ) { $result = '1' . $result; } $result = ltrim( $result, '0' ); if ( '' === $result ) { return '0'; } return ( $negative ? '-' : '' ) . $result; } /** Exact step alignment, with bounded decimal arithmetic and no float division. */ public static function step_matches( $value, $base, $step ) { $parts = array(); foreach ( array( $value, $base, $step ) as $index => $input ) { if ( ! is_scalar( $input ) || is_bool( $input ) ) { return false; } $input = trim( (string) $input ); if ( strlen( $input ) > ( 0 === $index ? 1000 : 40 ) || ! preg_match( '/^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/D', $input ) || ! is_finite( (float) $input ) ) { return false; } // Keep the existing configured-step policy: a finite, representably positive step. if ( 2 === $index && (float) $input <= 0 ) { return false; } $scientific = preg_split( '/[eE]/', ltrim( $input, '+-' ), 2 ); $decimal = explode( '.', $scientific[0], 2 ); $fraction = $decimal[1] ?? ''; $digits = ltrim( $decimal[0] . $fraction, '0' ); $trimmed = rtrim( $digits, '0' ); $parts[] = array( 'negative' => '-' === substr( $input, 0, 1 ) && '' !== $digits, 'digits' => '' === $digits ? '0' : $trimmed, 'exponent' => '' === $digits ? '0' : self::shift_exponent( $scientific[1] ?? '0', strlen( $digits ) - strlen( $trimmed ) - strlen( $fraction ) ) ); } if ( 0 === self::compare_numbers( $value, $base ) ) { return true; } foreach ( $parts as $part ) { // At most 1000 significand digits and a positive finite <=40-character step: // a nonzero operand below this scale is smaller than every valid step. // Its low digit cannot cancel a bounded-scale operand; equal operands were handled above. if ( self::compare_exponents( $part['exponent'], '-2048' ) < 0 || self::compare_exponents( $part['exponent'], '309' ) > 0 ) { return false; } } $scale = min( (int) $parts[0]['exponent'], (int) $parts[1]['exponent'], (int) $parts[2]['exponent'] ); $left = $parts[0]['digits'] . str_repeat( '0', (int) $parts[0]['exponent'] - $scale ); $right = $parts[1]['digits'] . str_repeat( '0', (int) $parts[1]['exponent'] - $scale ); $left = ltrim( $left, '0' ) ?: '0'; $right = ltrim( $right, '0' ) ?: '0'; if ( $parts[0]['negative'] !== $parts[1]['negative'] ) { $difference = self::add_digits( $left, $right ); } elseif ( self::compare_exponents( $left, $right ) >= 0 ) { $difference = self::subtract_digits( $left, $right ); } else { $difference = self::subtract_digits( $right, $left ); } $zeros = (int) $parts[2]['exponent'] - $scale; if ( $zeros ) { if ( strlen( $difference ) <= $zeros || substr( $difference, -$zeros ) !== str_repeat( '0', $zeros ) ) { return false; } $difference = substr( $difference, 0, -$zeros ); } // After removing powers of ten, the divisor has at most 40 digits. $divisor = $parts[2]['digits']; $remainder = '0'; for ( $i = 0, $length = strlen( $difference ); $i < $length; $i++ ) { $remainder = ltrim( $remainder . $difference[ $i ], '0' ) ?: '0'; while ( self::compare_exponents( $remainder, $divisor ) >= 0 ) { $remainder = self::subtract_digits( $remainder, $divisor ); } } return '0' === $remainder; } private static function add_digits( $left, $right ) { $result = ''; $carry = 0; $a = strlen( $left ) - 1; $b = strlen( $right ) - 1; while ( $a >= 0 || $b >= 0 || $carry ) { $sum = ( $a >= 0 ? ord( $left[ $a-- ] ) - 48 : 0 ) + ( $b >= 0 ? ord( $right[ $b-- ] ) - 48 : 0 ) + $carry; $result = (string) ( $sum % 10 ) . $result; $carry = (int) floor( $sum / 10 ); } return $result; } private static function subtract_digits( $left, $right ) { $result = ''; $borrow = 0; $a = strlen( $left ) - 1; $b = strlen( $right ) - 1; while ( $a >= 0 ) { $digit = ord( $left[ $a-- ] ) - 48 - ( $b >= 0 ? ord( $right[ $b-- ] ) - 48 : 0 ) - $borrow; $borrow = $digit < 0 ? 1 : 0; $result = (string) ( ( $digit + 10 ) % 10 ) . $result; } return ltrim( $result, '0' ) ?: '0'; } public static function matches( $condition, $field, $raw ) { list( $clean, $errors ) = KCONV_Core::validate_values( array( array_merge( $field, array( 'required' => false ) ) ), array( $field['id'] => $raw ) ); if ( $errors ) { return false; } $value = $clean[ $field['id'] ]; $expected = $condition['value']; $op = $condition['operator']; // Conditions use the same accepted answer that the entry stores. if ( in_array( $op, array( 'contains', 'not_contains' ), true ) ) { if ( ! is_array( $value ) ) { return false; } $hit = in_array( $expected, $value, true ); return 'contains' === $op ? $hit : ! $hit; } if ( ! in_array( $op, array( 'equals', 'not_equals' ), true ) ) { return false; } $hit = 'number' === $field['type'] && '' !== $value && '' !== $expected ? 0 === self::compare_numbers( $value, $expected ) : $value === $expected; return 'equals' === $op ? $hit : ! $hit; } public static function active_fields( $fields, $raw, $form ) { $active = array(); $visible = array(); foreach ( $fields as $field ) { $match = function ( $condition ) use ( &$visible, $raw ) { $source = $condition['source']; return isset( $visible[ $source ] ) && self::matches( $condition, $visible[ $source ], isset( $raw[ $source ] ) ? $raw[ $source ] : '' ); }; $show = empty( $field['condition'] ) || $match( $field['condition'] ); if ( $show ) { $active[] = $field; $visible[ $field['id'] ] = $field; } } return $active; } public static function render_field( $html, $field, $form, $instance ) { if ( empty( $field['condition'] ) ) { return $html; } $processor = new WP_HTML_Tag_Processor( $html ); while ( $processor->next_tag() ) { if ( in_array( $processor->get_tag(), array( 'INPUT', 'SELECT', 'TEXTAREA' ), true ) ) { $processor->remove_attribute( 'required' ); $processor->remove_attribute( 'aria-required' ); } } return $processor->get_updated_html(); } public static function render_form( $html, $form, $instance ) { if ( ! self::has_fields( $form ) ) { return $html; } if ( false !== strpos( $html, '