JezK
Edit File: class-facebookwordpresswpforms.php
<?php /** * Facebook Pixel Plugin FacebookWordpressWPForms class. * * This file contains the main logic for FacebookWordpressWPForms. * * @package FacebookPixelPlugin */ /** * Define FacebookWordpressWPForms class. * * @return void */ /* * Copyright (C) 2017-present, Meta, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. */ namespace FacebookPixelPlugin\Integration; defined( 'ABSPATH' ) || die( 'Direct access not allowed' ); use FacebookPixelPlugin\Core\FacebookPixel; use FacebookPixelPlugin\Core\FacebookPluginUtils; use FacebookPixelPlugin\Core\FacebookServerSideEvent; use FacebookPixelPlugin\Core\FacebookWordPressOptions; use FacebookPixelPlugin\Core\ServerEventFactory; use FacebookPixelPlugin\Core\PixelRenderer; use FacebookPixelPlugin\FacebookAds\Object\ServerSide\Event; use FacebookPixelPlugin\FacebookAds\Object\ServerSide\UserData; /** * FacebookWordpressWPForms class. */ class FacebookWordpressWPForms extends FacebookWordpressIntegrationBase { const PLUGIN_FILE = 'wpforms-lite/wpforms.php'; const TRACKING_NAME = 'wpforms-lite'; /** * Hooks into WPForms to inject the Pixel code. * * This method adds an action to the 'wpforms_process_before' hook, * which will trigger the 'trackEvent' method. It ensures that * the Pixel code is injected during the form processing stage. */ public static function inject_pixel_code() { // Tracks server and browser events when a submission is processed. add_action( 'wpforms_process_before', array( __CLASS__, 'trackEvent' ), 20, 2 ); // Enriches AJAX responses (success or redirect) with pixel code. add_filter( 'wpforms_ajax_submit_success_response', array( __CLASS__, 'injectLeadEventAjax' ), 20, 3 ); add_filter( 'wpforms_ajax_submit_redirect', array( __CLASS__, 'injectLeadEventAjax' ), 20, 3 ); // Adds a front-end listener that fires pixel code returned in AJAX responses. add_action( 'wp_footer', array( __CLASS__, 'injectAjaxListener' ), 9 ); } /** * Tracks a server-side event for a form submission in WPForms. * * This method is hooked into the 'wpforms_process_before' action, which is * fired by WPForms before a form is processed. * It then calls the track method * on the FacebookServerSideEvent instance, which generates a lead event for * the form submission. * * If the user is an internal user, the method returns without tracking * any event. * * @param array $entry The form entry data. * @param array $form_data The form data. * * @return void */ public static function trackEvent( $entry, $form_data ) { if ( FacebookPluginUtils::is_internal_user() ) { return; } $server_event = ServerEventFactory::safe_create_event( 'Lead', array( __CLASS__, 'readFormData' ), array( $entry, $form_data ), self::TRACKING_NAME, true ); FacebookServerSideEvent::get_instance()->track( $server_event ); add_action( 'wp_footer', array( __CLASS__, 'injectLeadEvent' ), 20 ); } /** * Injects lead event code into the footer. * * This method retrieves tracked events from the FacebookServerSideEvent * instance and renders them into pixel code using the PixelRenderer. * The resulting code is printed into the footer section of the page. * If the user is an internal user, the method returns without injecting * any code. * * @return void */ public static function injectLeadEvent() { if ( FacebookPluginUtils::is_internal_user() ) { return; } $events = FacebookServerSideEvent::get_instance()->get_tracked_events(); $pixel_code = PixelRenderer::render( $events, self::TRACKING_NAME ); printf( ' <!-- Meta Pixel Event Code --> %s <!-- End Meta Pixel Event Code --> ', $pixel_code // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); } /** * Add pixel code into AJAX success/redirect responses. * * @param array $response Existing AJAX response payload. * @param int $form_id Form ID (provided by WPForms). * @param mixed $extra Unused extra parameter (URL or form data). * * @return array Modified response containing fb_pxl_code when available. */ public static function injectLeadEventAjax( $response, $form_id = null, $extra = null ) { if ( FacebookPluginUtils::is_internal_user() ) { return $response; } $events = FacebookServerSideEvent::get_instance()->get_tracked_events(); if ( empty( $events ) ) { return $response; } $response['fb_pxl_code'] = PixelRenderer::render( $events, self::TRACKING_NAME, false // Return raw fbq calls; they will be eval'd on the client. ); return $response; } /** * Outputs a JS listener that evaluates fb_pxl_code from WPForms AJAX responses. * * This covers the default WPForms AJAX path where the page is not reloaded * and no wp_footer hook is executed after submission. * * @return void */ public static function injectAjaxListener() { ?> <!-- Meta Pixel Event Code --> <script type='text/javascript'> (function ( $ ) { if ( ! $ || typeof document === 'undefined' ) { return; } // WPForms triggers this jQuery event and passes the AJAX response object. $( document ).on( 'wpformsAjaxSubmitSuccess', function ( event, data ) { if ( data && data.data && data.data.fb_pxl_code ) { try { new Function( data.data.fb_pxl_code )(); } catch ( e ) { console && console.warn && console.warn( 'Meta Pixel eval failed', e ); } } } ); })( window.jQuery ); </script> <!-- End Meta Pixel Event Code --> <?php } /** * Reads the form submission data and extracts user information. * * This method processes the form entry and form * data to extract user-related * information such as email, first name, last name, * and phone number. It also * retrieves the address data, including city, * state, country, and postal code. * * If either the form entry or form data is * empty, an empty array is returned. * * @param array $entry The form entry data. * @param array $form_data The form schema data. * * @return array An associative array * containing user and address information * extracted from the form entry. */ public static function readFormData( $entry, $form_data ) { if ( empty( $entry ) || empty( $form_data ) ) { return array(); } $name = self::getName( $entry, $form_data ); $event_data = array( 'email' => self::getEmail( $entry, $form_data ), 'first_name' => ! empty( $name ) ? $name[0] : null, 'last_name' => ! empty( $name ) ? $name[1] : null, 'phone' => self::getPhone( $entry, $form_data ), ); $event_data = array_merge( $event_data, self::getAddress( $entry, $form_data ) ); return $event_data; } /** * Retrieves the phone number from the form data. * * This method extracts the phone number field from the provided form entry * and form data. * * @param array $entry The form entry data. * @param array $form_data The form schema data. * * @return string|null The phone number, or null if no phone field is found. */ private static function getPhone( $entry, $form_data ) { $phone = self::getField( $entry, $form_data, 'phone' ); if ( ! is_null( $phone ) && '' !== $phone ) { return $phone; } return self::getTextFieldByLabel( $entry, $form_data, array( 'phone', 'tel', 'telephone', 'mobile' ) ); } /** * Retrieves the email address from the form data. * * This method extracts the email address field from the provided form entry * and form data. * * @param array $entry The form entry data. * @param array $form_data The form schema data. * * @return string|null The email address, or null * if no email field is found. */ private static function getEmail( $entry, $form_data ) { return self::getField( $entry, $form_data, 'email' ); } /** * Retrieves the address data from the form data. * * This method extracts the address data (city, state, country, and zip) * from the provided form entry * and form data. The country is sent in ISO format. * * Note that if the address scheme is 'us' and country * is not present, 'US' is used as the country. * * @param array $entry The form entry data. * @param array $form_data The form schema data. * * @return array The address data. */ private static function getAddress( $entry, $form_data ) { $address_field_data = self::getField( $entry, $form_data, 'address' ); if ( is_null( $address_field_data ) ) { // Fall back to individual text fields when the Address fancy field // is not available in WPForms Lite. return self::getAddressFromTextFields( $entry, $form_data ); } $address_data = array(); if ( isset( $address_field_data['city'] ) ) { $address_data['city'] = $address_field_data['city']; } if ( isset( $address_field_data['state'] ) ) { $address_data['state'] = $address_field_data['state']; } if ( isset( $address_field_data['country'] ) ) { $address_data['country'] = $address_field_data['country']; } else { $address_scheme = self::getAddressScheme( $form_data ); if ( 'us' === $address_scheme ) { $address_data['country'] = 'US'; } } if ( isset( $address_field_data['postal'] ) ) { $address_data['zip'] = $address_field_data['postal']; } return $address_data; } /** * Retrieves the user's name from the form data. * * This method extracts the name field from the provided form entry * and form data. It supports two formats: * - 'simple': where the name is a single string, * split into first and last name. * - 'first-last': where the name is provided as separate * 'first' and 'last' fields. * * @param array $entry The form entry data. * @param array $form_data The form schema data. * * @return array|null An array containing the first and * last name, or null if no name field is found. */ private static function getName( $entry, $form_data ) { if ( empty( $form_data['fields'] ) || empty( $entry['fields'] ) ) { return null; } $entries = $entry['fields']; foreach ( $form_data['fields'] as $field ) { if ( 'name' === $field['type'] ) { if ( 'simple' === $field['format'] ) { return ServerEventFactory::split_name( $entries[ $field['id'] ] ); } elseif ( 'first-last' === $field['format'] ) { return array( $entries[ $field['id'] ]['first'], $entries[ $field['id'] ]['last'], ); } } } return null; } /** * Retrieves the value of a specific field type from the form entry data. * * This method searches through the form schema data to find a field of * the specified type and returns the corresponding value from the form * entry data. * * @param array $entry The form entry data. * @param array $form_data The form schema data. * @param string $type The type of the field to retrieve. * * @return mixed|null The value of the field, or null if no * field of the specified type is found. */ private static function getField( $entry, $form_data, $type ) { if ( empty( $form_data['fields'] ) || empty( $entry['fields'] ) ) { return null; } foreach ( $form_data['fields'] as $field ) { if ( $field['type'] === $type ) { return $entry['fields'][ $field['id'] ]; } } return null; } /** * Retrieves a text field value by matching its label. * * WPForms Lite users often rely on generic "text" fields instead of * the premium/fancy types. This helper lets us recover values for * phone/address-like fields when their labels match expected names. * * @param array $entry The form entry data. * @param array $form_data The form schema data. * @param string[] $labels Candidate labels (case-insensitive). * @return string|null */ private static function getTextFieldByLabel( $entry, $form_data, $labels ) { if ( empty( $form_data['fields'] ) || empty( $entry['fields'] ) ) { return null; } $normalized_labels = array_map( array( self::class, 'normalizeLabel' ), $labels ); foreach ( $form_data['fields'] as $field ) { if ( 'text' !== $field['type'] || empty( $field['label'] ) ) { continue; } $label = self::normalizeLabel( $field['label'] ); if ( in_array( $label, $normalized_labels, true ) ) { $value = isset( $entry['fields'][ $field['id'] ] ) ? $entry['fields'][ $field['id'] ] : null; return '' !== $value ? $value : null; } } return null; } /** * Builds address data from individual text fields when the Address field * isn't present. * * @param array $entry The form entry data. * @param array $form_data The form schema data. * * @return array */ private static function getAddressFromTextFields( $entry, $form_data ) { $address_data = array(); $address_data['city'] = self::getTextFieldByLabel( $entry, $form_data, array( 'city', 'town' ) ); $address_data['state'] = self::getTextFieldByLabel( $entry, $form_data, array( 'state', 'province', 'region', 'county' ) ); $address_data['country'] = self::getTextFieldByLabel( $entry, $form_data, array( 'country', 'country/region' ) ); $address_data['zip'] = self::getTextFieldByLabel( $entry, $form_data, array( 'zip', 'postal', 'postcode', 'zip code' ) ); // Remove null/empty values so we don't send sparse keys. return array_filter( $address_data, function ( $value ) { return ! is_null( $value ) && '' !== $value; } ); } /** * Normalizes labels for case-insensitive comparison. * * @param string $label The label to normalize. * @return string */ private static function normalizeLabel( $label ) { return strtolower( trim( $label ) ); } /** * Retrieves the address scheme from the form data. * * This method searches through the form schema data to find the first * 'address' field and returns its 'scheme' value, which is either 'us' or * 'international'. If no address field is found, or if the address field * does not have a scheme, this method returns null. * * @param array $form_data The form schema data. * * @return string|null The address scheme, or * null if no address field is found. */ private static function getAddressScheme( $form_data ) { foreach ( $form_data['fields'] as $field ) { if ( 'address' === $field['type'] ) { if ( isset( $field['scheme'] ) ) { return $field['scheme']; } } } return null; } }