528 lines
17 KiB
PHP
528 lines
17 KiB
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( 'self::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;
|
|
}
|
|
}
|