value pair to $all_fields. Because the map is keyed by * label, two fields sharing a label (e.g. two "Email" fields, or two * fields both called "Other") would otherwise silently overwrite each * other. When the label is already present, the lowest free numeric suffix * is appended instead: "Email", "Email 2", "Email 3", ... * * @param array $all_fields The fields map, passed by reference. * @param string $label The field label. * @param mixed $value The field value. */ private static function add_field( &$all_fields, $label, $value ) { if ( ! array_key_exists( $label, $all_fields ) ) { $all_fields[ $label ] = $value; return; } // Label already used — find the lowest free numeric suffix. $suffix = 2; while ( array_key_exists( $label . ' ' . $suffix, $all_fields ) ) { $suffix++; } $all_fields[ $label . ' ' . $suffix ] = $value; } /** * ───────────────────────────────────────────────────────────────── * HELPER: Build Fields Summary * ───────────────────────────────────────────────────────────────── * * Takes an associative array of field label => value pairs and * produces a newline-delimited string like: * Name: John Smith * Email: john@example.com * Service Type: Web Design * * Excludes internal tracking fields and empty values. Truncates to 2000 chars. * * @param array $fields Associative array of label => value. * @return string The formatted summary. */ private static function build_fields_summary( $fields ) { $lines = array(); foreach ( $fields as $label => $value ) { // Exclude TrueConversion's own internal tracking fields (ftcon_*, utm_*, // _tc*) so they are never sent to the AI proxy. First check — covers // every handler, not just Fluent Forms and Jetpack. if ( self::is_internal_field_key( $label ) ) { continue; } if ( null === $value || '' === $value ) { continue; } // Handle array values (multi-select, checkboxes, etc). if ( is_array( $value ) ) { $value = implode( ', ', array_filter( array_map( 'strval', $value ) ) ); } if ( ! is_string( $value ) && ! is_numeric( $value ) ) { continue; } $value = trim( (string) $value ); if ( '' === $value ) { continue; } // Global safety net: skip values that look like uploaded filenames // or file paths, regardless of which handler produced them. if ( self::value_looks_like_file( $value ) ) { continue; } $label = sanitize_text_field( (string) $label ); $value = sanitize_textarea_field( $value ); $lines[] = $label . ': ' . $value; } // The full summary is stored in the database and sent to the AI proxy — // no length limit here. Display-side truncation (e.g. the dashboard list) // is applied at render time only. $summary = implode( "\n", $lines ); return $summary; } /** * ───────────────────────────────────────────────────────────────── * HELPER: Extract Name and Email from Fields * ───────────────────────────────────────────────────────────────── * * Searches field labels for common patterns using fuzzy matching: * - Name: "name", "full name", "your-name", "first name" + "last name" * - Email: "email", "your-email", "e-mail", "email address" * * @param array $fields Associative array of label => value. * @return array { 'name' => string, 'email' => string } */ private static function extract_name_email( $fields ) { $name = ''; $first_name = ''; $last_name = ''; $email = ''; foreach ( $fields as $label => $value ) { if ( is_array( $value ) ) { $value = implode( ' ', $value ); } if ( ! is_string( $value ) ) { continue; } $label_lower = strtolower( (string) $label ); $value = trim( $value ); if ( empty( $value ) ) { continue; } // Email detection. if ( empty( $email ) && false !== strpos( $label_lower, 'email' ) ) { if ( is_email( $value ) ) { $email = $value; } } // Exact full name match (highest priority). if ( empty( $name ) && empty( $first_name ) ) { if ( in_array( $label_lower, array( 'name', 'your-name', 'your name', 'full name', 'full_name', 'fullname' ), true ) ) { $name = $value; continue; } } // First/last name detection. if ( false !== strpos( $label_lower, 'first' ) && false !== strpos( $label_lower, 'name' ) ) { $first_name = $value; continue; } if ( false !== strpos( $label_lower, 'last' ) && false !== strpos( $label_lower, 'name' ) ) { $last_name = $value; continue; } // Fallback: label contains "name" but not excluded words. if ( empty( $name ) && empty( $first_name ) && false !== strpos( $label_lower, 'name' ) ) { $excluded = array( 'email', 'company', 'business', 'username', 'user_name', 'form_name', 'form name' ); $is_excluded = false; foreach ( $excluded as $exc ) { if ( false !== strpos( $label_lower, $exc ) ) { $is_excluded = true; break; } } if ( ! $is_excluded ) { $name = $value; } } } // Combine first + last if no full name found. if ( empty( $name ) && ( ! empty( $first_name ) || ! empty( $last_name ) ) ) { $name = trim( $first_name . ' ' . $last_name ); } return array( 'name' => $name, 'email' => $email, ); } /** * ───────────────────────────────────────────────────────────────── * HELPER: Build Entry Data * ───────────────────────────────────────────────────────────────── * * Common method used by all form integrations. Reads the UTM cookie, * detects the traffic source, and builds a standardised entry array * ready for saving to the database. * * @param string $form_plugin Name of the form plugin. * @param string $form_name Name/title of the specific form. * @param string $name Submitter's name (if available). * @param string $email Submitter's email (if available). * @param string $message Message/content from the form. * @param string $form_fields_summary All form fields as "Label: Value" lines. * * @return array Entry data ready for FTCON_Entry_Manager::save_entry(). */ private static function build_entry_data( $form_plugin, $form_name, $name = '', $email = '', $message = '', $form_fields_summary = '' ) { // Read UTM data from the cookie. $utm_data = FTCON_Cookie::get_data(); // Server-side referrer fallback. if ( empty( $utm_data['referrer'] ) && ! empty( $_SERVER['HTTP_REFERER'] ) ) { $server_referrer = esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ); $referrer_host = wp_parse_url( $server_referrer, PHP_URL_HOST ); $site_host = wp_parse_url( home_url(), PHP_URL_HOST ); if ( $referrer_host && $referrer_host !== $site_host ) { $utm_data['referrer'] = $server_referrer; } } // Detect the traffic source from the UTM data. $detection = FTCON_Detector::detect_source( $utm_data ); // Capture user agent for Meta CAPI and LinkedIn attribution. $user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; // Raw IP for Meta CAPI (Meta requires the unhashed IP). // Stored separately from the anonymised ip_address column. $ip_address_raw = self::get_raw_client_ip(); // _fbp browser cookie — read from JS-injected POST field first, // then fall back to direct PHP cookie read. $meta_fbp = ''; if ( ! empty( $_POST['ftcon_fbp'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing $meta_fbp = sanitize_text_field( wp_unslash( $_POST['ftcon_fbp'] ) ); } elseif ( ! empty( $_COOKIE['_fbp'] ) ) { $meta_fbp = sanitize_text_field( wp_unslash( $_COOKIE['_fbp'] ) ); } // Construct _fbc server-side from fbclid when present. // Format: fb.1.{creation_time_ms}.{fbclid} $meta_fbc = ''; if ( 'fbclid' === $detection['click_id_type'] && ! empty( $detection['click_id'] ) ) { $meta_fbc = 'fb.1.' . ( (int) current_time( 'timestamp' ) * 1000 ) . '.' . $detection['click_id']; } // Build the entry array. return array( 'form_plugin' => sanitize_text_field( $form_plugin ), 'form_name' => sanitize_text_field( $form_name ), 'submitter_name' => sanitize_text_field( $name ), 'submitter_email' => sanitize_email( $email ), 'message_excerpt' => sanitize_textarea_field( $message ), 'traffic_source' => sanitize_text_field( $detection['traffic_source'] ), 'utm_source' => sanitize_text_field( isset( $utm_data['utm_source'] ) ? $utm_data['utm_source'] : '' ), 'utm_medium' => sanitize_text_field( isset( $utm_data['utm_medium'] ) ? $utm_data['utm_medium'] : '' ), 'utm_campaign' => sanitize_text_field( isset( $utm_data['utm_campaign'] ) ? $utm_data['utm_campaign'] : '' ), 'utm_term' => sanitize_text_field( isset( $utm_data['utm_term'] ) ? $utm_data['utm_term'] : '' ), 'utm_content' => sanitize_text_field( isset( $utm_data['utm_content'] ) ? $utm_data['utm_content'] : '' ), 'click_id' => sanitize_text_field( $detection['click_id'] ), 'click_id_type' => sanitize_text_field( $detection['click_id_type'] ), 'page_url' => esc_url_raw( isset( $_SERVER['REQUEST_URI'] ) ? home_url( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) : '' ), 'landing_page' => esc_url_raw( isset( $utm_data['landing_page'] ) ? $utm_data['landing_page'] : '' ), 'referrer' => esc_url_raw( isset( $utm_data['referrer'] ) ? $utm_data['referrer'] : '' ), 'ip_address' => self::get_client_ip(), 'ip_address_raw' => $ip_address_raw, 'user_agent' => $user_agent, 'meta_fbp' => $meta_fbp, 'meta_fbc' => $meta_fbc, 'form_fields_summary' => sanitize_textarea_field( $form_fields_summary ), ); } /** * ───────────────────────────────────────────────────────────────── * HELPER: Process Entry * ───────────────────────────────────────────────────────────────── * * Saves the entry to the database, sends the per-submission email * notification, and fires `ftcon_after_entry_saved` for add-ons. * * @param array $entry_data The entry data from build_entry_data(). */ private static function process_entry( $entry_data ) { // Save to database. $entry_id = FTCON_Entry_Manager::save_entry( $entry_data ); if ( ! $entry_id ) { return; } // Send per-submission email notification (available to all plans). $frequency = get_option( 'ftcon_email_frequency', array( 'each' ) ); if ( is_array( $frequency ) && in_array( 'each', $frequency, true ) ) { FTCON_Email_Notifier::send_notification( $entry_data ); } /** * Fires after an entry is saved and its per-submission email sent. * * Add-ons (AI classification, other ad platforms) hook in here * instead of being wired directly into the core save path. * * @param int $entry_id The saved entry ID. * @param array $entry_data The entry data array. */ do_action( 'ftcon_after_entry_saved', $entry_id, $entry_data ); } /** * ───────────────────────────────────────────────────────────────── * HELPER: Get Raw Client IP Address * ───────────────────────────────────────────────────────────────── * * Returns the validated, un-anonymised IP address. Used when the * full IP is required (e.g. Meta CAPI, LinkedIn attribution). * * @return string The raw IP address, or empty string if invalid. */ private static function get_raw_client_ip() { $ip = ''; if ( ! empty( $_SERVER['HTTP_CLIENT_IP'] ) ) { $ip = sanitize_text_field( wp_unslash( $_SERVER['HTTP_CLIENT_IP'] ) ); } elseif ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { $ips = explode( ',', sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) ); $ip = trim( $ips[0] ); } elseif ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) { $ip = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ); } return filter_var( $ip, FILTER_VALIDATE_IP ) ? $ip : ''; } /** * ───────────────────────────────────────────────────────────────── * HELPER: Get Client IP Address (Anonymised) * ───────────────────────────────────────────────────────────────── * * Returns the visitor's IP with the last octet zeroed for privacy. * Stored in the ip_address column. See get_raw_client_ip() for the * full IP stored in ip_address_raw. * * @return string The anonymised IP address. */ private static function get_client_ip() { $ip = self::get_raw_client_ip(); if ( '' === $ip ) { return ''; } // Anonymise: remove last octet for IPv4, last group for IPv6. if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) { return preg_replace( '/\.\d+$/', '.0', $ip ); } if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) { return preg_replace( '/:[^:]+$/', ':0', $ip ); } return $ip; } /** * ═════════════════════════════════════════════════════════════════ * 1. CONTACT FORM 7 * ═════════════════════════════════════════════════════════════════ * * @param WPCF7_ContactForm $contact_form The form instance. * @param bool $abort Whether to abort sending. * @param WPCF7_Submission $submission The submission instance. */ public static function handle_cf7( $contact_form, &$abort, $submission ) { if ( ! $submission ) { return; } $posted_data = $submission->get_posted_data(); $form_name = $contact_form->title(); // Build all-fields array from posted data (skip internal CF7 keys). $all_fields = array(); $message_parts = array(); // Collect the names of file-upload fields (basetype file / mfile) from // the form definition so their filenames are never added to the summary. $file_field_names = array(); foreach ( $contact_form->scan_form_tags() as $tag ) { $basetype = isset( $tag->basetype ) ? $tag->basetype : ''; if ( in_array( $basetype, array( 'file', 'mfile' ), true ) ) { $file_field_names[] = $tag->name; } } foreach ( $posted_data as $key => $value ) { if ( strpos( $key, '_wpcf7' ) === 0 ) { continue; } // Skip file upload fields (the posted value is the uploaded filename). if ( in_array( $key, $file_field_names, true ) ) { continue; } $label = ucwords( str_replace( array( '-', '_' ), ' ', $key ) ); if ( is_array( $value ) ) { $value = implode( ', ', $value ); } self::add_field( $all_fields, $label, $value ); // Detect message fields. $key_lower = strtolower( $key ); if ( ( false !== strpos( $key_lower, 'message' ) || false !== strpos( $key_lower, 'comment' ) || false !== strpos( $key_lower, 'brief' ) || false !== strpos( $key_lower, 'notes' ) ) && ! empty( $value ) ) { $message_parts[] = is_array( $value ) ? implode( ' ', $value ) : $value; } } // Fallback: if no message detected by label, check for textarea tags. if ( empty( $message_parts ) ) { $tags = $contact_form->scan_form_tags( array( 'basetype' => 'textarea' ) ); foreach ( $tags as $tag ) { if ( isset( $posted_data[ $tag->name ] ) && ! empty( $posted_data[ $tag->name ] ) ) { $val = $posted_data[ $tag->name ]; $message_parts[] = is_array( $val ) ? implode( ' ', $val ) : $val; } } } $form_fields_summary = self::build_fields_summary( $all_fields ); $extracted = self::extract_name_email( $all_fields ); $message = implode( "\n\n", $message_parts ); $entry_data = self::build_entry_data( 'Contact Form 7', $form_name, $extracted['name'], $extracted['email'], $message, $form_fields_summary ); self::process_entry( $entry_data ); // Append traffic source to the email body. $mail = $contact_form->prop( 'mail' ); if ( is_array( $mail ) && isset( $mail['body'] ) ) { /* translators: %s: Traffic source label */ $mail['body'] .= "\n\n---\n" . sprintf( __( 'Traffic Source: %s', 'forge-trueconversion' ), $entry_data['traffic_source'] ); if ( ! empty( $entry_data['landing_page'] ) ) { /* translators: %s: Landing page URL */ $mail['body'] .= "\n" . sprintf( __( 'Landing Page: %s', 'forge-trueconversion' ), $entry_data['landing_page'] ); } $contact_form->set_properties( array( 'mail' => $mail ) ); } } /** * ═════════════════════════════════════════════════════════════════ * 2. WPFORMS * ═════════════════════════════════════════════════════════════════ * * @param array $fields The form fields with values. * @param array $entry The entry data. * @param array $form_data The form configuration. * @param int $entry_id The WPForms entry ID. */ public static function handle_wpforms( $fields, $entry, $form_data, $entry_id ) { $form_name = isset( $form_data['settings']['form_title'] ) ? $form_data['settings']['form_title'] : __( 'WPForms Form', 'forge-trueconversion' ); $all_fields = array(); $message_parts = array(); foreach ( $fields as $field ) { $type = isset( $field['type'] ) ? $field['type'] : ''; $val = isset( $field['value'] ) ? $field['value'] : ''; $label = isset( $field['name'] ) ? $field['name'] : ( 'Field ' . ( isset( $field['id'] ) ? $field['id'] : '' ) ); // Skip password and file upload fields. if ( in_array( $type, array( 'password', 'file-upload' ), true ) ) { continue; } self::add_field( $all_fields, $label, $val ); if ( 'textarea' === $type && ! empty( $val ) ) { $message_parts[] = $val; } } $form_fields_summary = self::build_fields_summary( $all_fields ); $extracted = self::extract_name_email( $all_fields ); $message = implode( "\n\n", $message_parts ); $entry_data = self::build_entry_data( 'WPForms', $form_name, $extracted['name'], $extracted['email'], $message, $form_fields_summary ); self::process_entry( $entry_data ); } /** * ═════════════════════════════════════════════════════════════════ * 3. GRAVITY FORMS * ═════════════════════════════════════════════════════════════════ * * @param array $entry The Gravity Forms entry. * @param array $form The form configuration. */ public static function handle_gravityforms( $entry, $form ) { $form_name = isset( $form['title'] ) ? $form['title'] : __( 'Gravity Form', 'forge-trueconversion' ); $all_fields = array(); $message_parts = array(); if ( isset( $form['fields'] ) && is_array( $form['fields'] ) ) { foreach ( $form['fields'] as $field ) { $field_id = (string) $field->id; $label = isset( $field->label ) ? $field->label : 'Field ' . $field_id; // Skip password and file upload fields. if ( in_array( $field->type, array( 'password', 'fileupload' ), true ) ) { continue; } // Handle name fields with sub-fields (.3 = first, .6 = last). if ( 'name' === $field->type ) { $first = isset( $entry[ $field_id . '.3' ] ) ? $entry[ $field_id . '.3' ] : ''; $last = isset( $entry[ $field_id . '.6' ] ) ? $entry[ $field_id . '.6' ] : ''; self::add_field( $all_fields, $label, trim( $first . ' ' . $last ) ); continue; } // Handle address fields with sub-fields. if ( 'address' === $field->type ) { $parts = array(); for ( $i = 1; $i <= 6; $i++ ) { $sub_val = isset( $entry[ $field_id . '.' . $i ] ) ? $entry[ $field_id . '.' . $i ] : ''; if ( ! empty( $sub_val ) ) { $parts[] = $sub_val; } } self::add_field( $all_fields, $label, implode( ', ', $parts ) ); continue; } $val = isset( $entry[ $field_id ] ) ? $entry[ $field_id ] : ''; self::add_field( $all_fields, $label, $val ); if ( 'textarea' === $field->type && ! empty( $val ) ) { $message_parts[] = $val; } } } $form_fields_summary = self::build_fields_summary( $all_fields ); $extracted = self::extract_name_email( $all_fields ); $message = implode( "\n\n", $message_parts ); $entry_data = self::build_entry_data( 'Gravity Forms', $form_name, $extracted['name'], $extracted['email'], $message, $form_fields_summary ); self::process_entry( $entry_data ); } /** * ═════════════════════════════════════════════════════════════════ * 4. NINJA FORMS * ═════════════════════════════════════════════════════════════════ * * @param array $form_data The submitted form data. */ public static function handle_ninjaforms( $form_data ) { $form_name = isset( $form_data['settings']['title'] ) ? $form_data['settings']['title'] : __( 'Ninja Form', 'forge-trueconversion' ); $all_fields = array(); $message_parts = array(); if ( isset( $form_data['fields'] ) && is_array( $form_data['fields'] ) ) { foreach ( $form_data['fields'] as $field ) { $type = isset( $field['type'] ) ? $field['type'] : ''; $val = isset( $field['value'] ) ? $field['value'] : ''; $label = isset( $field['label'] ) ? $field['label'] : ( isset( $field['key'] ) ? $field['key'] : 'Field' ); // Skip non-data fields. if ( in_array( $type, array( 'submit', 'hidden', 'html', 'hr', 'divider', 'recaptcha', 'password', 'file_upload' ), true ) ) { continue; } self::add_field( $all_fields, $label, $val ); if ( 'textarea' === $type && ! empty( $val ) ) { $message_parts[] = $val; } } } $form_fields_summary = self::build_fields_summary( $all_fields ); $extracted = self::extract_name_email( $all_fields ); $message = implode( "\n\n", $message_parts ); $entry_data = self::build_entry_data( 'Ninja Forms', $form_name, $extracted['name'], $extracted['email'], $message, $form_fields_summary ); self::process_entry( $entry_data ); } /** * ═════════════════════════════════════════════════════════════════ * 5. FORMIDABLE FORMS * ═════════════════════════════════════════════════════════════════ * * @param int $entry_id The new entry ID. * @param array $form_id The form ID. */ public static function handle_formidable( $entry_id, $form_id ) { if ( ! class_exists( 'FrmEntry' ) || ! class_exists( 'FrmForm' ) ) { return; } $entry_obj = FrmEntry::getOne( $entry_id, true ); $form = FrmForm::getOne( $form_id ); $form_name = isset( $form->name ) ? $form->name : __( 'Formidable Form', 'forge-trueconversion' ); $all_fields = array(); $message_parts = array(); if ( isset( $entry_obj->metas ) && is_array( $entry_obj->metas ) ) { $fields = FrmField::get_all_for_form( $form_id ); foreach ( $fields as $field ) { $val = isset( $entry_obj->metas[ $field->id ] ) ? $entry_obj->metas[ $field->id ] : ''; // Skip password and file upload. if ( in_array( $field->type, array( 'password', 'file' ), true ) ) { continue; } $label = isset( $field->name ) ? $field->name : 'Field ' . $field->id; if ( is_array( $val ) ) { $val = implode( ', ', $val ); } self::add_field( $all_fields, $label, $val ); if ( 'textarea' === $field->type && ! empty( $val ) ) { $message_parts[] = $val; } } } $form_fields_summary = self::build_fields_summary( $all_fields ); $extracted = self::extract_name_email( $all_fields ); $message = implode( "\n\n", $message_parts ); $entry_data = self::build_entry_data( 'Formidable Forms', $form_name, $extracted['name'], $extracted['email'], $message, $form_fields_summary ); self::process_entry( $entry_data ); } /** * ═════════════════════════════════════════════════════════════════ * 6. FLUENT FORMS * ═════════════════════════════════════════════════════════════════ * * @param int $insert_id The new entry ID. * @param array $form_data The submitted data. * @param object $form The form object. */ public static function handle_fluentforms( $insert_id, $form_data, $form ) { $form_name = isset( $form->title ) ? $form->title : __( 'Fluent Form', 'forge-trueconversion' ); $all_fields = array(); $message_parts = array(); if ( is_array( $form_data ) ) { foreach ( $form_data as $key => $val ) { // Skip Fluent internal keys and TrueConversion's own tracking fields. if ( strpos( $key, '__' ) === 0 || strpos( $key, '_fluentform' ) === 0 || self::is_internal_field_key( $key ) ) { continue; } $label = ucwords( str_replace( array( '-', '_' ), ' ', $key ) ); if ( is_array( $val ) ) { $val = implode( ', ', array_filter( $val ) ); } elseif ( ! is_string( $val ) && ! is_numeric( $val ) ) { continue; } // No per-field type in Fluent's flat submission data — detect // file/image uploads by value (filename / path / URL). if ( self::value_looks_like_file( (string) $val ) ) { continue; } self::add_field( $all_fields, $label, (string) $val ); // Detect message/textarea fields by key name. $key_lower = strtolower( $key ); if ( ( false !== strpos( $key_lower, 'message' ) || false !== strpos( $key_lower, 'comment' ) || false !== strpos( $key_lower, 'description' ) || false !== strpos( $key_lower, 'brief' ) || false !== strpos( $key_lower, 'notes' ) ) && ! empty( $val ) ) { $message_parts[] = (string) $val; } } } $form_fields_summary = self::build_fields_summary( $all_fields ); $extracted = self::extract_name_email( $all_fields ); $message = implode( "\n\n", $message_parts ); $entry_data = self::build_entry_data( 'Fluent Forms', $form_name, $extracted['name'], $extracted['email'], $message, $form_fields_summary ); self::process_entry( $entry_data ); } /** * ═════════════════════════════════════════════════════════════════ * 7. ELEMENTOR PRO FORMS * ═════════════════════════════════════════════════════════════════ * * @param \ElementorPro\Modules\Forms\Classes\Form_Record $record The form record. * @param \ElementorPro\Modules\Forms\Classes\Ajax_Handler $ajax_handler The AJAX handler. */ public static function handle_elementor( $record, $ajax_handler ) { $form_name = $record->get_form_settings( 'form_name' ); $raw_fields = $record->get( 'fields' ); $all_fields = array(); $message_parts = array(); foreach ( $raw_fields as $id => $field ) { $type = isset( $field['type'] ) ? $field['type'] : ''; $val = isset( $field['value'] ) ? $field['value'] : ''; $label = isset( $field['title'] ) ? $field['title'] : ( ! empty( $id ) ? $id : 'Field' ); // Skip password and file upload. if ( in_array( $type, array( 'password', 'upload' ), true ) ) { continue; } self::add_field( $all_fields, $label, $val ); if ( 'textarea' === $type && ! empty( $val ) ) { $message_parts[] = $val; } } $form_fields_summary = self::build_fields_summary( $all_fields ); $extracted = self::extract_name_email( $all_fields ); $message = implode( "\n\n", $message_parts ); $entry_data = self::build_entry_data( 'Elementor Forms', $form_name, $extracted['name'], $extracted['email'], $message, $form_fields_summary ); self::process_entry( $entry_data ); } /** * ═════════════════════════════════════════════════════════════════ * 8. FORMINATOR * ═════════════════════════════════════════════════════════════════ * * IMPORTANT: The hook 'forminator_custom_form_submit_before_set_fields' * passes parameters as: ($entry, $form_id, $field_data_array) * * @param object $entry The entry object. * @param int $form_id The form ID. * @param array $field_data The field data array. */ public static function handle_forminator( $entry, $form_id, $field_data ) { $form_name = __( 'Forminator Form', 'forge-trueconversion' ); $label_map = array(); // element_id => field_label from form definition. $field_types = array(); // element_id => field type (text, textarea, email, etc.). if ( class_exists( 'Forminator_Form_Model' ) ) { $model = Forminator_Form_Model::model()->load( $form_id ); if ( $model && isset( $model->settings['formName'] ) ) { $form_name = $model->settings['formName']; } // Build a map of element_id => human-readable field_label. if ( $model && ! empty( $model->fields ) ) { foreach ( $model->fields as $field_def ) { $raw = is_object( $field_def ) ? $field_def->raw : ( is_array( $field_def ) ? $field_def : array() ); $eid = isset( $raw['element_id'] ) ? $raw['element_id'] : ''; $fl = isset( $raw['field_label'] ) ? $raw['field_label'] : ''; $ft = isset( $raw['type'] ) ? $raw['type'] : ''; if ( $eid && $fl ) { $label_map[ $eid ] = $fl; } if ( $eid && $ft ) { $field_types[ $eid ] = $ft; } } } } $all_fields = array(); $message_parts = array(); if ( is_array( $field_data ) ) { foreach ( $field_data as $field ) { $field_name = isset( $field['name'] ) ? $field['name'] : ''; $val = isset( $field['value'] ) ? $field['value'] : ''; if ( is_array( $val ) ) { $val = implode( ', ', $val ); } elseif ( ! is_string( $val ) ) { continue; } // Skip file upload fields using the field-type map loaded above. $element_type = isset( $field_types[ $field_name ] ) ? $field_types[ $field_name ] : ''; if ( 'upload' === $element_type ) { continue; } // Use the actual field label from the form definition when available. if ( isset( $label_map[ $field_name ] ) ) { $label = $label_map[ $field_name ]; } else { $label = ucwords( str_replace( array( '-', '_' ), ' ', $field_name ) ); } self::add_field( $all_fields, $label, $val ); // Detect message/textarea fields by type or by label/element_id. $ftype = isset( $field_types[ $field_name ] ) ? $field_types[ $field_name ] : ''; $name_lower = strtolower( $field_name ); $is_textarea = 'textarea' === $ftype || false !== strpos( $name_lower, 'message' ) || false !== strpos( $name_lower, 'textarea' ) || false !== strpos( $name_lower, 'comment' ); if ( $is_textarea && ! empty( $val ) ) { $message_parts[] = $val; } } } $form_fields_summary = self::build_fields_summary( $all_fields ); $extracted = self::extract_name_email( $all_fields ); $message = implode( "\n\n", $message_parts ); $entry_data = self::build_entry_data( 'Forminator', $form_name, $extracted['name'], $extracted['email'], $message, $form_fields_summary ); self::process_entry( $entry_data ); } /** * ═════════════════════════════════════════════════════════════════ * 9. JETPACK FORMS (Partial Support) * ═════════════════════════════════════════════════════════════════ * * @param int $post_id The post ID where the form was submitted. * @param array $all_values All submitted field values. * @param array $extra Extra data about the submission. */ public static function handle_jetpack( $post_id, $all_values, $extra ) { $form_name = __( 'Jetpack Form', 'forge-trueconversion' ); if ( $post_id ) { $post_title = get_the_title( $post_id ); if ( $post_title ) { /* translators: %s: Post title where the form was submitted */ $form_name = sprintf( __( 'Jetpack Form on: %s', 'forge-trueconversion' ), $post_title ); } } $all_fields = array(); $message_parts = array(); if ( is_array( $all_values ) ) { // Jetpack internal keys to skip. $skip_keys = array( 'is_spam', 'contact_form_id', 'comment_post_ID', 'utm_traffic_source' ); foreach ( $all_values as $key => $val ) { // Skip Jetpack internal keys and TrueConversion's own tracking fields. if ( in_array( $key, $skip_keys, true ) || self::is_internal_field_key( $key ) ) { continue; } if ( is_array( $val ) ) { $val = implode( ', ', $val ); } if ( ! is_string( $val ) ) { continue; } // Jetpack provides no field type. Detect uploads by value, and — // when the field key itself indicates an upload — match more // strictly (lenient) so filenames containing spaces, e.g. // "my resume.pdf", are caught too. $key_lower = strtolower( $key ); $is_upload_field = false !== strpos( $key_lower, 'file' ) || false !== strpos( $key_lower, 'upload' ) || false !== strpos( $key_lower, 'attach' ) || false !== strpos( $key_lower, 'document' ) || false !== strpos( $key_lower, 'resume' ) || false !== strpos( $key_lower, 'cv' ); if ( self::value_looks_like_file( $val, $is_upload_field ) ) { continue; } self::add_field( $all_fields, $key, $val ); // Detect message fields. $key_lower = strtolower( $key ); if ( ( false !== strpos( $key_lower, 'message' ) || false !== strpos( $key_lower, 'comment' ) || false !== strpos( $key_lower, 'question' ) || false !== strpos( $key_lower, 'brief' ) || false !== strpos( $key_lower, 'notes' ) ) && ! empty( $val ) ) { $message_parts[] = $val; } } } $form_fields_summary = self::build_fields_summary( $all_fields ); $extracted = self::extract_name_email( $all_fields ); $message = implode( "\n\n", $message_parts ); $entry_data = self::build_entry_data( 'Jetpack Forms', $form_name, $extracted['name'], $extracted['email'], $message, $form_fields_summary ); self::process_entry( $entry_data ); } /** * Jetpack: Add hidden fields to form HTML. * * @param string $field_html The field HTML. * @param string $field_label The field label. * @param int $post_id The post ID. * * @return string Modified field HTML. */ public static function handle_jetpack_fields( $field_html, $field_label, $post_id ) { static $added = false; if ( $added ) { return $field_html; } $added = true; $detection = FTCON_Cookie::get_detected_source(); $source = esc_attr( $detection['traffic_source'] ); $field_html .= ''; return $field_html; } /** * ═════════════════════════════════════════════════════════════════ * 10. FALLBACK SHORTCODE: [ftcon_fields] * ═════════════════════════════════════════════════════════════════ * * For custom forms not built with a supported plugin. * Place [ftcon_fields] inside your
tags and it will output * hidden input fields with the current UTM data. * * @return string Hidden input HTML. */ public static function shortcode_hidden_fields() { $utm_data = FTCON_Cookie::get_data(); $detection = FTCON_Cookie::get_detected_source(); $html = ''; $html .= ''; $utm_fields = array( 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content' ); foreach ( $utm_fields as $field ) { $value = isset( $utm_data[ $field ] ) ? $utm_data[ $field ] : ''; $html .= ''; } $html .= ''; $html .= ''; return $html; } /** * Get Supported Form Plugins * * @return array List of form plugin info. */ public static function get_supported_plugins() { return array( array( 'slug' => 'cf7', 'name' => __( 'Contact Form 7', 'forge-trueconversion' ), 'detect_by' => 'class', 'detect_name' => 'WPCF7', ), array( 'slug' => 'wpforms', 'name' => __( 'WPForms', 'forge-trueconversion' ), 'detect_by' => 'function', 'detect_name' => 'wpforms', ), array( 'slug' => 'gravityforms', 'name' => __( 'Gravity Forms', 'forge-trueconversion' ), 'detect_by' => 'class', 'detect_name' => 'GFForms', ), array( 'slug' => 'ninjaforms', 'name' => __( 'Ninja Forms', 'forge-trueconversion' ), 'detect_by' => 'class', 'detect_name' => 'Ninja_Forms', ), array( 'slug' => 'formidable', 'name' => __( 'Formidable Forms', 'forge-trueconversion' ), 'detect_by' => 'class', 'detect_name' => 'FrmAppHelper', ), array( 'slug' => 'fluentforms', 'name' => __( 'Fluent Forms', 'forge-trueconversion' ), 'detect_by' => 'constant', 'detect_name' => 'FLUENTFORM', ), array( 'slug' => 'elementor', 'name' => __( 'Elementor Pro Forms', 'forge-trueconversion' ), 'detect_by' => 'action', 'detect_name' => 'elementor/loaded', ), array( 'slug' => 'forminator', 'name' => __( 'Forminator', 'forge-trueconversion' ), 'detect_by' => 'class', 'detect_name' => 'Forminator', ), array( 'slug' => 'jetpack', 'name' => __( 'Jetpack Forms', 'forge-trueconversion' ), 'detect_by' => 'class', 'detect_name' => 'Jetpack', ), ); } /** * Detect Installed Form Plugins * * @return array Array of installed plugin info arrays. */ public static function detect_installed_plugins() { $installed = array(); foreach ( self::get_supported_plugins() as $plugin ) { $is_active = false; switch ( $plugin['detect_by'] ) { case 'class': $is_active = class_exists( $plugin['detect_name'] ); break; case 'function': $is_active = function_exists( $plugin['detect_name'] ); break; case 'constant': $is_active = defined( $plugin['detect_name'] ); break; case 'action': $is_active = did_action( $plugin['detect_name'] ) > 0; break; } if ( $is_active ) { $installed[] = $plugin; } } return $installed; } } // Initialize form integrations when WordPress is ready. add_action( 'init', array( 'FTCON_Form_Integrations', 'init' ) );