PluginProbe ʕ •ᴥ•ʔ
Meta for WooCommerce / 3.7.2
Meta for WooCommerce v3.7.2
3.7.3 3.7.2 3.7.1 trunk 1.10.0 1.10.1 1.10.2 1.11.0 1.11.1 1.11.2 1.11.3 1.11.4 1.9.11 1.9.12 1.9.13 1.9.14 1.9.15 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.2.0 2.3.0 2.3.1 2.3.2 2.3.3 2.3.4 2.3.5 2.4.0 2.4.1 2.5.0 2.5.1 2.6.0 2.6.1 2.6.10 2.6.11 2.6.12 2.6.13 2.6.14 2.6.15 2.6.16 2.6.17 2.6.18 2.6.19 2.6.2 2.6.20 2.6.21 2.6.22 2.6.23 2.6.24 2.6.25 2.6.26 2.6.27 2.6.28 2.6.29 2.6.3 2.6.30 2.6.4 2.6.5 2.6.6 2.6.7 2.6.8 2.6.9 3.0.0 3.0.1 3.0.10 3.0.11 3.0.12 3.0.13 3.0.14 3.0.15 3.0.16 3.0.17 3.0.18 3.0.19 3.0.2 3.0.20 3.0.21 3.0.22 3.0.23 3.0.24 3.0.25 3.0.26 3.0.27 3.0.28 3.0.29 3.0.3 3.0.30 3.0.31 3.0.32 3.0.33 3.0.34 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 3.1.10 3.1.11 3.1.12 3.1.13 3.1.14 3.1.15 3.1.2 3.1.3 3.1.4 3.1.5 3.1.6 3.1.7 3.1.8 3.1.9 3.2.0 3.2.1 3.2.10 3.2.2 3.2.3 3.2.4 3.2.5 3.2.6 3.2.7 3.2.8 3.2.9 3.3.0 3.3.1 3.3.2 3.3.3 3.3.4 3.3.5 3.4.0 3.4.1 3.4.10 3.4.2 3.4.3 3.4.4 3.4.5 3.4.6 3.4.7 3.4.8 3.4.9 3.5.10 3.5.11 3.5.12 3.5.13 3.5.14 3.5.15 3.5.16 3.5.17 3.5.18 3.5.2 3.5.3 3.5.4 3.5.5 3.5.6 3.5.7 3.5.8 3.5.9 3.6.0 3.6.1 3.6.2 3.6.3 3.7.0
facebook-for-woocommerce / facebook-commerce-pixel-event.php
facebook-for-woocommerce Last commit date
assets 6 days ago data 1 year ago i18n 6 days ago includes 6 days ago vendor 6 days ago LICENSE 7 years ago changelog.txt 6 days ago class-wc-facebookcommerce.php 3 weeks ago facebook-commerce-admin-notice.php 4 months ago facebook-commerce-events-tracker.php 6 days ago facebook-commerce-iframe-whatsapp-utility-event.php 3 weeks ago facebook-commerce-pixel-event.php 6 days ago facebook-commerce.php 6 days ago facebook-config-warmer.php 4 months ago facebook-for-woocommerce.php 6 days ago playwright.config.js 4 months ago readme.txt 6 days ago
facebook-commerce-pixel-event.php
1224 lines
1 <?php
2 /**
3 * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
4 *
5 * This source code is licensed under the license found in the
6 * LICENSE file in the root directory of this source tree.
7 *
8 * @package MetaCommerce
9 */
10
11 use WooCommerce\Facebook\Events\Event;
12 use WooCommerce\Facebook\Events\FacebookSignalsState;
13
14 /**
15 * Class WC_Facebookcommerce_Pixel
16 *
17 * This class initializes the Facebook Pixel and provides methods to track events.
18 */
19 class WC_Facebookcommerce_Pixel {
20
21
22 const SETTINGS_KEY = 'facebook_config';
23 const PIXEL_ID_KEY = 'pixel_id';
24 const USE_PII_KEY = 'use_pii';
25 const USE_S2S_KEY = 'use_s2s';
26 const ACCESS_TOKEN_KEY = 'access_token';
27
28 /**
29 * Cache key for pixel script block output.
30 *
31 * @var string cache key.
32 * */
33 const PIXEL_RENDER = 'pixel_render';
34
35 /**
36 * Cache key for pixel noscript block output.
37 *
38 * @var string cache key.
39 * */
40 const NO_SCRIPT_RENDER = 'no_script_render';
41
42 /**
43 * Script render memoization helper.
44 *
45 * @var array Cache array.
46 */
47 public static $render_cache = [];
48
49 /**
50 * Queued pixel events for isolated script execution.
51 *
52 * Events are collected here and output via wp_localize_script() to an external
53 * JS file, preventing errors from other plugins breaking pixel tracking.
54 *
55 * @var array Queued events array.
56 */
57 private static $event_queue = [];
58
59 /**
60 * Whether external script has been enqueued.
61 *
62 * @var bool
63 */
64 private static $script_enqueued = false;
65
66 /**
67 * Whether hooks have been initialized.
68 *
69 * @var bool
70 */
71 private static $hooks_initialized = false;
72
73 /**
74 * User information.
75 *
76 * @var array Information array.
77 */
78 private $user_info;
79
80 /**
81 * The name of the last event.
82 *
83 * @var string Event name.
84 */
85 private $last_event;
86
87 /**
88 * Class constructor.
89 *
90 * @param array $user_info User information array.
91 */
92 public function __construct( $user_info = [] ) {
93 $this->user_info = $user_info;
94 $this->last_event = '';
95 }
96
97 /**
98 * Initialize hooks for external JavaScript event handling.
99 * Uses wp_localize_script() + external JS file to prevent JavaScript errors
100 * from other plugins breaking our pixel tracking.
101 */
102 public static function init_external_js_hooks() {
103 if ( self::$hooks_initialized ) {
104 return;
105 }
106
107 self::$hooks_initialized = true;
108
109 // Deferred events from previous page are loaded via WC_Facebookcommerce_Utils::print_deferred_events()
110 // which is hooked to wp_head in facebook-commerce-events-tracker.php.
111
112 add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_pixel_events_script' ) );
113
114 // Pass event data to JavaScript before footer scripts.
115 add_action( 'wp_footer', array( __CLASS__, 'localize_pixel_events_data' ), 5 );
116 }
117
118 /**
119 * Enqueues the external pixel events script.
120 * External script runs in isolated context - not affected by other plugin errors.
121 */
122 public static function enqueue_pixel_events_script() {
123 if ( self::$script_enqueued ) {
124 return;
125 }
126
127 $pixel_id = self::get_pixel_id();
128 if ( empty( $pixel_id ) ) {
129 return;
130 }
131
132 self::$script_enqueued = true;
133
134 wp_enqueue_script(
135 'wc-facebook-pixel-events',
136 plugins_url( 'assets/js/frontend/pixel-events.js', __FILE__ ),
137 array(),
138 WC_Facebookcommerce_Utils::PLUGIN_VERSION,
139 true // Load in footer, after fbq is initialized.
140 );
141 }
142
143 /**
144 * Passes queued event data to the frontend JavaScript.
145 * Uses wp_localize_script() to pass data (not code) to the external script.
146 */
147 public static function localize_pixel_events_data() {
148 if ( ! self::$script_enqueued || empty( self::$event_queue ) ) {
149 return;
150 }
151
152 $pixel_id = self::get_pixel_id();
153
154 wp_localize_script(
155 'wc-facebook-pixel-events',
156 'wc_facebook_pixel_data',
157 array(
158 'pixelId' => esc_js( $pixel_id ),
159 'eventQueue' => self::$event_queue,
160 'agentString' => Event::get_platform_identifier(),
161 )
162 );
163 }
164
165 /**
166 * Enqueue an event for isolated script execution.
167 * Events are stored as DATA, not executable code.
168 *
169 * @param string $event_name The name of the event to track.
170 * @param array $params Event parameters.
171 * @param string $method The fbq method to use (track, trackCustom, etc.).
172 * @param string $event_id Optional event ID for deduplication.
173 */
174 public static function enqueue_event( $event_name, $params, $method = 'track', $event_id = '' ) {
175 // Initialize hooks if not already done.
176 self::init_external_js_hooks();
177
178 $event_data = array(
179 'name' => $event_name,
180 'params' => $params,
181 'method' => $method,
182 );
183
184 if ( ! empty( $event_id ) ) {
185 $event_data['eventId'] = $event_id;
186 }
187
188 self::$event_queue[] = $event_data;
189 }
190
191 /**
192 * Enqueue an event for deferred execution on next page load.
193 * Used when events need to be deferred (e.g., AddToCart with redirect).
194 *
195 * @param string $event_name The name of the event to track.
196 * @param array $params Event parameters.
197 * @param string $method The fbq method to use (track, trackCustom, etc.).
198 * @param string $event_id Optional event ID for deduplication.
199 */
200 public static function enqueue_deferred_event( $event_name, $params, $method = 'track', $event_id = '' ) {
201 $event_data = array(
202 'name' => $event_name,
203 'params' => $params,
204 'method' => $method,
205 );
206
207 if ( ! empty( $event_id ) ) {
208 $event_data['eventId'] = $event_id;
209 }
210
211 WC_Facebookcommerce_Utils::add_deferred_event( $event_data );
212 }
213
214 /**
215 * Prepares event parameters for pixel tracking.
216 *
217 * Extracts event_id, unwraps custom_data, and applies build_params().
218 *
219 * @param array $params Raw event parameters.
220 * @param string $event_name The name of the event.
221 * @return array ['params' => array, 'event_id' => string]
222 */
223 private static function prepare_event_params( $params, $event_name ) {
224 $event_id = '';
225
226 // Do not send the event name in the params.
227 if ( isset( $params['event_name'] ) ) {
228 unset( $params['event_name'] );
229 }
230
231 /**
232 * If possible, send the event ID to avoid duplication.
233 *
234 * @see https://developers.facebook.com/docs/marketing-api/server-side-api/deduplicate-pixel-and-server-side-events#deduplication-best-practices
235 */
236 if ( isset( $params['event_id'] ) ) {
237 $event_id = $params['event_id'];
238 unset( $params['event_id'] );
239 }
240
241 // If custom_data is set, extract it (send only the inner data).
242 if ( isset( $params['custom_data'] ) ) {
243 $params = $params['custom_data'];
244 }
245
246 // Apply build_params() to add version info and apply filters.
247 $params = self::build_params( $params, $event_name );
248
249 return array(
250 'params' => $params,
251 'event_id' => $event_id,
252 );
253 }
254
255 /**
256 * Initialize pixelID.
257 */
258 public static function initialize() {
259 if ( ! is_admin() ) {
260 return;
261 }
262
263 // Initialize PixelID in storage - this will only need to happen when the user is an admin.
264 $pixel_id = self::get_pixel_id();
265 if ( ! WC_Facebookcommerce_Utils::is_valid_id( $pixel_id ) &&
266 class_exists( 'WC_Facebookcommerce_WarmConfig' ) ) {
267 $fb_warm_pixel_id = WC_Facebookcommerce_WarmConfig::$fb_warm_pixel_id;
268
269 // phpcs:disable Universal.Operators.StrictComparisons.LooseEqual
270 if ( WC_Facebookcommerce_Utils::is_valid_id( $fb_warm_pixel_id ) &&
271 (int) $fb_warm_pixel_id == $fb_warm_pixel_id ) {
272 $fb_warm_pixel_id = (string) $fb_warm_pixel_id;
273 self::set_pixel_id( $fb_warm_pixel_id );
274 }
275 }
276
277 $is_advanced_matching_enabled = self::get_use_pii_key();
278 //phpcs:disable Universal.Operators.StrictComparisons.LooseEqual
279 if ( null == $is_advanced_matching_enabled &&
280 class_exists( 'WC_Facebookcommerce_WarmConfig' ) ) {
281 $fb_warm_is_advanced_matching_enabled =
282 WC_Facebookcommerce_WarmConfig::$fb_warm_is_advanced_matching_enabled;
283 if ( is_bool( $fb_warm_is_advanced_matching_enabled ) ) {
284 self::set_use_pii_key( $fb_warm_is_advanced_matching_enabled ? 1 : 0 );
285 }
286 }
287 }
288
289
290 /**
291 * Gets Facebook Pixel init code.
292 *
293 * Init code might contain additional information to help matching website users with facebook users.
294 * Information is hashed in JS side using SHA256 before sending to Facebook.
295 *
296 * @return string
297 */
298 private function get_pixel_init_code() {
299
300 $agent_string = Event::get_platform_identifier();
301
302 /**
303 * Filters Facebook Pixel init code.
304 *
305 * @param string $js_code
306 */
307 return apply_filters(
308 'facebook_woocommerce_pixel_init',
309 sprintf(
310 "fbq('init', '%s', %s, %s);\n",
311 esc_js( self::get_pixel_id() ),
312 wp_json_encode( $this->user_info, JSON_PRETTY_PRINT | JSON_FORCE_OBJECT ),
313 wp_json_encode( array( 'agent' => $agent_string ), JSON_PRETTY_PRINT | JSON_FORCE_OBJECT )
314 )
315 );
316 }
317
318
319 /**
320 * Gets the Facebook Pixel code scripts.
321 *
322 * @return string HTML scripts
323 *
324 * phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
325 */
326 public function pixel_base_code() {
327
328 $pixel_id = self::get_pixel_id();
329
330 // Bail if no ID or already rendered.
331 if ( empty( $pixel_id ) || ! empty( self::$render_cache[ self::PIXEL_RENDER ] ) ) {
332 return '';
333 }
334
335 self::$render_cache[ self::PIXEL_RENDER ] = true;
336
337 ob_start();
338
339 $is_held = FacebookSignalsState::is_held();
340
341 ?>
342 <script <?php echo self::get_script_attributes(); ?>>
343 !function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?
344 n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;
345 n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;
346 t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,
347 document,'script','https://connect.facebook.net/en_US/fbevents.js');
348 </script>
349 <!-- WooCommerce Facebook Integration Begin -->
350 <script <?php echo self::get_script_attributes(); ?>>
351
352 <?php echo self::get_facebook_signals_js(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
353
354 <?php if ( $is_held ) : ?>
355 fbq('consent', 'revoke');
356 <?php endif; ?>
357
358 <?php echo $this->get_pixel_init_code(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
359
360 <?php if ( $is_held ) : ?>
361 FacebookSignals.init({
362 held: true,
363 ajaxUrl: <?php echo wp_json_encode( admin_url( 'admin-ajax.php' ) ); ?>,
364 nonce: <?php echo wp_json_encode( wp_create_nonce( 'facebook_release_signals' ) ); ?>,
365 action: 'facebook_release_signals',
366 pixelId: <?php echo wp_json_encode( self::get_pixel_id() ); ?>,
367 attribution: {
368 fbp: <?php echo wp_json_encode( FacebookSignalsState::get_attribution_data( 'fbp' ) ); ?>,
369 fbc: <?php echo wp_json_encode( FacebookSignalsState::get_attribution_data( 'fbc' ) ); ?>,
370 fbpDomain: <?php echo wp_json_encode( FacebookSignalsState::get_attribution_data( 'fbp_domain' ) ); ?>,
371 fbcDomain: <?php echo wp_json_encode( FacebookSignalsState::get_attribution_data( 'fbc_domain' ) ); ?>
372 }
373 });
374 <?php else : ?>
375 FacebookSignals.init({ held: false });
376 <?php endif; ?>
377
378 document.addEventListener( 'DOMContentLoaded', function() {
379 // Insert placeholder for events injected when a product is added to the cart through AJAX.
380 document.body.insertAdjacentHTML( 'beforeend', '<div class=\"wc-facebook-pixel-event-placeholder\"></div>' );
381 }, false );
382
383 </script>
384 <!-- WooCommerce Facebook Integration End -->
385 <?php
386
387 return ob_get_clean();
388 }
389
390
391 /**
392 * Gets Facebook Pixel code noscript part to avoid W3 validation errors.
393 *
394 * @return string
395 */
396 public function pixel_base_code_noscript() {
397
398 $pixel_id = self::get_pixel_id();
399
400 if ( empty( $pixel_id ) || ! empty( self::$render_cache[ self::NO_SCRIPT_RENDER ] ) ) {
401 return '';
402 }
403
404 self::$render_cache[ self::NO_SCRIPT_RENDER ] = true;
405
406 ob_start();
407
408 ?>
409 <!-- Facebook Pixel Code -->
410 <noscript>
411 <img
412 height="1"
413 width="1"
414 style="display:none"
415 alt="fbpx"
416 src="https://www.facebook.com/tr?id=<?php echo esc_attr( $pixel_id ); ?>&ev=PageView&noscript=1"
417 />
418 </noscript>
419 <!-- End Facebook Pixel Code -->
420 <?php
421
422 return ob_get_clean();
423 }
424
425
426 /**
427 * Gets the inline FacebookSignals JS API definition.
428 *
429 * @since 3.6.0
430 *
431 * @return string JavaScript code defining window.FacebookSignals.
432 */
433 private static function get_facebook_signals_js() {
434 // phpcs:disable
435 return <<<'JS'
436 window.FacebookSignals = window.FacebookSignals || {
437 _held: false,
438 _queue: [],
439 _config: {},
440 _attribution: {},
441 _seenEventIds: {},
442 _fbclid: (function() {
443 try {
444 var m = window.location.search.match(/[?&]fbclid=([^&]*)/);
445 return m ? decodeURIComponent(m[1]) : null;
446 } catch(e) { return null; }
447 })(),
448
449 init: function(config) {
450 config = config || {};
451 this._config = config;
452 this._attribution = config.attribution || {};
453 this._held = !!config.held;
454 this._fbclid = this._fbclid || null;
455
456 try {
457 var raw = window.sessionStorage.getItem('wc_facebook_signals_seen_event_ids');
458 this._seenEventIds = raw ? JSON.parse(raw) : {};
459 } catch (e) {
460 this._seenEventIds = this._seenEventIds || {};
461 }
462 },
463
464 queueEvent: function(eventData) {
465 if (!eventData || !eventData.event_name) return;
466 if (eventData.event_id && this._seenEventIds[eventData.event_id]) return;
467
468 eventData.event_time = eventData.event_time || Math.floor(Date.now() / 1000);
469 this._queue.push(eventData);
470
471 if (eventData.event_id) {
472 this._seenEventIds[eventData.event_id] = 1;
473 try {
474 window.sessionStorage.setItem(
475 'wc_facebook_signals_seen_event_ids',
476 JSON.stringify(this._seenEventIds)
477 );
478 } catch (e) {}
479 }
480 },
481
482 trackEvent: function(name, params, userData) {
483 if (this._held) {
484 this.queueEvent({
485 event_name: name,
486 custom_data: params || {},
487 user_data: userData || {},
488 event_id: (params && params.eventID) || null,
489 event_time: Math.floor(Date.now() / 1000)
490 });
491 } else {
492 if (params && params.eventID) {
493 fbq('track', name, params, { eventID: params.eventID });
494 } else {
495 fbq('track', name, params);
496 }
497 }
498 },
499
500 release: function() {
501 var self = this;
502 if (!self._held || !self._config.ajaxUrl) {
503 return Promise.resolve({ success: true, data: { sent_count: 0 } });
504 }
505
506 var payload = JSON.stringify({
507 security: self._config.nonce,
508 events: self._queue,
509 attribution: {
510 fbp: self._attribution.fbp || null,
511 fbc: self._attribution.fbc || null
512 }
513 });
514
515 // Pass action as a query parameter so WordPress can route the request.
516 var url = self._config.ajaxUrl +
517 (self._config.ajaxUrl.indexOf('?') === -1 ? '?' : '&') +
518 'action=' + encodeURIComponent(self._config.action);
519
520 return new Promise(function(resolve, reject) {
521 var xhr = new XMLHttpRequest();
522 xhr.open('POST', url, true);
523 xhr.setRequestHeader('Content-Type', 'application/json');
524 xhr.onload = function() {
525 if (xhr.status >= 200 && xhr.status < 300) {
526 try {
527 var resp = JSON.parse(xhr.responseText);
528 self._handleReleaseResponse(resp.data || {});
529 resolve(resp);
530 } catch(e) { reject(e); }
531 } else {
532 reject(new Error('Signal release AJAX failed: ' + xhr.status));
533 }
534 };
535 xhr.onerror = function() { reject(new Error('Network error')); };
536 xhr.send(payload);
537 });
538 },
539
540 _handleReleaseResponse: function(data) {
541 this._syncAttributionCookies(data || {});
542
543 // Re-enable the browser signal path so fbevents.js starts firing.
544 fbq('consent', 'grant');
545
546 // Replay queued events via the pixel.
547 var queue = this._queue;
548 for (var i = 0; i < queue.length; i++) {
549 var ev = queue[i];
550 if (ev.event_id) {
551 fbq('track', ev.event_name, ev.custom_data || {}, { eventID: ev.event_id });
552 } else {
553 fbq('track', ev.event_name, ev.custom_data || {});
554 }
555 }
556
557 // Clear the queue and mark signals as active again.
558 this._queue = [];
559 this._held = false;
560 },
561
562 _syncAttributionCookies: function(data) {
563 var clientParams = {};
564
565 if (typeof clientParamBuilder !== 'undefined') {
566 try {
567 // Let the client-side ParamBuilder generate/set missing attribution
568 // cookies using its normal domain-scoping logic.
569 clientParams = clientParamBuilder.processAndCollectParams(this._getAttributionUrl()) || {};
570 } catch (e) {}
571 }
572
573 var fbp = data.fbp || clientParams._fbp || (typeof clientParamBuilder !== 'undefined' ? clientParamBuilder.getFbp() : null);
574 var fbc = data.fbc || clientParams._fbc || (typeof clientParamBuilder !== 'undefined' ? clientParamBuilder.getFbc() : null);
575
576 // If the backend supplied exact values used for CAPI, write them so Pixel
577 // replay and CAPI use matching attribution.
578 if (data.fbp) {
579 this._setAttributionCookie('_fbp', fbp, data.fbp_domain || data.cookie_domain || this._attribution.fbpDomain);
580 }
581 if (data.fbc) {
582 this._setAttributionCookie('_fbc', fbc, data.fbc_domain || data.cookie_domain || this._attribution.fbcDomain || this._attribution.fbpDomain);
583 }
584 },
585
586 _setAttributionCookie: function(name, value, domain) {
587 if (!value) return;
588
589 var domainAttr = domain ? ';domain=' + domain : '';
590 document.cookie = name + '=' + encodeURIComponent(value) + ';path=/;max-age=7776000' + domainAttr + ';SameSite=Lax';
591 },
592
593 _getAttributionUrl: function() {
594 if (!this._fbclid) {
595 return window.location.href;
596 }
597
598 try {
599 var url = new URL(window.location.href);
600 if (!url.searchParams.get('fbclid')) {
601 url.searchParams.set('fbclid', this._fbclid);
602 }
603 return url.toString();
604 } catch (e) {
605 return window.location.href;
606 }
607 }
608 };
609 JS;
610 // phpcs:enable
611 }
612
613
614 /**
615 * Gets JS code that queues an event via FacebookSignals while signals are held.
616 *
617 * Captures PII at event-generation time so it's included when events are replayed.
618 *
619 * @since 3.6.0
620 *
621 * @param string $event_name The event name.
622 * @param array $params Event parameters including custom_data, user_data, event_id.
623 * @return string JavaScript code.
624 */
625 public function get_queued_event_code( $event_name, $params ) {
626 $this->last_event = $event_name;
627
628 $event_id = isset( $params['event_id'] ) ? $params['event_id'] : null;
629 $queue_data = ! empty( $event_id ) ? FacebookSignalsState::get_queued_event( $event_id ) : null;
630
631 if ( ! is_array( $queue_data ) ) {
632 $queue_data = array(
633 'event_name' => $event_name,
634 'custom_data' => isset( $params['custom_data'] ) ? $params['custom_data'] : $params,
635 'event_id' => $event_id,
636 'event_time' => time(),
637 );
638
639 if ( isset( $params['user_data'] ) ) {
640 $queue_data['user_data'] = $params['user_data'];
641 }
642 }
643
644 if ( empty( $queue_data['event_name'] ) ) {
645 $queue_data['event_name'] = $event_name;
646 }
647
648 return sprintf(
649 "/* %s Facebook Integration Event Queueing */\nFacebookSignals.queueEvent(%s);",
650 \WC_Facebookcommerce_Utils::get_integration_name(),
651 wp_json_encode( $queue_data )
652 );
653 }
654
655
656 /**
657 * Determines if the last event in the current thread matches a given event.
658 *
659 * @since 1.11.0
660 *
661 * @param string $event_name
662 * @return bool
663 */
664 public function is_last_event( $event_name ) {
665
666 return $event_name === $this->last_event;
667 }
668
669
670 /**
671 * Gets the JavaScript code to track an event.
672 *
673 * Updates the last event property and returns the code.
674 *
675 * Use {@see \WC_Facebookcommerce_Pixel::inject_event()} to print or enqueue the code.
676 *
677 * @since 1.10.2
678 *
679 * @param string $event_name The name of the event to track.
680 * @param array $params Custom event parameters.
681 * @param string $method Name of the pixel's fbq() function to call.
682 * @return string
683 */
684 public function get_event_code( $event_name, $params, $method = 'track' ) {
685
686 $this->last_event = $event_name;
687
688 return self::build_event( $event_name, $params, $method );
689 }
690
691
692 /**
693 * Gets the JavaScript code to track an event wrapped in <script> tag.
694 *
695 * @see \WC_Facebookcommerce_Pixel::get_event_code()
696 *
697 * @since 1.10.2
698 *
699 * @param string $event_name The name of the event to track.
700 * @param array $params Custom event parameters.
701 * @param string $method Name of the pixel's fbq() function to call.
702 * @return string
703 *
704 * phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
705 */
706 public function get_event_script( $event_name, $params, $method = 'track' ) {
707
708 ob_start();
709
710 ?>
711 <!-- Facebook Pixel Event Code -->
712 <script <?php echo self::get_script_attributes(); ?>>
713 <?php
714 if ( FacebookSignalsState::is_held() ) {
715 echo $this->get_queued_event_code( $event_name, $params ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
716 } else {
717 echo $this->get_event_code( $event_name, $params, $method ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
718 }
719 ?>
720 </script>
721 <!-- End Facebook Pixel Event Code -->
722 <?php
723
724 return ob_get_clean();
725 }
726
727 /**
728 * Prints or enqueues the JavaScript code to track an event.
729 * Preferred method to inject events in a page.
730 *
731 * Supports two execution modes controlled by rollout switch:
732 * - Isolated execution (switch ON): Uses external JS via wp_localize_script() to prevent
733 * other plugins' JavaScript errors from breaking pixel tracking.
734 * - Legacy execution (switch OFF): Uses enqueue_inline_js() for inline script output.
735 *
736 * @see \WC_Facebookcommerce_Pixel::build_event()
737 *
738 * @param string $event_name The name of the event to track.
739 * @param array $params Custom event parameters.
740 * @param string $method Name of the pixel's fbq() function to call.
741 *
742 * phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
743 */
744 public function inject_event( $event_name, $params, $method = 'track' ) {
745 // When signals are held, queue events in the browser instead of firing them.
746 if ( FacebookSignalsState::is_held() ) {
747 $code = $this->get_queued_event_code( $event_name, $params );
748 if ( WC_Facebookcommerce_Utils::is_woocommerce_integration() ) {
749 WC_Facebookcommerce_Utils::enqueue_inline_js( $code );
750 } else {
751 printf(
752 '<!-- Facebook Pixel Event Code --><script %s>%s</script><!-- End Facebook Pixel Event Code -->',
753 self::get_script_attributes(), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
754 $code // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
755 );
756 }
757 return;
758 }
759 if ( WC_Facebookcommerce_Utils::is_woocommerce_integration() ) {
760 // Check rollout switch for isolated pixel execution.
761 // When enabled, pixel events are output via external JS file (wp_localize_script)
762 // to prevent other plugins' JavaScript errors from breaking pixel tracking.
763 $is_isolated_pixel_execution_enabled = facebook_for_woocommerce()->get_rollout_switches()->is_switch_enabled(
764 \WooCommerce\Facebook\RolloutSwitches::SWITCH_ISOLATED_PIXEL_EXECUTION_ENABLED
765 );
766
767 // If we have add to cart redirect enabled, we must defer the AddToCart events to render them the next page load.
768 $is_redirect = 'yes' === get_option( 'woocommerce_cart_redirect_after_add', 'no' );
769 $is_add_to_cart = 'AddToCart' === $event_name;
770 $is_deferred = $is_redirect && $is_add_to_cart;
771
772 if ( $is_isolated_pixel_execution_enabled ) {
773 // Isolated execution: Use external JS via wp_localize_script.
774 // Set last_event here since we don't call get_event_code() in this path.
775 $this->last_event = $event_name;
776 [ 'params' => $event_params, 'event_id' => $event_id ] = self::prepare_event_params( $params, $event_name );
777
778 if ( $is_deferred ) {
779 // Store event data for next page load.
780 self::enqueue_deferred_event( $event_name, $event_params, $method, $event_id );
781 } else {
782 // Queue event for this page's external script.
783 self::enqueue_event( $event_name, $event_params, $method, $event_id );
784 }
785 } else {
786 // Legacy execution: Use enqueue_inline_js for inline script.
787 $code = $this->get_event_code( $event_name, self::build_params( $params, $event_name ), $method );
788
789 if ( $is_deferred ) {
790 // Store JS code string for inline script at print time.
791 WC_Facebookcommerce_Utils::add_deferred_event( $code );
792 } else {
793 WC_Facebookcommerce_Utils::enqueue_inline_js( $code );
794 }
795 }
796 } else {
797 printf( $this->get_event_script( $event_name, self::build_params( $params, $event_name ), $method ) ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped
798 }
799 }
800
801 /**
802 * Gets the JavaScript code to track a conditional event wrapped in <script> tag.
803 *
804 * @see \WC_Facebookcommerce_Pixel::get_event_code()
805 *
806 * @since 1.10.2
807 *
808 * @param string $event_name The name of the event to track.
809 * @param array $params Custom event parameters.
810 * @param string $listener Name of the JavaScript event to listen for.
811 * @param string $jsonified_pii JavaScript code representing an object of data for Advanced Matching.
812 * @return string
813 *
814 * phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
815 */
816 public function get_conditional_event_script( $event_name, $params, $listener, $jsonified_pii ) {
817
818 $this->last_event = $event_name;
819
820 // When signals are held, use FacebookSignals.trackEvent() to queue the event.
821 if ( FacebookSignalsState::is_held() ) {
822 $user_data_js = $jsonified_pii ? $jsonified_pii : '{}';
823 ob_start();
824 ?>
825 <!-- Facebook Pixel Event Code -->
826 <script <?php echo self::get_script_attributes(); ?>>
827 document.addEventListener( '<?php echo esc_js( $listener ); ?>', function (event) {
828 FacebookSignals.trackEvent(
829 <?php echo wp_json_encode( $event_name ); ?>,
830 <?php echo wp_json_encode( $params ); ?>,
831 <?php echo $user_data_js; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
832 );
833 }, false );
834 </script>
835 <!-- End Facebook Pixel Event Code -->
836 <?php
837 return ob_get_clean();
838 }
839
840 $code = self::build_event( $event_name, $params, 'track' );
841
842 /**
843 * TODO: use the settings stored by {@see \WC_Facebookcommerce_Integration}.
844 * The use_pii setting here is currently always disabled regardless of
845 * the value configured in the plugin settings page {WV-2020-01-02}.
846 */
847
848 // Prepends fbq(...) with pii information to the injected code.
849 if ( $jsonified_pii && get_option( self::SETTINGS_KEY )[ self::USE_PII_KEY ] ) {
850 $this->user_info = '%s';
851 $code = sprintf( $this->get_pixel_init_code(), '" || ' . $jsonified_pii . ' || "' ) . $code;
852 }
853
854 ob_start();
855
856 ?>
857 <!-- Facebook Pixel Event Code -->
858 <script <?php echo self::get_script_attributes(); ?>>
859 document.addEventListener( '<?php echo esc_js( $listener ); ?>', function (event) {
860 <?php echo $code; ?>
861 }, false );
862 </script>
863 <!-- End Facebook Pixel Event Code -->
864 <?php
865
866 return ob_get_clean();
867 }
868
869
870 /**
871 * Prints the JavaScript code to track a conditional event.
872 *
873 * The tracking code will be executed when the given JavaScript event is triggered.
874 *
875 * @param string $event_name Name of the event.
876 * @param array $params Custom event parameters.
877 * @param string $listener Name of the JavaScript event to listen for.
878 * @param string $jsonified_pii JavaScript code representing an object of data for Advanced Matching.
879 * @return string
880 */
881 public function inject_conditional_event( $event_name, $params, $listener, $jsonified_pii = '' ) {
882
883 // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped
884 return $this->get_conditional_event_script( $event_name, self::build_params( $params, $event_name ), $listener, $jsonified_pii );
885 }
886
887
888 /**
889 * Gets the JavaScript code to track a conditional event that is only triggered one time wrapped in <script> tag.
890 *
891 * @internal
892 *
893 * @since 1.10.2
894 *
895 * @param string $event_name The name of the event to track.
896 * @param array $params Custom event parameters.
897 * @param string $listened_event Name of the JavaScript event to listen for.
898 * @return string
899 *
900 * phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
901 */
902 public function get_conditional_one_time_event_script( $event_name, $params, $listened_event ) {
903
904 // When signals are held, queue via FacebookSignals.trackEvent().
905 if ( FacebookSignalsState::is_held() ) {
906 $this->last_event = $event_name;
907 ob_start();
908 ?>
909 <!-- Facebook Pixel Event Code -->
910 <script <?php echo self::get_script_attributes(); ?>>
911 function handle<?php echo $event_name; ?>Event() {
912 FacebookSignals.trackEvent(<?php echo wp_json_encode( $event_name ); ?>, <?php echo wp_json_encode( $params ); ?>);
913 jQuery( document.body ).off( '<?php echo esc_js( $listened_event ); ?>', handle<?php echo $event_name; ?>Event );
914 }
915 jQuery( document.body ).one( '<?php echo esc_js( $listened_event ); ?>', handle<?php echo $event_name; ?>Event );
916 </script>
917 <!-- End Facebook Pixel Event Code -->
918 <?php
919 return ob_get_clean();
920 }
921
922 $code = $this->get_event_code( $event_name, $params );
923
924 ob_start();
925
926 ?>
927 <!-- Facebook Pixel Event Code -->
928 <script <?php echo self::get_script_attributes(); ?>>
929 function handle<?php echo $event_name; ?>Event() {
930 <?php echo $code; ?>
931 // Some weird themes (hi, Basel) are running this script twice, so two listeners are added and we need to remove them after running one.
932 jQuery( document.body ).off( '<?php echo esc_js( $listened_event ); ?>', handle<?php echo $event_name; ?>Event );
933 }
934
935 jQuery( document.body ).one( '<?php echo esc_js( $listened_event ); ?>', handle<?php echo $event_name; ?>Event );
936 </script>
937 <!-- End Facebook Pixel Event Code -->
938 <?php
939
940 return ob_get_clean();
941 }
942
943
944 /**
945 * Builds an event.
946 *
947 * @see \WC_Facebookcommerce_Pixel::inject_event() for the preferred method to inject an event.
948 *
949 * @param string $event_name Event name.
950 * @param array $params Event params.
951 * @param string $method Optional, defaults to 'track'.
952 * @return string
953 */
954 public static function build_event( $event_name, $params, $method = 'track' ) {
955 // Reuse shared param preparation logic.
956 [ 'params' => $event_params, 'event_id' => $event_id ] = self::prepare_event_params( $params, $event_name );
957
958 if ( ! empty( $event_id ) ) {
959 $event = sprintf(
960 "/* %s Facebook Integration Event Tracking */\n" .
961 "fbq('set', 'agent', '%s', '%s');\n" .
962 "window.wcFacebookPixelFiredEvents = window.wcFacebookPixelFiredEvents || {};\n" .
963 "if (!window.wcFacebookPixelFiredEvents[%s]) {\n" .
964 "window.wcFacebookPixelFiredEvents[%s] = true;\n" .
965 "fbq('%s', '%s', %s, %s);\n" .
966 '}',
967 WC_Facebookcommerce_Utils::get_integration_name(),
968 Event::get_platform_identifier(),
969 self::get_pixel_id(),
970 wp_json_encode( $event_id ),
971 wp_json_encode( $event_id ),
972 esc_js( $method ),
973 esc_js( $event_name ),
974 wp_json_encode( $event_params, JSON_PRETTY_PRINT | JSON_FORCE_OBJECT ),
975 wp_json_encode( array( 'eventID' => $event_id ), JSON_PRETTY_PRINT | JSON_FORCE_OBJECT )
976 );
977
978 } else {
979
980 $event = sprintf(
981 "/* %s Facebook Integration Event Tracking */\n" .
982 "fbq('set', 'agent', '%s', '%s');\n" .
983 "fbq('%s', '%s', %s);",
984 WC_Facebookcommerce_Utils::get_integration_name(),
985 Event::get_platform_identifier(),
986 self::get_pixel_id(),
987 esc_js( $method ),
988 esc_js( $event_name ),
989 wp_json_encode( $event_params, JSON_PRETTY_PRINT | JSON_FORCE_OBJECT )
990 );
991 }
992
993 return $event;
994 }
995
996
997 /**
998 * Gets an array with version_info for pixel fires.
999 *
1000 * Parameters provided by users should not be overwritten by this function.
1001 *
1002 * @since 1.10.2
1003 *
1004 * @param array $params User defined parameters.
1005 * @param string $event The event name the params are for.
1006 * @return array
1007 */
1008 private static function build_params( $params = [], $event = '' ) {
1009
1010 $params = array_replace( Event::get_version_info(), $params );
1011
1012 /**
1013 * Filters the parameters for the pixel code.
1014 *
1015 * @since 1.10.2
1016 *
1017 * @param array $params User defined parameters.
1018 * @param string $event The event name.
1019 */
1020 return (array) apply_filters( 'wc_facebook_pixel_params', $params, $event );
1021 }
1022
1023
1024 /**
1025 * Gets script tag attributes.
1026 *
1027 * @since 1.10.2
1028 *
1029 * @return string
1030 */
1031 private static function get_script_attributes() {
1032
1033 $script_attributes = '';
1034
1035 /**
1036 * Filters Facebook Pixel script attributes.
1037 *
1038 * @since 1.10.2
1039 *
1040 * @param array $custom_attributes
1041 */
1042 $custom_attributes = (array) apply_filters( 'wc_facebook_pixel_script_attributes', array( 'type' => 'text/javascript' ) );
1043
1044 foreach ( $custom_attributes as $tag => $value ) {
1045 $script_attributes .= ' ' . $tag . '="' . esc_attr( $value ) . '"';
1046 }
1047
1048 return $script_attributes;
1049 }
1050
1051 /**
1052 * Get the PixelId.
1053 */
1054 public static function get_pixel_id() {
1055 $fb_options = self::get_options();
1056 if ( ! $fb_options ) {
1057 return '';
1058 }
1059 return isset( $fb_options[ self::PIXEL_ID_KEY ] ) ?
1060 $fb_options[ self::PIXEL_ID_KEY ] : '';
1061 }
1062
1063 /**
1064 * Set the PixelId.
1065 *
1066 * @param string $pixel_id PixelId.
1067 */
1068 public static function set_pixel_id( $pixel_id ) {
1069 $fb_options = self::get_options();
1070
1071 if ( isset( $fb_options[ self::PIXEL_ID_KEY ] )
1072 && $fb_options[ self::PIXEL_ID_KEY ] === $pixel_id ) {
1073 return;
1074 }
1075
1076 $fb_options[ self::PIXEL_ID_KEY ] = $pixel_id;
1077 update_option( self::SETTINGS_KEY, $fb_options );
1078 }
1079
1080 /**
1081 * Check if PII key use is enabled.
1082 */
1083 public static function get_use_pii_key() {
1084 $fb_options = self::get_options();
1085 if ( ! $fb_options ) {
1086 return null;
1087 }
1088 return isset( $fb_options[ self::USE_PII_KEY ] ) ?
1089 $fb_options[ self::USE_PII_KEY ] : null;
1090 }
1091
1092 /**
1093 * Enable or disable use of PII key.
1094 *
1095 * @param string $use_pii PII key.
1096 */
1097 public static function set_use_pii_key( $use_pii ) {
1098 $fb_options = self::get_options();
1099
1100 if ( isset( $fb_options[ self::USE_PII_KEY ] )
1101 && $fb_options[ self::USE_PII_KEY ] === $use_pii ) {
1102 return;
1103 }
1104
1105 $fb_options[ self::USE_PII_KEY ] = $use_pii;
1106 update_option( self::SETTINGS_KEY, $fb_options );
1107 }
1108
1109 /**
1110 * Check if S2S is set.
1111 */
1112 public static function get_use_s2s() {
1113 $fb_options = self::get_options();
1114 if ( ! $fb_options ) {
1115 return false;
1116 }
1117 return isset( $fb_options[ self::USE_S2S_KEY ] ) ?
1118 $fb_options[ self::USE_S2S_KEY ] : false;
1119 }
1120
1121 /**
1122 * Enable or disable use of S2S key.
1123 *
1124 * @param string $use_s2s S2S setting.
1125 */
1126 public static function set_use_s2s( $use_s2s ) {
1127 $fb_options = self::get_options();
1128
1129 if ( isset( $fb_options[ self::USE_S2S_KEY ] )
1130 && $fb_options[ self::USE_S2S_KEY ] === $use_s2s ) {
1131 return;
1132 }
1133
1134 $fb_options[ self::USE_S2S_KEY ] = $use_s2s;
1135 update_option( self::SETTINGS_KEY, $fb_options );
1136 }
1137
1138 /**
1139 * Get access token.
1140 */
1141 public static function get_access_token() {
1142 $fb_options = self::get_options();
1143 if ( ! $fb_options ) {
1144 return '';
1145 }
1146 return isset( $fb_options[ self::ACCESS_TOKEN_KEY ] ) ?
1147 $fb_options[ self::ACCESS_TOKEN_KEY ] : '';
1148 }
1149
1150 /**
1151 * Set access token.
1152 *
1153 * @param string $access_token Access token.
1154 */
1155 public static function set_access_token( $access_token ) {
1156 $fb_options = self::get_options();
1157
1158 if ( isset( $fb_options[ self::ACCESS_TOKEN_KEY ] )
1159 && $fb_options[ self::ACCESS_TOKEN_KEY ] === $access_token ) {
1160 return;
1161 }
1162
1163 $fb_options[ self::ACCESS_TOKEN_KEY ] = $access_token;
1164 update_option( self::SETTINGS_KEY, $fb_options );
1165 }
1166
1167 /**
1168 * Get WooCommerce/Wordpress information.
1169 */
1170 private static function get_version_info() {
1171 global $wp_version;
1172
1173 if ( WC_Facebookcommerce_Utils::is_woocommerce_integration() ) {
1174 return array(
1175 'source' => 'woocommerce',
1176 'version' => WC()->version,
1177 'pluginVersion' => WC_Facebookcommerce_Utils::PLUGIN_VERSION,
1178 );
1179 }
1180
1181 return array(
1182 'source' => 'wordpress',
1183 'version' => $wp_version,
1184 'pluginVersion' => WC_Facebookcommerce_Utils::PLUGIN_VERSION,
1185 );
1186 }
1187
1188 /**
1189 * Get PixelID related settings.
1190 */
1191 public static function get_options() {
1192
1193 $default_options = array(
1194 self::PIXEL_ID_KEY => '0',
1195 self::USE_PII_KEY => 0,
1196 self::USE_S2S_KEY => false,
1197 self::ACCESS_TOKEN_KEY => '',
1198 );
1199
1200 $fb_options = get_option( self::SETTINGS_KEY );
1201
1202 if ( ! is_array( $fb_options ) ) {
1203 $fb_options = $default_options;
1204 } else {
1205 foreach ( $default_options as $key => $value ) {
1206 if ( ! isset( $fb_options[ $key ] ) ) {
1207 $fb_options[ $key ] = $value;
1208 }
1209 }
1210 }
1211
1212 return $fb_options;
1213 }
1214
1215 /**
1216 * Gets the logged in user info
1217 *
1218 * @return string[]
1219 */
1220 public function get_user_info() {
1221 return $this->user_info;
1222 }
1223 }
1224