$row ) { if ( ! is_array( $row ) ) continue; $id = sanitize_key( (string) ( $row['field_id'] ?? '' ) ); if ( '' === $id ) { $id = 'field_' . ( $i + 1 ); } if ( isset( $fields[ $id ] ) ) { $id .= '_' . ( $i + 1 ); } while ( isset( $fields[ $id ] ) ) { $id .= '_x'; } $type = sanitize_key( (string) ( $row['field_type'] ?? 'text' ) ); if ( ! in_array( $type, self::FIELD_TYPES, true ) ) { $type = 'text'; } $label = sanitize_text_field( (string) ( $row['field_label'] ?? '' ) ); $options = array_map( 'trim', explode( "\n", str_replace( "\r", '', (string) ( $row['field_options'] ?? '' ) ) ) ); $options = array_values( array_filter( $options, 'strlen' ) ); $hidden_source = sanitize_key( (string) ( $row['hidden_source'] ?? 'static' ) ); if ( ! in_array( $hidden_source, self::HIDDEN_SOURCES, true ) ) { $hidden_source = 'static'; } $fields[ $id ] = [ 'id' => $id, 'type' => $type, 'label' => '' !== $label ? $label : $id, // Hidden fields and display blocks can never be filled in by the visitor. 'required' => 'yes' === ( $row['field_required'] ?? '' ) && ! in_array( $type, [ 'hidden', 'html' ], true ), 'default' => sanitize_text_field( (string) ( $row['field_default'] ?? '' ) ), 'hidden_source' => $hidden_source, 'hidden_param' => sanitize_key( (string) ( $row['hidden_param'] ?? '' ) ), 'date_min' => self::valid_date( substr( (string) ( $row['date_min'] ?? '' ), 0, 10 ) ), 'date_max' => self::valid_date( substr( (string) ( $row['date_max'] ?? '' ), 0, 10 ) ), 'options' => $options, 'allowed_types' => self::parse_extensions( (string) ( $row['allowed_types'] ?? '' ) ), 'cond_enabled' => 'yes' === ( $row['condition_enabled'] ?? '' ), 'cond_field' => sanitize_key( (string) ( $row['condition_field'] ?? '' ) ), 'cond_op' => sanitize_key( (string) ( $row['condition_operator'] ?? 'equals' ) ), 'cond_value' => (string) ( $row['condition_value'] ?? '' ), 'row' => $row, ]; } return $fields; } /** * Active CAPTCHA configuration for a form, or null when CAPTCHA is off. * CAPTCHA is only enforced when the widget enables it AND the global * settings contain a provider, a site key and a secret key — the same * rule the renderer uses to decide whether to print the CAPTCHA. */ public static function captcha_config( array $widget_settings ): ?array { if ( 'yes' !== ( $widget_settings['enable_captcha'] ?? '' ) ) return null; if ( ! class_exists( 'EPPRO_Admin_Settings' ) ) return null; $s = EPPRO_Admin_Settings::get_settings(); $type = (string) ( $s['captcha_type'] ?? 'none' ); $site = trim( (string) ( $s['captcha_site_key'] ?? '' ) ); $secret = trim( (string) ( $s['captcha_secret'] ?? '' ) ); if ( ! in_array( $type, [ 'recaptcha_v2', 'recaptcha_v3', 'hcaptcha' ], true ) ) return null; if ( '' === $site || '' === $secret ) return null; return [ 'type' => $type, 'site_key' => $site, 'secret' => $secret ]; } /** "YYYY-MM-DD" when $value is a real calendar date, otherwise ''. */ public static function valid_date( string $value ): string { $value = trim( $value ); if ( ! preg_match( '/^(\d{4})-(\d{2})-(\d{2})$/', $value, $m ) ) return ''; return checkdate( (int) $m[2], (int) $m[3], (int) $m[1] ) ? $value : ''; } /** A phone number: digits with optional + ( ) - . and spaces, 6–15 digits. */ public static function valid_phone( string $value ): bool { if ( ! preg_match( '/^\+?[0-9()\-.\s]+$/', $value ) ) return false; $digits = strlen( preg_replace( '/\D/', '', $value ) ); return $digits >= 6 && $digits <= 15; } /** Parse "jpg, PNG,.pdf" into [ 'jpg', 'png', 'pdf' ]. */ public static function parse_extensions( string $list ): array { $exts = []; foreach ( explode( ',', strtolower( $list ) ) as $ext ) { $ext = preg_replace( '/[^a-z0-9]/', '', $ext ); if ( '' !== $ext ) { $exts[] = $ext; } } $exts = array_values( array_unique( $exts ) ); return $exts ?: self::DEFAULT_FILE_TYPES; } // ========================================================================= // Main entry point // ========================================================================= public static function handle(): void { // 1. Nonce. The JS refreshes an expired nonce (cached pages) and retries once. if ( ! check_ajax_referer( self::NONCE_ACTION, 'nonce', false ) ) { wp_send_json_error( [ 'code' => 'invalid_nonce', 'message' => __( 'Your session has expired. Please reload the page and try again.', 'admore-widgets-suite' ), ], 403 ); } // 2. Rate limiting. $ip = self::get_client_ip(); $rl_key = 'eppro_form_rl_' . md5( $ip ); $attempts = (int) get_transient( $rl_key ); if ( $attempts >= self::RATE_LIMIT ) { wp_send_json_error( [ 'message' => __( 'Too many submissions. Please wait a moment and try again.', 'admore-widgets-suite' ) ], 429 ); } set_transient( $rl_key, $attempts + 1, MINUTE_IN_SECONDS ); // 3. Honeypot — pretend success so bots do not retry. if ( ! empty( $_POST['eppro_hp_field'] ) ) { wp_send_json_success( [ 'message' => esc_html__( 'Thank you!', 'admore-widgets-suite' ) ] ); } // 4. Load the saved widget settings. $form_id = isset( $_POST['form_id'] ) ? sanitize_key( wp_unslash( $_POST['form_id'] ) ) : ''; $post_id = absint( $_POST['post_id'] ?? 0 ); $page_id = absint( $_POST['page_id'] ?? 0 ); if ( ! $post_id ) { // Requests from the previous version of the form script only sent page_id. $post_id = $page_id; } $widget = EPPRO_Elementor_Helper::get_widget( $post_id, $form_id, 'eppro_form' ); $settings = $widget ? (array) $widget->get_settings_for_display() : null; if ( null === $settings ) { wp_send_json_error( [ 'message' => __( 'This form could not be found. Please reload the page and try again.', 'admore-widgets-suite' ) ], 400 ); } $fields = self::normalize_fields( (array) ( $settings['form_fields'] ?? [] ) ); if ( empty( $fields ) ) { wp_send_json_error( [ 'message' => __( 'This form has no fields.', 'admore-widgets-suite' ) ], 400 ); } // 5. CAPTCHA (only when this form enables it). $captcha = self::captcha_config( $settings ); if ( $captcha ) { $token_keys = [ 'recaptcha_v2' => 'g-recaptcha-response', 'recaptcha_v3' => 'eppro_captcha_token', 'hcaptcha' => 'h-captcha-response', ]; $token_key = $token_keys[ $captcha['type'] ]; $token = isset( $_POST[ $token_key ] ) ? sanitize_text_field( wp_unslash( $_POST[ $token_key ] ) ) : ''; $error = self::verify_captcha( $captcha, $token, $ip ); if ( null !== $error ) { wp_send_json_error( [ 'code' => 'captcha', 'message' => $error ], 400 ); } } // 6. Read the submitted values for the fields this form actually has. $values = []; $files = []; foreach ( $fields as $id => $field ) { if ( in_array( $field['type'], self::DISPLAY_TYPES, true ) ) continue; if ( 'file' === $field['type'] ) { $file_key = 'eppro_file_' . $id; if ( isset( $_FILES[ $file_key ]['name'] ) && is_string( $_FILES[ $file_key ]['name'] ) && '' !== $_FILES[ $file_key ]['name'] ) { $files[ $id ] = [ 'name' => sanitize_file_name( wp_unslash( $_FILES[ $file_key ]['name'] ) ), 'type' => isset( $_FILES[ $file_key ]['type'] ) && is_string( $_FILES[ $file_key ]['type'] ) ? sanitize_mime_type( wp_unslash( $_FILES[ $file_key ]['type'] ) ) : '', 'tmp_name' => isset( $_FILES[ $file_key ]['tmp_name'] ) && is_string( $_FILES[ $file_key ]['tmp_name'] ) ? sanitize_text_field( $_FILES[ $file_key ]['tmp_name'] ) : '', 'error' => isset( $_FILES[ $file_key ]['error'] ) ? (int) $_FILES[ $file_key ]['error'] : UPLOAD_ERR_NO_FILE, 'size' => isset( $_FILES[ $file_key ]['size'] ) ? (int) $_FILES[ $file_key ]['size'] : 0, ]; } continue; } $post_key = 'eppro_field_' . $id; if ( ! isset( $_POST[ $post_key ] ) ) { $values[ $id ] = 'checkbox' === $field['type'] ? [] : ''; continue; } if ( 'checkbox' === $field['type'] ) { $checked = array_map( 'sanitize_text_field', (array) wp_unslash( $_POST[ $post_key ] ) ); $values[ $id ] = array_values( array_filter( $checked, 'strlen' ) ); } elseif ( 'textarea' === $field['type'] ) { $values[ $id ] = sanitize_textarea_field( wp_unslash( $_POST[ $post_key ] ) ); } else { $values[ $id ] = sanitize_text_field( wp_unslash( $_POST[ $post_key ] ) ); } } // 7. Validate (fields hidden by conditional logic are skipped). $visible = self::visible_fields( $fields, $values ); list( $clean, $errors ) = self::validate( $fields, $visible, $values, $files ); if ( ! empty( $errors ) ) { wp_send_json_error( [ 'errors' => $errors ] ); } // 8. File uploads. $uploaded = self::handle_uploads( $fields, $visible, $files ); if ( ! empty( $uploaded['errors'] ) ) { wp_send_json_error( [ 'errors' => $uploaded['errors'] ] ); } // 9. Build the label => value map in field order. $data = []; $reply_to = ''; foreach ( $fields as $id => $field ) { if ( empty( $visible[ $id ] ) || in_array( $field['type'], self::DISPLAY_TYPES, true ) ) continue; $value = 'file' === $field['type'] ? ( $uploaded['urls'][ $id ] ?? '' ) : ( $clean[ $id ] ?? '' ); $label = $field['label']; if ( array_key_exists( $label, $data ) ) { $label .= ' (' . $id . ')'; } $data[ $label ] = $value; if ( '' === $reply_to && 'email' === $field['type'] && is_email( $value ) ) { $reply_to = $value; } } $form_name = sanitize_text_field( (string) ( $settings['form_name'] ?? '' ) ); if ( '' === $form_name ) { $form_name = __( 'Form', 'admore-widgets-suite' ); } $page_url = isset( $_POST['page_url'] ) ? esc_url_raw( wp_unslash( $_POST['page_url'] ) ) : ''; $page_url = $page_url ? (string) wp_validate_redirect( $page_url, '' ) : ''; // 10. Save the entry. if ( class_exists( 'EPPRO_DB' ) ) { EPPRO_DB::insert_entry( [ 'form_id' => $form_id, 'form_name' => $form_name, 'page_id' => $page_id ?: $post_id, 'page_url' => $page_url, 'fields' => $data, 'ip_address' => $ip, 'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '', ] ); } // 11. E-mail notification. $recipients = self::get_recipients( (string) ( $settings['email_to'] ?? '' ) ); if ( ! empty( $recipients ) ) { self::send_email( $recipients, (string) ( $settings['email_subject'] ?? '' ), $form_name, $data, $reply_to ); } // 12. Webhook (URL comes from the saved settings, never from the request). $webhook = esc_url_raw( trim( (string) ( $settings['webhook_url'] ?? '' ) ) ); if ( '' !== $webhook && wp_http_validate_url( $webhook ) ) { self::dispatch_webhook( $webhook, $data ); } // 13. Integrations enabled on this form (Mailchimp, Brevo, Google Sheets, Slack). // They run after the response has been sent, so a slow API never keeps // the visitor waiting (see EPPRO_Form_Integrations::run_after_response()). if ( class_exists( 'EPPRO_Form_Integrations' ) ) { EPPRO_Form_Integrations::run_after_response( $settings, $fields, $clean, $data, [ 'form_name' => $form_name, 'page_url' => $page_url, ] ); } /** * Fires after a valid Advanced Form submission was stored and sent. * * @param array $data Field label => value. * @param array $settings The form widget's settings. * @param string $form_name Form name. */ do_action( 'eppro_form_submitted', $data, $settings, $form_name ); // 14. Success. $message = wp_kses_post( (string) ( $settings['success_message'] ?? '' ) ); if ( '' === trim( wp_strip_all_tags( $message ) ) ) { $message = esc_html__( 'Thank you! Your submission has been received.', 'admore-widgets-suite' ); } $response = [ 'message' => $message ]; // Optional redirect after submit (same site or any http(s) URL the editor entered). $redirect = esc_url_raw( trim( (string) ( $settings['redirect_url']['url'] ?? '' ) ), [ 'http', 'https' ] ); if ( '' !== $redirect ) { $response['redirect'] = $redirect; $response['redirectDelay'] = min( 10, absint( $settings['redirect_delay'] ?? 0 ) ); } wp_send_json_success( $response ); } // ========================================================================= // Conditional logic (mirrors evaluateConditions() in form.js) // ========================================================================= private static function visible_fields( array $fields, array $values ): array { $visible = []; foreach ( $fields as $id => $field ) { if ( ! $field['cond_enabled'] ) { $visible[ $id ] = true; continue; } $watch = $field['cond_field']; if ( '' === $watch || ! isset( $fields[ $watch ] ) ) { // The script hides a field whose watched field does not exist. $visible[ $id ] = false; continue; } $value = $values[ $watch ] ?? ''; if ( is_array( $value ) ) { $value = implode( ',', $value ); } $visible[ $id ] = self::condition_passes( (string) $value, $field['cond_op'], $field['cond_value'] ); } return $visible; } private static function condition_passes( string $value, string $operator, string $expected ): bool { $value_l = mb_strtolower( $value ); $expected_l = mb_strtolower( $expected ); switch ( $operator ) { case 'equals': return $value === $expected; case 'not_equals': return $value !== $expected; case 'contains': return str_contains( $value_l, $expected_l ); case 'not_contains': return ! str_contains( $value_l, $expected_l ); case 'starts_with': return str_starts_with( $value_l, $expected_l ); case 'ends_with': return '' === $expected_l ? '' === $value_l : str_ends_with( $value_l, $expected_l ); case 'is_empty': return '' === trim( $value ); case 'not_empty': return '' !== trim( $value ); default: return true; } } // ========================================================================= // Validation // ========================================================================= /** * @return array{0: array, 1: array} [ id => clean string value ], [ id => error message ] */ private static function validate( array $fields, array $visible, array $values, array $files ): array { $clean = []; $errors = []; foreach ( $fields as $id => $field ) { if ( empty( $visible[ $id ] ) || in_array( $field['type'], self::DISPLAY_TYPES, true ) ) continue; /* translators: %s: form field label */ $required_msg = sprintf( __( '%s is required.', 'admore-widgets-suite' ), $field['label'] ); if ( 'file' === $field['type'] ) { if ( $field['required'] && empty( $files[ $id ] ) ) { $errors[ $id ] = $required_msg; } continue; } $raw = $values[ $id ] ?? ''; $value = ''; switch ( $field['type'] ) { case 'email': $value = sanitize_email( (string) $raw ); if ( '' !== trim( (string) $raw ) && ! is_email( $value ) ) { $errors[ $id ] = __( 'Please enter a valid email address.', 'admore-widgets-suite' ); } break; case 'number': $value = trim( (string) $raw ); if ( '' !== $value && ! is_numeric( $value ) ) { $errors[ $id ] = __( 'Please enter a valid number.', 'admore-widgets-suite' ); } break; case 'tel': $value = trim( (string) $raw ); if ( '' !== $value && ! self::valid_phone( $value ) ) { $errors[ $id ] = __( 'Please enter a valid phone number.', 'admore-widgets-suite' ); } break; case 'url': $value = trim( (string) $raw ); if ( '' !== $value ) { // Visitors often type "example.com" — accept it as https://. if ( ! preg_match( '#^[a-z][a-z0-9+.\-]*://#i', $value ) ) { $value = 'https://' . $value; } $value = esc_url_raw( $value, [ 'http', 'https' ] ); if ( '' === $value || ! filter_var( $value, FILTER_VALIDATE_URL ) ) { $errors[ $id ] = __( 'Please enter a valid URL.', 'admore-widgets-suite' ); } } break; case 'date': $value = trim( (string) $raw ); if ( '' !== $value ) { if ( '' === self::valid_date( $value ) ) { $errors[ $id ] = __( 'Please enter a valid date.', 'admore-widgets-suite' ); } elseif ( ( $field['date_min'] && $value < $field['date_min'] ) || ( $field['date_max'] && $value > $field['date_max'] ) ) { $errors[ $id ] = __( 'Please choose a date in the allowed range.', 'admore-widgets-suite' ); } } break; case 'time': $value = trim( (string) $raw ); if ( '' !== $value && ! preg_match( '/^([01]\d|2[0-3]):[0-5]\d(:[0-5]\d)?$/', $value ) ) { $errors[ $id ] = __( 'Please enter a valid time.', 'admore-widgets-suite' ); } break; case 'acceptance': // The browser sends "1" when ticked; store a readable answer. $value = '' !== trim( (string) $raw ) ? __( 'Yes', 'admore-widgets-suite' ) : ''; if ( $field['required'] && '' === $value ) { $errors[ $id ] = __( 'Please accept to continue.', 'admore-widgets-suite' ); } break; case 'hidden': $value = mb_substr( trim( (string) $raw ), 0, 500 ); break; case 'select': case 'radio': $value = (string) $raw; if ( '' !== $value && ! in_array( $value, array_map( 'sanitize_text_field', $field['options'] ), true ) ) { $errors[ $id ] = __( 'Please select a valid option.', 'admore-widgets-suite' ); } break; case 'checkbox': $allowed = array_map( 'sanitize_text_field', $field['options'] ); $value = implode( ', ', array_values( array_intersect( (array) $raw, $allowed ) ) ); break; default: // text, textarea — already sanitized when read. $value = (string) $raw; break; } if ( ! isset( $errors[ $id ] ) && $field['required'] && '' === trim( $value ) ) { $errors[ $id ] = $required_msg; } $clean[ $id ] = $value; } return [ $clean, $errors ]; } // ========================================================================= // File uploads // ========================================================================= /** * Upload the files of visible file fields. Only the extensions configured * for the field (and permitted by WordPress) are accepted. Files get an * unguessable name inside uploads/eppro-forms/. If any upload fails, the * files already stored for this submission are removed again. * * @return array{urls: array, errors: array} */ private static function handle_uploads( array $fields, array $visible, array $files ): array { $urls = []; $errors = []; $paths = []; foreach ( $fields as $id => $field ) { if ( 'file' !== $field['type'] || empty( $visible[ $id ] ) || empty( $files[ $id ] ) ) continue; $file = $files[ $id ]; if ( UPLOAD_ERR_OK !== $file['error'] ) { $errors[ $id ] = self::upload_error_message( $file['error'] ); continue; } $mimes = self::allowed_mimes( $field['allowed_types'] ); if ( empty( $mimes ) ) { $errors[ $id ] = __( 'This file type is not allowed.', 'admore-widgets-suite' ); continue; } if ( ! function_exists( 'wp_handle_upload' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; } $file['name'] = strtolower( wp_generate_password( 12, false ) ) . '-' . $file['name']; add_filter( 'upload_dir', [ __CLASS__, 'filter_upload_dir' ] ); $result = wp_handle_upload( $file, [ 'test_form' => false, 'mimes' => $mimes ] ); remove_filter( 'upload_dir', [ __CLASS__, 'filter_upload_dir' ] ); if ( ! empty( $result['error'] ) || empty( $result['url'] ) || empty( $result['file'] ) ) { $errors[ $id ] = ! empty( $result['error'] ) ? sanitize_text_field( (string) $result['error'] ) : __( 'The file could not be uploaded.', 'admore-widgets-suite' ); continue; } $urls[ $id ] = $result['url']; $paths[] = $result['file']; } if ( ! empty( $errors ) ) { foreach ( $paths as $path ) { wp_delete_file( $path ); } $urls = []; } return [ 'urls' => $urls, 'errors' => $errors ]; } /** Route form attachments into uploads/eppro-forms/{yyyy/mm}. */ public static function filter_upload_dir( array $dirs ): array { $subdir = '/' . self::UPLOAD_SUBDIR . $dirs['subdir']; $dirs['path'] = $dirs['basedir'] . $subdir; $dirs['url'] = $dirs['baseurl'] . $subdir; $dirs['subdir'] = $subdir; return $dirs; } /** WordPress-allowed MIME types restricted to the given extensions. */ private static function allowed_mimes( array $extensions ): array { $mimes = []; foreach ( get_allowed_mime_types() as $pattern => $mime ) { $keep = array_intersect( explode( '|', $pattern ), $extensions ); if ( ! empty( $keep ) ) { $mimes[ implode( '|', $keep ) ] = $mime; } } return $mimes; } private static function upload_error_message( int $code ): string { if ( in_array( $code, [ UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE ], true ) ) { return __( 'The file is too large.', 'admore-widgets-suite' ); } return __( 'The file could not be uploaded.', 'admore-widgets-suite' ); } // ========================================================================= // CAPTCHA // ========================================================================= private static function verify_captcha( array $captcha, string $token, string $ip ): ?string { if ( '' === $token ) { return __( 'Please complete the CAPTCHA.', 'admore-widgets-suite' ); } $url = 'hcaptcha' === $captcha['type'] ? 'https://hcaptcha.com/siteverify' : 'https://www.google.com/recaptcha/api/siteverify'; $response = wp_remote_post( $url, [ 'body' => [ 'secret' => $captcha['secret'], 'response' => $token, 'remoteip' => $ip ], 'timeout' => 8, ] ); $failed = __( 'CAPTCHA verification failed. Please try again.', 'admore-widgets-suite' ); if ( is_wp_error( $response ) ) { return $failed; } $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( ! is_array( $body ) || empty( $body['success'] ) ) { return $failed; } if ( 'recaptcha_v3' === $captcha['type'] ) { if ( isset( $body['score'] ) && (float) $body['score'] < 0.5 ) { return __( 'Your submission was flagged as spam. Please try again.', 'admore-widgets-suite' ); } if ( ! empty( $body['action'] ) && 'eppro_form_submit' !== $body['action'] ) { return $failed; } } return null; } // ========================================================================= // Notifications // ========================================================================= /** Split "a@x.com, b@y.com" into valid addresses. */ private static function get_recipients( string $list ): array { $recipients = []; foreach ( preg_split( '/[,;]+/', $list ) ?: [] as $address ) { $address = sanitize_email( trim( $address ) ); if ( '' !== $address && is_email( $address ) ) { $recipients[] = $address; } } return array_values( array_unique( $recipients ) ); } private static function send_email( array $to, string $subject, string $form_name, array $data, string $reply_to ): void { $subject = sanitize_text_field( $subject ); if ( '' === $subject ) { $subject = __( 'New Form Submission', 'admore-widgets-suite' ); } $upload_base = trailingslashit( wp_get_upload_dir()['baseurl'] ) . self::UPLOAD_SUBDIR . '/'; $rows = ''; foreach ( $data as $label => $value ) { $value = (string) $value; if ( '' !== $value && str_starts_with( $value, $upload_base ) ) { $cell = '' . esc_html( wp_basename( $value ) ) . ''; } else { $cell = nl2br( esc_html( $value ) ); } $rows .= '' . esc_html( (string) $label ) . '' . '' . $cell . ''; } $intro = sprintf( /* translators: 1: form name, 2: site name */ __( 'New submission from "%1$s" on %2$s.', 'admore-widgets-suite' ), $form_name, wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ) ); $body = '

' . esc_html( $intro ) . '

' . '' . $rows . '
'; $headers = [ 'Content-Type: text/html; charset=UTF-8' ]; if ( '' !== $reply_to ) { $headers[] = 'Reply-To: ' . $reply_to; } wp_mail( $to, $subject, $body, $headers ); } private static function dispatch_webhook( string $url, array $data ): void { wp_safe_remote_post( $url, [ 'body' => wp_json_encode( $data ), 'headers' => [ 'Content-Type' => 'application/json' ], 'timeout' => 10, 'blocking' => false, 'data_format' => 'body', ] ); } // ========================================================================= // Client IP // ========================================================================= /** * The connecting IP. Proxy headers such as X-Forwarded-For are ignored * because any visitor can forge them; sites behind a trusted proxy can * supply the real address through the "eppro_form_client_ip" filter. */ private static function get_client_ip(): string { $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; $ip = (string) apply_filters( 'eppro_form_client_ip', $ip ); return filter_var( $ip, FILTER_VALIDATE_IP ) ? $ip : '0.0.0.0'; } }