JezK
Edit File: facebook-commerce-pixel-event.php
<?php /** * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * * @package MetaCommerce */ use WooCommerce\Facebook\Events\Event; use WooCommerce\Facebook\Events\FacebookSignalsState; /** * Class WC_Facebookcommerce_Pixel * * This class initializes the Facebook Pixel and provides methods to track events. */ class WC_Facebookcommerce_Pixel { const SETTINGS_KEY = 'facebook_config'; const PIXEL_ID_KEY = 'pixel_id'; const USE_PII_KEY = 'use_pii'; const USE_S2S_KEY = 'use_s2s'; const ACCESS_TOKEN_KEY = 'access_token'; /** * Cache key for pixel script block output. * * @var string cache key. * */ const PIXEL_RENDER = 'pixel_render'; /** * Cache key for pixel noscript block output. * * @var string cache key. * */ const NO_SCRIPT_RENDER = 'no_script_render'; /** * Script render memoization helper. * * @var array Cache array. */ public static $render_cache = []; /** * Queued pixel events for isolated script execution. * * Events are collected here and output via wp_localize_script() to an external * JS file, preventing errors from other plugins breaking pixel tracking. * * @var array Queued events array. */ private static $event_queue = []; /** * Whether external script has been enqueued. * * @var bool */ private static $script_enqueued = false; /** * Whether hooks have been initialized. * * @var bool */ private static $hooks_initialized = false; /** * User information. * * @var array Information array. */ private $user_info; /** * The name of the last event. * * @var string Event name. */ private $last_event; /** * Class constructor. * * @param array $user_info User information array. */ public function __construct( $user_info = [] ) { $this->user_info = $user_info; $this->last_event = ''; } /** * Initialize hooks for external JavaScript event handling. * Uses wp_localize_script() + external JS file to prevent JavaScript errors * from other plugins breaking our pixel tracking. */ public static function init_external_js_hooks() { if ( self::$hooks_initialized ) { return; } self::$hooks_initialized = true; // Deferred events from previous page are loaded via WC_Facebookcommerce_Utils::print_deferred_events() // which is hooked to wp_head in facebook-commerce-events-tracker.php. add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_pixel_events_script' ) ); // Pass event data to JavaScript before footer scripts. add_action( 'wp_footer', array( __CLASS__, 'localize_pixel_events_data' ), 5 ); } /** * Enqueues the external pixel events script. * External script runs in isolated context - not affected by other plugin errors. */ public static function enqueue_pixel_events_script() { if ( self::$script_enqueued ) { return; } $pixel_id = self::get_pixel_id(); if ( empty( $pixel_id ) ) { return; } self::$script_enqueued = true; wp_enqueue_script( 'wc-facebook-pixel-events', plugins_url( 'assets/js/frontend/pixel-events.js', __FILE__ ), array(), WC_Facebookcommerce_Utils::PLUGIN_VERSION, true // Load in footer, after fbq is initialized. ); } /** * Passes queued event data to the frontend JavaScript. * Uses wp_localize_script() to pass data (not code) to the external script. */ public static function localize_pixel_events_data() { if ( ! self::$script_enqueued || empty( self::$event_queue ) ) { return; } $pixel_id = self::get_pixel_id(); wp_localize_script( 'wc-facebook-pixel-events', 'wc_facebook_pixel_data', array( 'pixelId' => esc_js( $pixel_id ), 'eventQueue' => self::$event_queue, 'agentString' => Event::get_platform_identifier(), ) ); } /** * Enqueue an event for isolated script execution. * Events are stored as DATA, not executable code. * * @param string $event_name The name of the event to track. * @param array $params Event parameters. * @param string $method The fbq method to use (track, trackCustom, etc.). * @param string $event_id Optional event ID for deduplication. */ public static function enqueue_event( $event_name, $params, $method = 'track', $event_id = '' ) { // Initialize hooks if not already done. self::init_external_js_hooks(); $event_data = array( 'name' => $event_name, 'params' => $params, 'method' => $method, ); if ( ! empty( $event_id ) ) { $event_data['eventId'] = $event_id; } self::$event_queue[] = $event_data; } /** * Enqueue an event for deferred execution on next page load. * Used when events need to be deferred (e.g., AddToCart with redirect). * * @param string $event_name The name of the event to track. * @param array $params Event parameters. * @param string $method The fbq method to use (track, trackCustom, etc.). * @param string $event_id Optional event ID for deduplication. */ public static function enqueue_deferred_event( $event_name, $params, $method = 'track', $event_id = '' ) { $event_data = array( 'name' => $event_name, 'params' => $params, 'method' => $method, ); if ( ! empty( $event_id ) ) { $event_data['eventId'] = $event_id; } WC_Facebookcommerce_Utils::add_deferred_event( $event_data ); } /** * Prepares event parameters for pixel tracking. * * Extracts event_id, unwraps custom_data, and applies build_params(). * * @param array $params Raw event parameters. * @param string $event_name The name of the event. * @return array ['params' => array, 'event_id' => string] */ private static function prepare_event_params( $params, $event_name ) { $event_id = ''; // Do not send the event name in the params. if ( isset( $params['event_name'] ) ) { unset( $params['event_name'] ); } /** * If possible, send the event ID to avoid duplication. * * @see https://developers.facebook.com/docs/marketing-api/server-side-api/deduplicate-pixel-and-server-side-events#deduplication-best-practices */ if ( isset( $params['event_id'] ) ) { $event_id = $params['event_id']; unset( $params['event_id'] ); } // If custom_data is set, extract it (send only the inner data). if ( isset( $params['custom_data'] ) ) { $params = $params['custom_data']; } // Apply build_params() to add version info and apply filters. $params = self::build_params( $params, $event_name ); return array( 'params' => $params, 'event_id' => $event_id, ); } /** * Initialize pixelID. */ public static function initialize() { if ( ! is_admin() ) { return; } // Initialize PixelID in storage - this will only need to happen when the user is an admin. $pixel_id = self::get_pixel_id(); if ( ! WC_Facebookcommerce_Utils::is_valid_id( $pixel_id ) && class_exists( 'WC_Facebookcommerce_WarmConfig' ) ) { $fb_warm_pixel_id = WC_Facebookcommerce_WarmConfig::$fb_warm_pixel_id; // phpcs:disable Universal.Operators.StrictComparisons.LooseEqual if ( WC_Facebookcommerce_Utils::is_valid_id( $fb_warm_pixel_id ) && (int) $fb_warm_pixel_id == $fb_warm_pixel_id ) { $fb_warm_pixel_id = (string) $fb_warm_pixel_id; self::set_pixel_id( $fb_warm_pixel_id ); } } $is_advanced_matching_enabled = self::get_use_pii_key(); //phpcs:disable Universal.Operators.StrictComparisons.LooseEqual if ( null == $is_advanced_matching_enabled && class_exists( 'WC_Facebookcommerce_WarmConfig' ) ) { $fb_warm_is_advanced_matching_enabled = WC_Facebookcommerce_WarmConfig::$fb_warm_is_advanced_matching_enabled; if ( is_bool( $fb_warm_is_advanced_matching_enabled ) ) { self::set_use_pii_key( $fb_warm_is_advanced_matching_enabled ? 1 : 0 ); } } } /** * Gets Facebook Pixel init code. * * Init code might contain additional information to help matching website users with facebook users. * Information is hashed in JS side using SHA256 before sending to Facebook. * * @return string */ private function get_pixel_init_code() { $agent_string = Event::get_platform_identifier(); /** * Filters Facebook Pixel init code. * * @param string $js_code */ return apply_filters( 'facebook_woocommerce_pixel_init', sprintf( "fbq('init', '%s', %s, %s);\n", esc_js( self::get_pixel_id() ), wp_json_encode( $this->user_info, JSON_PRETTY_PRINT | JSON_FORCE_OBJECT ), wp_json_encode( array( 'agent' => $agent_string ), JSON_PRETTY_PRINT | JSON_FORCE_OBJECT ) ) ); } /** * Gets the Facebook Pixel code scripts. * * @return string HTML scripts * * phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped */ public function pixel_base_code() { $pixel_id = self::get_pixel_id(); // Bail if no ID or already rendered. if ( empty( $pixel_id ) || ! empty( self::$render_cache[ self::PIXEL_RENDER ] ) ) { return ''; } self::$render_cache[ self::PIXEL_RENDER ] = true; ob_start(); $is_held = FacebookSignalsState::is_held(); ?> <script <?php echo self::get_script_attributes(); ?>> !function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod? n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n; n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0; t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window, document,'script','https://connect.facebook.net/en_US/fbevents.js'); </script> <!-- WooCommerce Facebook Integration Begin --> <script <?php echo self::get_script_attributes(); ?>> <?php echo self::get_facebook_signals_js(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> <?php if ( $is_held ) : ?> fbq('consent', 'revoke'); <?php endif; ?> <?php echo $this->get_pixel_init_code(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> <?php if ( $is_held ) : ?> FacebookSignals.init({ held: true, ajaxUrl: <?php echo wp_json_encode( admin_url( 'admin-ajax.php' ) ); ?>, nonce: <?php echo wp_json_encode( wp_create_nonce( 'facebook_release_signals' ) ); ?>, action: 'facebook_release_signals', pixelId: <?php echo wp_json_encode( self::get_pixel_id() ); ?>, attribution: { fbp: <?php echo wp_json_encode( FacebookSignalsState::get_attribution_data( 'fbp' ) ); ?>, fbc: <?php echo wp_json_encode( FacebookSignalsState::get_attribution_data( 'fbc' ) ); ?>, fbpDomain: <?php echo wp_json_encode( FacebookSignalsState::get_attribution_data( 'fbp_domain' ) ); ?>, fbcDomain: <?php echo wp_json_encode( FacebookSignalsState::get_attribution_data( 'fbc_domain' ) ); ?> } }); <?php else : ?> FacebookSignals.init({ held: false }); <?php endif; ?> document.addEventListener( 'DOMContentLoaded', function() { // Insert placeholder for events injected when a product is added to the cart through AJAX. document.body.insertAdjacentHTML( 'beforeend', '<div class=\"wc-facebook-pixel-event-placeholder\"></div>' ); }, false ); </script> <!-- WooCommerce Facebook Integration End --> <?php return ob_get_clean(); } /** * Gets Facebook Pixel code noscript part to avoid W3 validation errors. * * @return string */ public function pixel_base_code_noscript() { $pixel_id = self::get_pixel_id(); if ( empty( $pixel_id ) || ! empty( self::$render_cache[ self::NO_SCRIPT_RENDER ] ) ) { return ''; } self::$render_cache[ self::NO_SCRIPT_RENDER ] = true; ob_start(); ?> <!-- Facebook Pixel Code --> <noscript> <img height="1" width="1" style="display:none" alt="fbpx" src="https://www.facebook.com/tr?id=<?php echo esc_attr( $pixel_id ); ?>&ev=PageView&noscript=1" /> </noscript> <!-- End Facebook Pixel Code --> <?php return ob_get_clean(); } /** * Gets the inline FacebookSignals JS API definition. * * @since 3.6.0 * * @return string JavaScript code defining window.FacebookSignals. */ private static function get_facebook_signals_js() { // phpcs:disable return <<<'JS' window.FacebookSignals = window.FacebookSignals || { _held: false, _queue: [], _config: {}, _attribution: {}, _seenEventIds: {}, _fbclid: (function() { try { var m = window.location.search.match(/[?&]fbclid=([^&]*)/); return m ? decodeURIComponent(m[1]) : null; } catch(e) { return null; } })(), init: function(config) { config = config || {}; this._config = config; this._attribution = config.attribution || {}; this._held = !!config.held; this._fbclid = this._fbclid || null; try { var raw = window.sessionStorage.getItem('wc_facebook_signals_seen_event_ids'); this._seenEventIds = raw ? JSON.parse(raw) : {}; } catch (e) { this._seenEventIds = this._seenEventIds || {}; } }, queueEvent: function(eventData) { if (!eventData || !eventData.event_name) return; if (eventData.event_id && this._seenEventIds[eventData.event_id]) return; eventData.event_time = eventData.event_time || Math.floor(Date.now() / 1000); this._queue.push(eventData); if (eventData.event_id) { this._seenEventIds[eventData.event_id] = 1; try { window.sessionStorage.setItem( 'wc_facebook_signals_seen_event_ids', JSON.stringify(this._seenEventIds) ); } catch (e) {} } }, trackEvent: function(name, params, userData) { if (this._held) { this.queueEvent({ event_name: name, custom_data: params || {}, user_data: userData || {}, event_id: (params && params.eventID) || null, event_time: Math.floor(Date.now() / 1000) }); } else { if (params && params.eventID) { fbq('track', name, params, { eventID: params.eventID }); } else { fbq('track', name, params); } } }, release: function() { var self = this; if (!self._held || !self._config.ajaxUrl) { return Promise.resolve({ success: true, data: { sent_count: 0 } }); } var payload = JSON.stringify({ security: self._config.nonce, events: self._queue, attribution: { fbp: self._attribution.fbp || null, fbc: self._attribution.fbc || null } }); // Pass action as a query parameter so WordPress can route the request. var url = self._config.ajaxUrl + (self._config.ajaxUrl.indexOf('?') === -1 ? '?' : '&') + 'action=' + encodeURIComponent(self._config.action); return new Promise(function(resolve, reject) { var xhr = new XMLHttpRequest(); xhr.open('POST', url, true); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.onload = function() { if (xhr.status >= 200 && xhr.status < 300) { try { var resp = JSON.parse(xhr.responseText); self._handleReleaseResponse(resp.data || {}); resolve(resp); } catch(e) { reject(e); } } else { reject(new Error('Signal release AJAX failed: ' + xhr.status)); } }; xhr.onerror = function() { reject(new Error('Network error')); }; xhr.send(payload); }); }, _handleReleaseResponse: function(data) { this._syncAttributionCookies(data || {}); // Re-enable the browser signal path so fbevents.js starts firing. fbq('consent', 'grant'); // Replay queued events via the pixel. var queue = this._queue; for (var i = 0; i < queue.length; i++) { var ev = queue[i]; if (ev.event_id) { fbq('track', ev.event_name, ev.custom_data || {}, { eventID: ev.event_id }); } else { fbq('track', ev.event_name, ev.custom_data || {}); } } // Clear the queue and mark signals as active again. this._queue = []; this._held = false; }, _syncAttributionCookies: function(data) { var clientParams = {}; if (typeof clientParamBuilder !== 'undefined') { try { // Let the client-side ParamBuilder generate/set missing attribution // cookies using its normal domain-scoping logic. clientParams = clientParamBuilder.processAndCollectParams(this._getAttributionUrl()) || {}; } catch (e) {} } var fbp = data.fbp || clientParams._fbp || (typeof clientParamBuilder !== 'undefined' ? clientParamBuilder.getFbp() : null); var fbc = data.fbc || clientParams._fbc || (typeof clientParamBuilder !== 'undefined' ? clientParamBuilder.getFbc() : null); // If the backend supplied exact values used for CAPI, write them so Pixel // replay and CAPI use matching attribution. if (data.fbp) { this._setAttributionCookie('_fbp', fbp, data.fbp_domain || data.cookie_domain || this._attribution.fbpDomain); } if (data.fbc) { this._setAttributionCookie('_fbc', fbc, data.fbc_domain || data.cookie_domain || this._attribution.fbcDomain || this._attribution.fbpDomain); } }, _setAttributionCookie: function(name, value, domain) { if (!value) return; var domainAttr = domain ? ';domain=' + domain : ''; document.cookie = name + '=' + encodeURIComponent(value) + ';path=/;max-age=7776000' + domainAttr + ';SameSite=Lax'; }, _getAttributionUrl: function() { if (!this._fbclid) { return window.location.href; } try { var url = new URL(window.location.href); if (!url.searchParams.get('fbclid')) { url.searchParams.set('fbclid', this._fbclid); } return url.toString(); } catch (e) { return window.location.href; } } }; JS; // phpcs:enable } /** * Gets JS code that queues an event via FacebookSignals while signals are held. * * Captures PII at event-generation time so it's included when events are replayed. * * @since 3.6.0 * * @param string $event_name The event name. * @param array $params Event parameters including custom_data, user_data, event_id. * @return string JavaScript code. */ public function get_queued_event_code( $event_name, $params ) { $this->last_event = $event_name; $event_id = isset( $params['event_id'] ) ? $params['event_id'] : null; $queue_data = ! empty( $event_id ) ? FacebookSignalsState::get_queued_event( $event_id ) : null; if ( ! is_array( $queue_data ) ) { $queue_data = array( 'event_name' => $event_name, 'custom_data' => isset( $params['custom_data'] ) ? $params['custom_data'] : $params, 'event_id' => $event_id, 'event_time' => time(), ); if ( isset( $params['user_data'] ) ) { $queue_data['user_data'] = $params['user_data']; } } if ( empty( $queue_data['event_name'] ) ) { $queue_data['event_name'] = $event_name; } return sprintf( "/* %s Facebook Integration Event Queueing */\nFacebookSignals.queueEvent(%s);", \WC_Facebookcommerce_Utils::get_integration_name(), wp_json_encode( $queue_data ) ); } /** * Determines if the last event in the current thread matches a given event. * * @since 1.11.0 * * @param string $event_name * @return bool */ public function is_last_event( $event_name ) { return $event_name === $this->last_event; } /** * Gets the JavaScript code to track an event. * * Updates the last event property and returns the code. * * Use {@see \WC_Facebookcommerce_Pixel::inject_event()} to print or enqueue the code. * * @since 1.10.2 * * @param string $event_name The name of the event to track. * @param array $params Custom event parameters. * @param string $method Name of the pixel's fbq() function to call. * @return string */ public function get_event_code( $event_name, $params, $method = 'track' ) { $this->last_event = $event_name; return self::build_event( $event_name, $params, $method ); } /** * Gets the JavaScript code to track an event wrapped in <script> tag. * * @see \WC_Facebookcommerce_Pixel::get_event_code() * * @since 1.10.2 * * @param string $event_name The name of the event to track. * @param array $params Custom event parameters. * @param string $method Name of the pixel's fbq() function to call. * @return string * * phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped */ public function get_event_script( $event_name, $params, $method = 'track' ) { ob_start(); ?> <!-- Facebook Pixel Event Code --> <script <?php echo self::get_script_attributes(); ?>> <?php if ( FacebookSignalsState::is_held() ) { echo $this->get_queued_event_code( $event_name, $params ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } else { echo $this->get_event_code( $event_name, $params, $method ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } ?> </script> <!-- End Facebook Pixel Event Code --> <?php return ob_get_clean(); } /** * Prints or enqueues the JavaScript code to track an event. * Preferred method to inject events in a page. * * Supports two execution modes controlled by rollout switch: * - Isolated execution (switch ON): Uses external JS via wp_localize_script() to prevent * other plugins' JavaScript errors from breaking pixel tracking. * - Legacy execution (switch OFF): Uses enqueue_inline_js() for inline script output. * * @see \WC_Facebookcommerce_Pixel::build_event() * * @param string $event_name The name of the event to track. * @param array $params Custom event parameters. * @param string $method Name of the pixel's fbq() function to call. * * phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped */ public function inject_event( $event_name, $params, $method = 'track' ) { // When signals are held, queue events in the browser instead of firing them. if ( FacebookSignalsState::is_held() ) { $code = $this->get_queued_event_code( $event_name, $params ); if ( WC_Facebookcommerce_Utils::is_woocommerce_integration() ) { WC_Facebookcommerce_Utils::enqueue_inline_js( $code ); } else { printf( '<!-- Facebook Pixel Event Code --><script %s>%s</script><!-- End Facebook Pixel Event Code -->', self::get_script_attributes(), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped $code // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); } return; } if ( WC_Facebookcommerce_Utils::is_woocommerce_integration() ) { // Check rollout switch for isolated pixel execution. // When enabled, pixel events are output via external JS file (wp_localize_script) // to prevent other plugins' JavaScript errors from breaking pixel tracking. $is_isolated_pixel_execution_enabled = facebook_for_woocommerce()->get_rollout_switches()->is_switch_enabled( \WooCommerce\Facebook\RolloutSwitches::SWITCH_ISOLATED_PIXEL_EXECUTION_ENABLED ); // If we have add to cart redirect enabled, we must defer the AddToCart events to render them the next page load. $is_redirect = 'yes' === get_option( 'woocommerce_cart_redirect_after_add', 'no' ); $is_add_to_cart = 'AddToCart' === $event_name; $is_deferred = $is_redirect && $is_add_to_cart; if ( $is_isolated_pixel_execution_enabled ) { // Isolated execution: Use external JS via wp_localize_script. // Set last_event here since we don't call get_event_code() in this path. $this->last_event = $event_name; [ 'params' => $event_params, 'event_id' => $event_id ] = self::prepare_event_params( $params, $event_name ); if ( $is_deferred ) { // Store event data for next page load. self::enqueue_deferred_event( $event_name, $event_params, $method, $event_id ); } else { // Queue event for this page's external script. self::enqueue_event( $event_name, $event_params, $method, $event_id ); } } else { // Legacy execution: Use enqueue_inline_js for inline script. $code = $this->get_event_code( $event_name, self::build_params( $params, $event_name ), $method ); if ( $is_deferred ) { // Store JS code string for inline script at print time. WC_Facebookcommerce_Utils::add_deferred_event( $code ); } else { WC_Facebookcommerce_Utils::enqueue_inline_js( $code ); } } } else { printf( $this->get_event_script( $event_name, self::build_params( $params, $event_name ), $method ) ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped } } /** * Gets the JavaScript code to track a conditional event wrapped in <script> tag. * * @see \WC_Facebookcommerce_Pixel::get_event_code() * * @since 1.10.2 * * @param string $event_name The name of the event to track. * @param array $params Custom event parameters. * @param string $listener Name of the JavaScript event to listen for. * @param string $jsonified_pii JavaScript code representing an object of data for Advanced Matching. * @return string * * phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped */ public function get_conditional_event_script( $event_name, $params, $listener, $jsonified_pii ) { $this->last_event = $event_name; // When signals are held, use FacebookSignals.trackEvent() to queue the event. if ( FacebookSignalsState::is_held() ) { $user_data_js = $jsonified_pii ? $jsonified_pii : '{}'; ob_start(); ?> <!-- Facebook Pixel Event Code --> <script <?php echo self::get_script_attributes(); ?>> document.addEventListener( '<?php echo esc_js( $listener ); ?>', function (event) { FacebookSignals.trackEvent( <?php echo wp_json_encode( $event_name ); ?>, <?php echo wp_json_encode( $params ); ?>, <?php echo $user_data_js; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> ); }, false ); </script> <!-- End Facebook Pixel Event Code --> <?php return ob_get_clean(); } $code = self::build_event( $event_name, $params, 'track' ); /** * TODO: use the settings stored by {@see \WC_Facebookcommerce_Integration}. * The use_pii setting here is currently always disabled regardless of * the value configured in the plugin settings page {WV-2020-01-02}. */ // Prepends fbq(...) with pii information to the injected code. if ( $jsonified_pii && get_option( self::SETTINGS_KEY )[ self::USE_PII_KEY ] ) { $this->user_info = '%s'; $code = sprintf( $this->get_pixel_init_code(), '" || ' . $jsonified_pii . ' || "' ) . $code; } ob_start(); ?> <!-- Facebook Pixel Event Code --> <script <?php echo self::get_script_attributes(); ?>> document.addEventListener( '<?php echo esc_js( $listener ); ?>', function (event) { <?php echo $code; ?> }, false ); </script> <!-- End Facebook Pixel Event Code --> <?php return ob_get_clean(); } /** * Prints the JavaScript code to track a conditional event. * * The tracking code will be executed when the given JavaScript event is triggered. * * @param string $event_name Name of the event. * @param array $params Custom event parameters. * @param string $listener Name of the JavaScript event to listen for. * @param string $jsonified_pii JavaScript code representing an object of data for Advanced Matching. * @return string */ public function inject_conditional_event( $event_name, $params, $listener, $jsonified_pii = '' ) { // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped return $this->get_conditional_event_script( $event_name, self::build_params( $params, $event_name ), $listener, $jsonified_pii ); } /** * Gets the JavaScript code to track a conditional event that is only triggered one time wrapped in <script> tag. * * @internal * * @since 1.10.2 * * @param string $event_name The name of the event to track. * @param array $params Custom event parameters. * @param string $listened_event Name of the JavaScript event to listen for. * @return string * * phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped */ public function get_conditional_one_time_event_script( $event_name, $params, $listened_event ) { // When signals are held, queue via FacebookSignals.trackEvent(). if ( FacebookSignalsState::is_held() ) { $this->last_event = $event_name; ob_start(); ?> <!-- Facebook Pixel Event Code --> <script <?php echo self::get_script_attributes(); ?>> function handle<?php echo $event_name; ?>Event() { FacebookSignals.trackEvent(<?php echo wp_json_encode( $event_name ); ?>, <?php echo wp_json_encode( $params ); ?>); jQuery( document.body ).off( '<?php echo esc_js( $listened_event ); ?>', handle<?php echo $event_name; ?>Event ); } jQuery( document.body ).one( '<?php echo esc_js( $listened_event ); ?>', handle<?php echo $event_name; ?>Event ); </script> <!-- End Facebook Pixel Event Code --> <?php return ob_get_clean(); } $code = $this->get_event_code( $event_name, $params ); ob_start(); ?> <!-- Facebook Pixel Event Code --> <script <?php echo self::get_script_attributes(); ?>> function handle<?php echo $event_name; ?>Event() { <?php echo $code; ?> // Some weird themes (hi, Basel) are running this script twice, so two listeners are added and we need to remove them after running one. jQuery( document.body ).off( '<?php echo esc_js( $listened_event ); ?>', handle<?php echo $event_name; ?>Event ); } jQuery( document.body ).one( '<?php echo esc_js( $listened_event ); ?>', handle<?php echo $event_name; ?>Event ); </script> <!-- End Facebook Pixel Event Code --> <?php return ob_get_clean(); } /** * Builds an event. * * @see \WC_Facebookcommerce_Pixel::inject_event() for the preferred method to inject an event. * * @param string $event_name Event name. * @param array $params Event params. * @param string $method Optional, defaults to 'track'. * @return string */ public static function build_event( $event_name, $params, $method = 'track' ) { // Reuse shared param preparation logic. [ 'params' => $event_params, 'event_id' => $event_id ] = self::prepare_event_params( $params, $event_name ); if ( ! empty( $event_id ) ) { $event = sprintf( "/* %s Facebook Integration Event Tracking */\n" . "fbq('set', 'agent', '%s', '%s');\n" . "fbq('%s', '%s', %s, %s);", WC_Facebookcommerce_Utils::get_integration_name(), Event::get_platform_identifier(), self::get_pixel_id(), esc_js( $method ), esc_js( $event_name ), wp_json_encode( $event_params, JSON_PRETTY_PRINT | JSON_FORCE_OBJECT ), wp_json_encode( array( 'eventID' => $event_id ), JSON_PRETTY_PRINT | JSON_FORCE_OBJECT ) ); } else { $event = sprintf( "/* %s Facebook Integration Event Tracking */\n" . "fbq('set', 'agent', '%s', '%s');\n" . "fbq('%s', '%s', %s);", WC_Facebookcommerce_Utils::get_integration_name(), Event::get_platform_identifier(), self::get_pixel_id(), esc_js( $method ), esc_js( $event_name ), wp_json_encode( $event_params, JSON_PRETTY_PRINT | JSON_FORCE_OBJECT ) ); } return $event; } /** * Gets an array with version_info for pixel fires. * * Parameters provided by users should not be overwritten by this function. * * @since 1.10.2 * * @param array $params User defined parameters. * @param string $event The event name the params are for. * @return array */ private static function build_params( $params = [], $event = '' ) { $params = array_replace( Event::get_version_info(), $params ); /** * Filters the parameters for the pixel code. * * @since 1.10.2 * * @param array $params User defined parameters. * @param string $event The event name. */ return (array) apply_filters( 'wc_facebook_pixel_params', $params, $event ); } /** * Gets script tag attributes. * * @since 1.10.2 * * @return string */ private static function get_script_attributes() { $script_attributes = ''; /** * Filters Facebook Pixel script attributes. * * @since 1.10.2 * * @param array $custom_attributes */ $custom_attributes = (array) apply_filters( 'wc_facebook_pixel_script_attributes', array( 'type' => 'text/javascript' ) ); foreach ( $custom_attributes as $tag => $value ) { $script_attributes .= ' ' . $tag . '="' . esc_attr( $value ) . '"'; } return $script_attributes; } /** * Get the PixelId. */ public static function get_pixel_id() { $fb_options = self::get_options(); if ( ! $fb_options ) { return ''; } return isset( $fb_options[ self::PIXEL_ID_KEY ] ) ? $fb_options[ self::PIXEL_ID_KEY ] : ''; } /** * Set the PixelId. * * @param string $pixel_id PixelId. */ public static function set_pixel_id( $pixel_id ) { $fb_options = self::get_options(); if ( isset( $fb_options[ self::PIXEL_ID_KEY ] ) && $fb_options[ self::PIXEL_ID_KEY ] === $pixel_id ) { return; } $fb_options[ self::PIXEL_ID_KEY ] = $pixel_id; update_option( self::SETTINGS_KEY, $fb_options ); } /** * Check if PII key use is enabled. */ public static function get_use_pii_key() { $fb_options = self::get_options(); if ( ! $fb_options ) { return null; } return isset( $fb_options[ self::USE_PII_KEY ] ) ? $fb_options[ self::USE_PII_KEY ] : null; } /** * Enable or disable use of PII key. * * @param string $use_pii PII key. */ public static function set_use_pii_key( $use_pii ) { $fb_options = self::get_options(); if ( isset( $fb_options[ self::USE_PII_KEY ] ) && $fb_options[ self::USE_PII_KEY ] === $use_pii ) { return; } $fb_options[ self::USE_PII_KEY ] = $use_pii; update_option( self::SETTINGS_KEY, $fb_options ); } /** * Check if S2S is set. */ public static function get_use_s2s() { $fb_options = self::get_options(); if ( ! $fb_options ) { return false; } return isset( $fb_options[ self::USE_S2S_KEY ] ) ? $fb_options[ self::USE_S2S_KEY ] : false; } /** * Enable or disable use of S2S key. * * @param string $use_s2s S2S setting. */ public static function set_use_s2s( $use_s2s ) { $fb_options = self::get_options(); if ( isset( $fb_options[ self::USE_S2S_KEY ] ) && $fb_options[ self::USE_S2S_KEY ] === $use_s2s ) { return; } $fb_options[ self::USE_S2S_KEY ] = $use_s2s; update_option( self::SETTINGS_KEY, $fb_options ); } /** * Get access token. */ public static function get_access_token() { $fb_options = self::get_options(); if ( ! $fb_options ) { return ''; } return isset( $fb_options[ self::ACCESS_TOKEN_KEY ] ) ? $fb_options[ self::ACCESS_TOKEN_KEY ] : ''; } /** * Set access token. * * @param string $access_token Access token. */ public static function set_access_token( $access_token ) { $fb_options = self::get_options(); if ( isset( $fb_options[ self::ACCESS_TOKEN_KEY ] ) && $fb_options[ self::ACCESS_TOKEN_KEY ] === $access_token ) { return; } $fb_options[ self::ACCESS_TOKEN_KEY ] = $access_token; update_option( self::SETTINGS_KEY, $fb_options ); } /** * Get WooCommerce/Wordpress information. */ private static function get_version_info() { global $wp_version; if ( WC_Facebookcommerce_Utils::is_woocommerce_integration() ) { return array( 'source' => 'woocommerce', 'version' => WC()->version, 'pluginVersion' => WC_Facebookcommerce_Utils::PLUGIN_VERSION, ); } return array( 'source' => 'wordpress', 'version' => $wp_version, 'pluginVersion' => WC_Facebookcommerce_Utils::PLUGIN_VERSION, ); } /** * Get PixelID related settings. */ public static function get_options() { $default_options = array( self::PIXEL_ID_KEY => '0', self::USE_PII_KEY => 0, self::USE_S2S_KEY => false, self::ACCESS_TOKEN_KEY => '', ); $fb_options = get_option( self::SETTINGS_KEY ); if ( ! is_array( $fb_options ) ) { $fb_options = $default_options; } else { foreach ( $default_options as $key => $value ) { if ( ! isset( $fb_options[ $key ] ) ) { $fb_options[ $key ] = $value; } } } return $fb_options; } /** * Gets the logged in user info * * @return string[] */ public function get_user_info() { return $this->user_info; } }