HEX
Server: Apache
System: Linux 244.240.109.208.host.secureserver.net 5.14.0-611.11.1.el9_7.x86_64 #1 SMP PREEMPT_DYNAMIC Wed Dec 3 09:47:37 EST 2025 x86_64
User: icsla (1002)
PHP: 8.1.34
Disabled: NONE
Upload Files
File: /home/icsla/public_html/wp-content/plugins/popup-maker/classes/Services/FormConversionTracking.php
<?php
/**
 * Form Conversion Tracking Service
 *
 * Tracks form submission conversions for upsell messaging and analytics.
 *
 * @package   PopupMaker
 * @copyright Copyright (c) 2025, Code Atlantic LLC
 */

namespace PopupMaker\Services;

use PopupMaker\Base\Service;

defined( 'ABSPATH' ) || exit;

/**
 * Form Conversion Tracking Service.
 *
 * Tracks site-wide and per-popup form conversion counts for:
 * - Milestone-based upsell triggers
 * - Future analytics dashboard
 *
 * @since 1.22.0
 */
class FormConversionTracking extends Service {

	/**
	 * Site-wide form conversion count option key.
	 */
	const SITE_COUNT_KEY = 'pum_form_conversion_count';

	/**
	 * Per-popup form conversion count meta key.
	 */
	const POPUP_META_KEY = '_pum_form_conversion_count';

	/**
	 * Initialize service.
	 *
	 * @since 1.22.0
	 */
	public function init() {
		// Track non-AJAX form submissions (PHP-side tracking).
		add_action( 'pum_integrated_form_submission', [ $this, 'track_form_conversion' ], 10, 1 );

		// Track AJAX form submissions (JS beacon tracking).
		add_action( 'pum_analytics_conversion', [ $this, 'track_ajax_conversion' ], 10, 2 );
	}

	/**
	 * Track form conversion when a form is submitted.
	 *
	 * Increments both site-wide and per-popup conversion counts.
	 *
	 * @since 1.22.0
	 *
	 * @param array<string, mixed> $args {
	 *     Form submission arguments.
	 *
	 *     @type int|null    $popup_id      Popup ID that captured the submission.
	 *     @type string|null $form_provider Form plugin name (e.g., 'gravity-forms').
	 *     @type string|null $form_id       Form ID from the provider.
	 *     @type bool        $tracked       Whether already tracked by other systems.
	 * }
	 */
	public function track_form_conversion( $args ) {
		// Defensive validation for third-party hook callers.
		if ( ! is_array( $args ) ) {
			return;
		}

		// Skip if already tracked by another system to prevent duplicates.
		if ( ! empty( $args['tracked'] ) ) {
			return;
		}

		// Only track submissions that were captured by a popup.
		if ( empty( $args['popup_id'] ) || ! is_numeric( $args['popup_id'] ) ) {
			return;
		}

		$popup_id = (int) $args['popup_id'];

		// Verify popup exists before tracking (prevents orphaned meta).
		$popup = pum_get_popup( $popup_id );
		if ( ! pum_is_popup( $popup ) ) {
			// Log but don't break form submission - tracking is non-critical.
			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				error_log( sprintf( '[Popup Maker] Skipping form conversion tracking for invalid popup ID: %d', $popup_id ) );
			}
			return;
		}

		// Increment site-wide form conversion count.
		$this->increment_site_count();

		// Increment per-popup count.
		$this->increment_popup_count( $popup_id );

