$field ) { $block_name = $field['blockName'] ?? ''; $attrs = $field['attrs'] ?? array(); $value = $form_data[ $field_id ] ?? ''; // Skip non-input blocks (submit, recaptcha, etc.) if ( ! $this->is_input_block( $block_name ) ) { continue; } // Required check applies to all input blocks. if ( ! empty( $attrs['required'] ) && $this->is_empty( $value, $block_name ) ) { $errors[ $field_id ] = sprintf( /* translators: %s: field label */ __( '%s is required.', '99forms' ), esc_html( $attrs['fieldLabel'] ?? $field_id ) ); // Skip type-specific rules when field is empty — no point validating format of nothing. continue; } // Skip further checks if value is empty and field is not required. if ( $this->is_empty( $value, $block_name ) ) { continue; } $error = $this->validate_field( $block_name, $attrs, $value ); if ( null !== $error ) { $errors[ $field_id ] = $error; } } if ( ! empty( $errors ) ) { wp_send_json_error( array( 'errors' => $errors ), 422 ); exit; } } /** * Dispatch to the right type-specific validator. * Returns an error string, or null if valid. */ private function validate_field( string $block_name, array $attrs, string $value ): ?string { switch ( $block_name ) { case 'nnforms/input': return $this->validate_input( $attrs, $value ); case 'nnforms/select': case 'nnforms/radio': return $this->validate_option( $attrs, $value ); case 'nnforms/checkbox': return $this->validate_checkbox( $attrs, $value ); default: return null; } } /** * Validate nnforms/input based on its inputType attribute. */ private function validate_input( array $attrs, string $value ): ?string { $label = esc_html( $attrs['fieldLabel'] ?? '' ); $input_type = $attrs['inputType'] ?? 'text'; switch ( $input_type ) { case 'email': if ( ! is_email( $value ) ) { return sprintf( /* translators: %s: field label */ __( '%s must be a valid email address.', '99forms' ), $label ); } break; case 'url': if ( ! wp_http_validate_url( $value ) ) { return sprintf( /* translators: %s: field label */ __( '%s must be a valid URL.', '99forms' ), $label ); } break; case 'tel': if ( ! preg_match( '/^[0-9\s\+\-\(\)]+$/', $value ) ) { return sprintf( /* translators: %s: field label */ __( '%s must be a valid phone number.', '99forms' ), $label ); } break; case 'number': if ( ! is_numeric( $value ) ) { return sprintf( /* translators: %s: field label */ __( '%s must be a number.', '99forms' ), $label ); } $min = $attrs['minValue'] ?? ''; $max = $attrs['maxValue'] ?? ''; if ( '' !== $min && is_numeric( $min ) && (float) $value < (float) $min ) { return sprintf( /* translators: 1: field label, 2: minimum value */ __( '%1$s must be at least %2$s.', '99forms' ), $label, $min ); } if ( '' !== $max && is_numeric( $max ) && (float) $value > (float) $max ) { return sprintf( /* translators: 1: field label, 2: maximum value */ __( '%1$s must be no greater than %2$s.', '99forms' ), $label, $max ); } break; case 'date': $date = \DateTime::createFromFormat( 'Y-m-d', $value ); if ( ! $date || $date->format( 'Y-m-d' ) !== $value ) { return sprintf( /* translators: %s: field label */ __( '%s must be a valid date (YYYY-MM-DD).', '99forms' ), $label ); } break; } return null; } /** * Validate nnforms/select and nnforms/radio. * The submitted value must be one of the options configured in the block. */ private function validate_option( array $attrs, string $value ): ?string { $label = esc_html( $attrs['fieldLabel'] ?? '' ); $raw_options = $attrs['options'] ?? ''; $allowed_options = BlockHelper::parse_radio_options( $raw_options ); if ( empty( $allowed_options ) ) { return null; } if ( ! array_key_exists( $value, $allowed_options ) ) { return sprintf( /* translators: %s: field label */ __( '%s contains an invalid selection.', '99forms' ), $label ); } return null; } /** * Validate nnforms/checkbox. * A required checkbox must have value 'on' (standard browser submit value). */ private function validate_checkbox( array $attrs, string $value ): ?string { if ( ! empty( $attrs['required'] ) && 'on' !== $value ) { return sprintf( /* translators: %s: field label */ __( '%s must be checked.', '99forms' ), esc_html( $attrs['fieldLabel'] ?? '' ) ); } return null; } /** * Blocks that carry user-submitted values and need validation. * Excludes structural blocks: submit, recaptcha, form wrapper. */ private function is_input_block( string $block_name ): bool { return in_array( $block_name, array( 'nnforms/input', 'nnforms/textarea', 'nnforms/select', 'nnforms/radio', 'nnforms/checkbox', 'nnforms/country', 'nnforms/file-upload', ), true ); } /** * Whether a submitted value should be treated as empty. * * File-upload fields are not in form_data — they live in $_FILES and are * handled by handle_uploads(). Required file checks belong there, not here. */ private function is_empty( string $value, string $block_name ): bool { if ( 'nnforms/file-upload' === $block_name ) { return false; } return '' === trim( $value ); } }