		/**
		 * Fires after a form conversion is tracked (non-AJAX).
		 *
		 * @since 1.22.0
		 *
		 * @param int   $popup_id Popup ID.
		 * @param array $args     Form submission arguments.
		 */
		do_action( 'popup_maker/form_conversion_tracked', $popup_id, $args );
	}

	/**
	 * Track AJAX form conversion from analytics beacon.
	 *
	 * Handles conversions tracked via frontend JS beacon (AJAX submissions).
	 *
	 * @since 1.22.0
	 *
	 * @param int   $popup_id Popup ID from analytics beacon.
	 * @param array $args     Additional arguments from beacon.
	 */
	public function track_ajax_conversion( $popup_id, $args = [] ) {
		// Defensive validation for third-party hook callers.
		if ( ! is_array( $args ) ) {
			return;
		}

		// Extract eventData (matches Pro's pattern).
		// REST endpoint sanitize_event_data() already decoded JSON to array.
		$event_data = isset( $args['eventData'] ) ? $args['eventData'] : [];

		// Only track conversions with explicit form submission metadata.
		if ( empty( $event_data ) || ! is_array( $event_data ) ) {
			return;
		}

		// Verify this is a form submission event (not CTA or link click).
		if ( empty( $event_data['type'] ) || 'form_submission' !== $event_data['type'] ) {
			return;
		}

		// Validate popup ID.
		if ( empty( $popup_id ) || ! is_numeric( $popup_id ) ) {
			return;
		}

		$popup_id = (int) $popup_id;

		// Verify popup exists before tracking (prevents orphaned meta).
		$popup = pum_get_popup( $popup_id );
		if ( ! pum_is_popup( $popup ) ) {
			// Log but don't break form submission - tracking is non-critical.
			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				error_log( sprintf( '[Popup Maker] Skipping AJAX form conversion tracking for invalid popup ID: %d', $popup_id ) );
			}
			return;
		}

		// Increment site-wide form conversion count.
		$this->increment_site_count();

		// Increment per-popup count.
		$this->increment_popup_count( $popup_id );

		/**
		 * Fires after an AJAX form conversion is tracked.
		 *
		 * @since 1.22.0
		 *
		 * @param int   $popup_id   Popup ID.
		 * @param array $event_data Form submission event data.
		 */
		do_action( 'popup_maker/form_conversion_tracked', $popup_id, $event_data );
	}

	/**
	 * Increment site-wide form conversion count.
	 *
	 * Uses atomic SQL update to prevent race conditions when multiple
	 * form submissions occur simultaneously.
	 *
	 * @since 1.22.0
	 *
	 * @return int New count after increment.
	 */
	protected function increment_site_count() {
		global $wpdb;

		// Check if option exists; if not, create it with autoload disabled.
		$exists = $wpdb->get_var(
			$wpdb->prepare(
				'SELECT option_id FROM %i WHERE option_name = %s LIMIT 1',
				$wpdb->options,
				self::SITE_COUNT_KEY
			)
		);

		if ( ! $exists ) {
			// Initialize with autoload=no (analytical data doesn't need to load on every request).
			add_option( self::SITE_COUNT_KEY, 0, '', false );
		}

		// Atomic increment (prevents race condition).
		$wpdb->query(
			$wpdb->prepare(
				'UPDATE %i SET option_value = option_value + 1 WHERE option_name = %s',
				$wpdb->options,
				self::SITE_COUNT_KEY
			)
		);

		// Clear cache since we bypassed WordPress's caching layer.
		wp_cache_delete( self::SITE_COUNT_KEY, 'options' );

		// Return updated count.
		return (int) get_option( self::SITE_COUNT_KEY, 0 );
	}

	/**
	 * Increment per-popup form conversion count.
	 *
	 * Uses atomic SQL update to prevent race conditions when multiple
	 * form submissions occur simultaneously for the same popup.
	 *
	 * @since 1.22.0
	 *
	 * @param int $popup_id Popup post ID.
	 * @return int New count after increment.
	 */
	protected function increment_popup_count( $popup_id ) {
		global $wpdb;

		// Check if meta exists; if not, create it.
		$exists = $wpdb->get_var(
			$wpdb->prepare(
				'SELECT meta_id FROM %i WHERE post_id = %d AND meta_key = %s LIMIT 1',
				$wpdb->postmeta,
				$popup_id,
				self::POPUP_META_KEY
			)
		);

		if ( ! $exists ) {
			add_post_meta( $popup_id, self::POPUP_META_KEY, 0, true );
		}

		// Atomic increment (prevents race condition).
		$wpdb->query(
			$wpdb->prepare(
				'UPDATE %i SET meta_value = meta_value + 1 WHERE post_id = %d AND meta_key = %s',
				$wpdb->postmeta,
				$popup_id,
				self::POPUP_META_KEY
			)
		);

		// Clear cache since we bypassed WordPress's caching layer.
		wp_cache_delete( $popup_id, 'post_meta' );

		// Return updated count.
		return (int) get_post_meta( $popup_id, self::POPUP_META_KEY, true );
	}

	/**
	 * Get site-wide form conversion count.
	 *
	 * @since 1.22.0
	 *
	 * @return int Total form conversions across all popups.
	 */
	public function get_site_count() {
		return (int) get_option( self::SITE_COUNT_KEY, 0 );
	}

	/**
	 * Get form conversion count for a specific popup.
	 *
	 * @since 1.22.0
	 *
	 * @param int $popup_id Popup post ID.
	 * @return int Form conversions for this popup.
	 */
	public function get_popup_count( $popup_id ) {
		return (int) get_post_meta( $popup_id, self::POPUP_META_KEY, true );
	}

	/**
	 * Reset site-wide form conversion count.
	 *
	 * Useful for testing or if data needs to be cleared.
	 *
	 * @since 1.22.0
	 */
	public function reset_site_count() {
		delete_option( self::SITE_COUNT_KEY );
	}

	/**
	 * Reset form conversion count for a specific popup.
	 *
	 * @since 1.22.0
	 *
	 * @param int $popup_id Popup post ID.
	 */
	public function reset_popup_count( $popup_id ) {
		delete_post_meta( $popup_id, self::POPUP_META_KEY );
	}
}