$value) { $key = sanitize_key($key); if (is_array($value)) { $sanitized[$key] = activetrail_wc_sanitize_array($value); } elseif (is_string($value)) { // Use sanitize_text_field for strings, but preserve email format if it's an email field. if (filter_var($value, FILTER_VALIDATE_EMAIL)) { $sanitized[$key] = sanitize_email($value); } elseif (filter_var($value, FILTER_VALIDATE_URL)) { $sanitized[$key] = esc_url_raw($value); } else { $sanitized[$key] = sanitize_text_field($value); } } elseif (is_numeric($value)) { $sanitized[$key] = is_float($value) ? floatval($value) : intval($value); } elseif (is_bool($value)) { $sanitized[$key] = (bool) $value; } else { $sanitized[$key] = sanitize_text_field(strval($value)); } } return $sanitized; } return $data; } /** * Generate external ID for ActiveTrail * * @param string $type Entity type (contact, order, cart) * @param string|int $id Entity ID * @return string */ function activetrail_generate_external_id($type, $id) { $type = strtoupper((string) $type); $id = (string) $id; return "WC-{$type}-{$id}"; } /** * Generate external ID for contact * * @param int $user_id User ID (0 for guest) * @param string $email Email address (for guest) * @return string */ function activetrail_external_id_for_contact($user_id = 0, $email = '') { if ($user_id) return activetrail_generate_external_id('CONTACT', $user_id); if ($email) return activetrail_generate_external_id('CONTACT', 'GUEST-' . md5(strtolower(trim($email)))); return activetrail_generate_external_id('CONTACT', 'GUEST-UNKNOWN'); } /** * Generate external ID for order * * @param int $order_id Order ID * @return string */ function activetrail_external_id_for_order($order_id) { return activetrail_generate_external_id('ORDER', $order_id); } /** * Generate external ID for cart * * @param string|int $key_or_user Cart key or user ID * @return string */ function activetrail_external_id_for_cart($key_or_user) { return activetrail_generate_external_id('CART', $key_or_user); } // Define external name constant. if (!defined('ACTIVETRAIL_EXTERNAL_NAME')) { define('ACTIVETRAIL_EXTERNAL_NAME', 'WooCommerce'); } /** * Check if WooCommerce is active */ function activetrail_wc_check_woocommerce() { if (!class_exists('WooCommerce')) { add_action('admin_notices', 'activetrail_wc_missing_woocommerce_notice'); return false; } return true; } /** * Display notice if WooCommerce is not active */ function activetrail_wc_missing_woocommerce_notice() { ?>

'percent', 'fixed_price' => 'fixed_cart', 'free_shipping' => 'fixed_cart', 'percent' => 'percent', 'fixed_cart' => 'fixed_cart', 'fixed_product' => 'fixed_product', ); return $type_map[$type] ?? $type; } /** * Normalize coupon payloads coming from ActiveTrail (camelCase โ†’ snake_case) * * @param array $payload * @return array */ function activetrail_wc_normalize_coupon_payload($payload) { if (!is_array($payload) || empty($payload)) { return $payload; } $field_map = array( 'couponCode' => 'coupon_code', 'discountType' => 'discount_type', 'amount' => 'amount', 'description' => 'description', 'expiryDate' => 'date_expires', 'minCartValue' => 'minimum_amount', 'maxCartValue' => 'maximum_amount', 'individualUse' => 'individual_use', 'usageLimit' => 'usage_limit', 'usageLimitPerUser' => 'usage_limit_per_user', 'freeShipping' => 'free_shipping', 'excludeSaleItems' => 'exclude_sale_items', 'productIds' => 'product_ids', 'excludedProductIds' => 'excluded_product_ids', 'productCategories' => 'product_categories', 'excludedProductCategories' => 'excluded_product_categories', 'emailRestrictions' => 'email_restrictions', 'externalId' => 'external_id', 'accountId' => 'account_id', ); $normalized = $payload; $converted = false; foreach ($field_map as $camel => $snake) { if (array_key_exists($camel, $payload) && (!array_key_exists($snake, $normalized) || $normalized[$snake] === '')) { $normalized[$snake] = $payload[$camel]; $converted = true; } } if ($converted && isset($normalized['discount_type'])) { $normalized['discount_type'] = activetrail_wc_map_discount_type($normalized['discount_type']); } $incoming_discount_type = $payload['discountType'] ?? ''; if ($incoming_discount_type === 'free_shipping') { $normalized['free_shipping'] = true; $normalized['discount_type'] = 'fixed_cart'; if (!isset($normalized['amount']) || $normalized['amount'] === '') { $normalized['amount'] = '0'; } } return $normalized; } /** * Handle ActiveTrail coupon creation requests */ function activetrail_wc_handle_coupon_request() { // Check if this is a coupon creation request. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately if (!isset($_GET['activetrail_coupon']) || sanitize_text_field(wp_unslash($_GET['activetrail_coupon'])) !== 'create') { return; } // Ensure WooCommerce is fully loaded. if (!function_exists('wc_get_product')) { if (class_exists('WooCommerce')) { WC(); } else { wp_die('WooCommerce is not available', 'ActiveTrail Coupon Error', array('response' => 500)); } } // Initialize logger if available. $logger = function_exists('activetrail_logger') ? activetrail_logger() : null; if ($logger) { $logger->product_feed('๐Ÿš€ ActiveTrail coupon creation request received', ActiveTrail_Logger::LEVEL_INFO, array( 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : 'unknown', 'ip' => isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : 'unknown', 'request_uri' => isset($_SERVER['REQUEST_URI']) ? esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])) : 'unknown' )); } // Check if request method is POST. $request_method = isset($_SERVER['REQUEST_METHOD']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_METHOD'])) : ''; if ($request_method !== 'POST') { if ($logger) { $logger->product_feed('โŒ Invalid request method for coupon creation', ActiveTrail_Logger::LEVEL_ERROR, array( 'method' => $request_method )); } wp_die('Method not allowed', 'ActiveTrail Coupon Error', array('response' => 405)); } // Get the request body. $input = file_get_contents('php://input'); $data = json_decode($input, true); if (!$data) { if ($logger) { $logger->product_feed('โŒ Invalid JSON data received', ActiveTrail_Logger::LEVEL_ERROR); } wp_die('Invalid JSON data', 'ActiveTrail Coupon Error', array('response' => 400)); } // Sanitize decoded data. $data = activetrail_wc_sanitize_array($data); $data = activetrail_wc_normalize_coupon_payload($data); // Validate required fields. $requires_amount = empty($data['free_shipping']); $required_fields = array('coupon_code', 'discount_type'); foreach ($required_fields as $field) { if (empty($data[$field])) { if ($logger) { $logger->product_feed('โŒ Missing required field: ' . $field, ActiveTrail_Logger::LEVEL_ERROR); } wp_die(esc_html('Missing required field: ' . $field), 'ActiveTrail Coupon Error', array('response' => 400)); } } if ($requires_amount && empty($data['amount'])) { if ($logger) { $logger->product_feed('โŒ Missing required field: amount', ActiveTrail_Logger::LEVEL_ERROR); } wp_die('Missing required field: amount', 'ActiveTrail Coupon Error', array('response' => 400)); } elseif (!$requires_amount && !isset($data['amount'])) { $data['amount'] = '0'; } // Validate discount type. $valid_types = array('percent', 'fixed_cart', 'fixed_product'); if (!in_array($data['discount_type'], $valid_types)) { if ($logger) { $logger->product_feed('โŒ Invalid discount type: ' . $data['discount_type'], ActiveTrail_Logger::LEVEL_ERROR); } wp_die(esc_html('Invalid discount_type. Must be one of: ' . implode(', ', $valid_types)), 'ActiveTrail Coupon Error', array('response' => 400)); } if ($requires_amount) { $amount = floatval($data['amount']); if ($amount <= 0) { if ($logger) { $logger->product_feed('โŒ Invalid amount: ' . $data['amount'], ActiveTrail_Logger::LEVEL_ERROR); } wp_die('Amount must be greater than 0', 'ActiveTrail Coupon Error', array('response' => 400)); } if ($data['discount_type'] === 'percent' && $amount > 100) { if ($logger) { $logger->product_feed('โŒ Percent amount cannot exceed 100: ' . $data['amount'], ActiveTrail_Logger::LEVEL_ERROR); } wp_die('Percent amount cannot exceed 100', 'ActiveTrail Coupon Error', array('response' => 400)); } } else { $data['amount'] = '0'; } // Check for existing coupon by external_id (idempotency). if (!empty($data['external_id'])) { $existing_coupon_id = activetrail_wc_find_coupon_by_external_id($data['external_id']); if ($existing_coupon_id) { // Update existing coupon. $result = activetrail_wc_update_coupon($existing_coupon_id, $data); if ($result['success']) { if ($logger) { $logger->product_feed('โœ… Coupon updated successfully: ' . $data['coupon_code'] . ' (ID: ' . $existing_coupon_id . ')', ActiveTrail_Logger::LEVEL_INFO); } wp_send_json($result, 200); } else { if ($logger) { $logger->product_feed('โŒ Failed to update coupon: ' . $result['message'], ActiveTrail_Logger::LEVEL_ERROR); } wp_send_json($result, 500); } } } // Check if coupon code already exists (without external_id). if (empty($data['external_id'])) { $existing_coupon_id = wc_get_coupon_id_by_code($data['coupon_code']); if ($existing_coupon_id) { if ($logger) { $logger->product_feed('โŒ Coupon code already exists without external_id: ' . $data['coupon_code'], ActiveTrail_Logger::LEVEL_ERROR); } wp_send_json(array( 'success' => false, 'message' => 'Coupon code already exists. Use external_id for idempotency.' ), 409); } } // Create new coupon. $result = activetrail_wc_create_coupon($data); if ($result['success']) { if ($logger) { $logger->product_feed('โœ… Coupon created successfully: ' . $result['coupon_code'] . ' (ID: ' . $result['coupon_id'] . ')', ActiveTrail_Logger::LEVEL_INFO); } wp_send_json($result, 201); } else { if ($logger) { $logger->product_feed('โŒ Failed to create coupon: ' . $result['message'], ActiveTrail_Logger::LEVEL_ERROR); } wp_send_json($result, 500); } } /** * Create WooCommerce coupon */ function activetrail_wc_create_coupon($coupon_data) { try { // Generate unique code if not provided. if (empty($coupon_data['coupon_code'])) { $prefix = $coupon_data['prefix'] ?? 'AT'; $coupon_data['coupon_code'] = activetrail_wc_generate_unique_code($prefix); } // Create new coupon. $coupon = new WC_Coupon(); // Set basic properties. $coupon->set_code($coupon_data['coupon_code']); $coupon->set_discount_type($coupon_data['discount_type']); $coupon->set_amount(floatval($coupon_data['amount'])); // Set description. if (!empty($coupon_data['description'])) { $coupon->set_description($coupon_data['description']); } // Set expiry date. if (!empty($coupon_data['date_expires'])) { $coupon->set_date_expires($coupon_data['date_expires']); } // Set usage limits. if (isset($coupon_data['usage_limit']) && $coupon_data['usage_limit'] > 0) { $coupon->set_usage_limit($coupon_data['usage_limit']); } if (isset($coupon_data['usage_limit_per_user']) && $coupon_data['usage_limit_per_user'] > 0) { $coupon->set_usage_limit_per_user($coupon_data['usage_limit_per_user']); } // Set amount limits. if (!empty($coupon_data['minimum_amount'])) { $coupon->set_minimum_amount(floatval($coupon_data['minimum_amount'])); } if (!empty($coupon_data['maximum_amount'])) { $coupon->set_maximum_amount(floatval($coupon_data['maximum_amount'])); } // Set boolean properties. if (isset($coupon_data['individual_use'])) { $coupon->set_individual_use($coupon_data['individual_use']); } if (isset($coupon_data['free_shipping'])) { $coupon->set_free_shipping($coupon_data['free_shipping']); } // Set product restrictions. if (!empty($coupon_data['product_ids'])) { $coupon->set_product_ids($coupon_data['product_ids']); } if (!empty($coupon_data['excluded_product_ids'])) { $coupon->set_excluded_product_ids($coupon_data['excluded_product_ids']); } if (!empty($coupon_data['product_categories'])) { $coupon->set_product_categories($coupon_data['product_categories']); } if (!empty($coupon_data['excluded_product_categories'])) { $coupon->set_excluded_product_categories($coupon_data['excluded_product_categories']); } // Set email restrictions. if (!empty($coupon_data['email_restrictions'])) { $coupon->set_email_restrictions($coupon_data['email_restrictions']); } // Save coupon. $coupon_id = $coupon->save(); if (!$coupon_id) { return array( 'success' => false, 'message' => 'Failed to save coupon' ); } // Mark as ActiveTrail coupon. update_post_meta($coupon_id, '_activetrail_coupon', 'yes'); update_post_meta($coupon_id, '_activetrail_created_date', current_time('mysql')); // Store external_id if provided. if (!empty($coupon_data['external_id'])) { update_post_meta($coupon_id, '_activetrail_external_id', $coupon_data['external_id']); } // Store account_id if provided. if (!empty($coupon_data['account_id'])) { update_post_meta($coupon_id, '_activetrail_account_id', $coupon_data['account_id']); } return array( 'success' => true, 'message' => 'Coupon created successfully', 'coupon_id' => $coupon_id, 'coupon_code' => $coupon_data['coupon_code'], 'external_id' => $coupon_data['external_id'] ?? null ); } catch (Exception $e) { return array( 'success' => false, 'message' => 'Failed to create coupon: ' . $e->getMessage() ); } } /** * Update existing WooCommerce coupon */ function activetrail_wc_update_coupon($coupon_id, $coupon_data) { try { $coupon = new WC_Coupon($coupon_id); // Update basic properties. $coupon->set_discount_type($coupon_data['discount_type']); $coupon->set_amount(floatval($coupon_data['amount'])); // Update description. if (!empty($coupon_data['description'])) { $coupon->set_description($coupon_data['description']); } // Update expiry date. if (!empty($coupon_data['date_expires'])) { $coupon->set_date_expires($coupon_data['date_expires']); } // Update usage limits. if (isset($coupon_data['usage_limit'])) { $coupon->set_usage_limit($coupon_data['usage_limit']); } if (isset($coupon_data['usage_limit_per_user'])) { $coupon->set_usage_limit_per_user($coupon_data['usage_limit_per_user']); } // Update amount limits. if (isset($coupon_data['minimum_amount'])) { $coupon->set_minimum_amount(floatval($coupon_data['minimum_amount'])); } if (isset($coupon_data['maximum_amount'])) { $coupon->set_maximum_amount(floatval($coupon_data['maximum_amount'])); } // Update boolean properties. if (isset($coupon_data['individual_use'])) { $coupon->set_individual_use($coupon_data['individual_use']); } if (isset($coupon_data['free_shipping'])) { $coupon->set_free_shipping($coupon_data['free_shipping']); } // Update product restrictions. if (isset($coupon_data['product_ids'])) { $coupon->set_product_ids($coupon_data['product_ids']); } if (isset($coupon_data['excluded_product_ids'])) { $coupon->set_excluded_product_ids($coupon_data['excluded_product_ids']); } if (isset($coupon_data['product_categories'])) { $coupon->set_product_categories($coupon_data['product_categories']); } if (isset($coupon_data['excluded_product_categories'])) { $coupon->set_excluded_product_categories($coupon_data['excluded_product_categories']); } // Update email restrictions. if (isset($coupon_data['email_restrictions'])) { $coupon->set_email_restrictions($coupon_data['email_restrictions']); } // Save coupon. $coupon->save(); // Update metadata. update_post_meta($coupon_id, '_activetrail_updated_date', current_time('mysql')); return array( 'success' => true, 'message' => 'Coupon updated successfully', 'coupon_id' => $coupon_id, 'coupon_code' => $coupon_data['coupon_code'], 'external_id' => $coupon_data['external_id'] ?? null ); } catch (Exception $e) { return array( 'success' => false, 'message' => 'Failed to update coupon: ' . $e->getMessage() ); } } /** * Find coupon by external_id */ function activetrail_wc_find_coupon_by_external_id($external_id) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct query needed for coupon lookup $coupon_id = $wpdb->get_var($wpdb->prepare( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_activetrail_external_id' AND meta_value = %s", $external_id )); return $coupon_id ? intval($coupon_id) : false; } // Hook the coupon creation handler. add_action('init', 'activetrail_wc_handle_coupon_request'); /** * Handle ActiveTrail coupon creation via GET request (alternative method) */ function activetrail_wc_handle_coupon_request_get() { // Check if this is a GET coupon creation request. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately if (!isset($_GET['activetrail_coupon']) || sanitize_text_field(wp_unslash($_GET['activetrail_coupon'])) !== 'createCoupon') { return; } // Ensure WooCommerce is fully loaded. if (!function_exists('wc_get_product')) { if (class_exists('WooCommerce')) { WC(); } else { wp_die('WooCommerce is not available', 'ActiveTrail Coupon Error', array('response' => 500)); } } // Initialize logger if available. $logger = function_exists('activetrail_logger') ? activetrail_logger() : null; if ($logger) { $logger->product_feed('๐Ÿš€ ActiveTrail GET coupon creation request received', ActiveTrail_Logger::LEVEL_INFO, array( 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : 'unknown', 'ip' => isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : 'unknown', 'request_uri' => isset($_SERVER['REQUEST_URI']) ? esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])) : 'unknown' )); } // Check if args parameter exists. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External feed endpoint, authentication handled separately if (!isset($_GET['args'])) { if ($logger) { $logger->product_feed('โŒ Missing args parameter', ActiveTrail_Logger::LEVEL_ERROR); } wp_die('Missing args parameter', 'ActiveTrail Coupon Error', array('response' => 400)); } // Decode the args parameter. // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- External feed endpoint, args are base64 encoded and decoded $args = isset($_GET['args']) ? wp_unslash($_GET['args']) : ''; $args = json_decode(base64_decode($args), true); if (!$args) { if ($logger) { $logger->product_feed('โŒ Invalid args parameter - failed to decode', ActiveTrail_Logger::LEVEL_ERROR); } wp_die('Invalid args parameter', 'ActiveTrail Coupon Error', array('response' => 400)); } // Sanitize decoded data. if (is_array($args)) { $args = activetrail_wc_sanitize_array($args); } $args = activetrail_wc_normalize_coupon_payload($args); $requires_amount = empty($args['free_shipping']); $required_fields = array('coupon_code', 'discount_type'); foreach ($required_fields as $field) { if (empty($args[$field])) { if ($logger) { $logger->product_feed('โŒ Missing required field: ' . $field, ActiveTrail_Logger::LEVEL_ERROR); } wp_die(esc_html('Missing required field: ' . $field), 'ActiveTrail Coupon Error', array('response' => 400)); } } if ($requires_amount && empty($args['amount'])) { if ($logger) { $logger->product_feed('โŒ Missing required field: amount', ActiveTrail_Logger::LEVEL_ERROR); } wp_die('Missing required field: amount', 'ActiveTrail Coupon Error', array('response' => 400)); } elseif (!$requires_amount && !isset($args['amount'])) { $args['amount'] = '0'; } // Validate discount type. $valid_types = array('percent', 'fixed_cart', 'fixed_product'); if (!in_array($args['discount_type'], $valid_types)) { if ($logger) { $logger->product_feed('โŒ Invalid discount type: ' . $args['discount_type'], ActiveTrail_Logger::LEVEL_ERROR); } wp_die(esc_html('Invalid discount_type. Must be one of: ' . implode(', ', $valid_types)), 'ActiveTrail Coupon Error', array('response' => 400)); } if ($requires_amount) { $amount = floatval($args['amount']); if ($amount <= 0) { if ($logger) { $logger->product_feed('โŒ Invalid amount: ' . $args['amount'], ActiveTrail_Logger::LEVEL_ERROR); } wp_die('Amount must be greater than 0', 'ActiveTrail Coupon Error', array('response' => 400)); } if ($args['discount_type'] === 'percent' && $amount > 100) { if ($logger) { $logger->product_feed('โŒ Percent amount cannot exceed 100: ' . $args['amount'], ActiveTrail_Logger::LEVEL_ERROR); } wp_die('Percent amount cannot exceed 100', 'ActiveTrail Coupon Error', array('response' => 400)); } } else { $args['amount'] = '0'; } // Check for existing coupon by external_id (idempotency). if (!empty($args['external_id'])) { $existing_coupon_id = activetrail_wc_find_coupon_by_external_id($args['external_id']); if ($existing_coupon_id) { // Update existing coupon. $result = activetrail_wc_update_coupon($existing_coupon_id, $args); if ($result['success']) { if ($logger) { $logger->product_feed('โœ… Coupon updated successfully via GET: ' . $args['coupon_code'] . ' (ID: ' . $existing_coupon_id . ')', ActiveTrail_Logger::LEVEL_INFO); } wp_send_json($result, 200); } else { if ($logger) { $logger->product_feed('โŒ Failed to update coupon via GET: ' . $result['message'], ActiveTrail_Logger::LEVEL_ERROR); } wp_send_json($result, 500); } } } // Check if coupon code already exists (without external_id). if (empty($args['external_id'])) { $existing_coupon_id = wc_get_coupon_id_by_code($args['coupon_code']); if ($existing_coupon_id) { if ($logger) { $logger->product_feed('โŒ Coupon code already exists without external_id via GET: ' . $args['coupon_code'], ActiveTrail_Logger::LEVEL_ERROR); } wp_send_json(array( 'success' => false, 'message' => 'Coupon code already exists. Use external_id for idempotency.' ), 409); } } // Create new coupon. $result = activetrail_wc_create_coupon($args); if ($result['success']) { if ($logger) { $logger->product_feed('โœ… Coupon created successfully via GET: ' . $result['coupon_code'] . ' (ID: ' . $result['coupon_id'] . ')', ActiveTrail_Logger::LEVEL_INFO); } wp_send_json($result, 201); } else { if ($logger) { $logger->product_feed('โŒ Failed to create coupon via GET: ' . $result['message'], ActiveTrail_Logger::LEVEL_ERROR); } wp_send_json($result, 500); } } // Hook the GET coupon creation handler. add_action('init', 'activetrail_wc_handle_coupon_request_get'); /** * Handle ActiveTrail coupon creation with defaults via GET request */ function activetrail_wc_handle_coupon_request_defaults() { // Check if this is a default coupon creation request. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately $activetrail_coupon = isset($_GET['activetrail_coupon']) ? sanitize_text_field(wp_unslash($_GET['activetrail_coupon'])) : ''; if ($activetrail_coupon !== 'createDefault') { return; } // Ensure WooCommerce is fully loaded. if (!function_exists('wc_get_product')) { if (class_exists('WooCommerce')) { WC(); } else { wp_die('WooCommerce is not available', 'ActiveTrail Coupon Error', array('response' => 500)); } } // Initialize logger if available. $logger = function_exists('activetrail_logger') ? activetrail_logger() : null; if ($logger) { $logger->product_feed('๐Ÿš€ ActiveTrail default coupon creation request received', ActiveTrail_Logger::LEVEL_INFO, array( 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : 'unknown', 'ip' => isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : 'unknown', 'request_uri' => isset($_SERVER['REQUEST_URI']) ? esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])) : 'unknown' )); } // Check if args parameter exists. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External feed endpoint, authentication handled separately if (!isset($_GET['args'])) { if ($logger) { $logger->product_feed('โŒ Missing args parameter for default coupon', ActiveTrail_Logger::LEVEL_ERROR); } wp_die('Missing args parameter', 'ActiveTrail Coupon Error', array('response' => 400)); } // Decode the args parameter. // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- External feed endpoint, args are base64 encoded and decoded $args = isset($_GET['args']) ? wp_unslash($_GET['args']) : ''; $args = json_decode(base64_decode($args), true); if (!$args) { if ($logger) { $logger->product_feed('โŒ Invalid args parameter for default coupon - failed to decode', ActiveTrail_Logger::LEVEL_ERROR); } wp_die('Invalid args parameter', 'ActiveTrail Coupon Error', array('response' => 400)); } // Sanitize decoded data. if (is_array($args)) { $args = activetrail_wc_sanitize_array($args); } $args = activetrail_wc_normalize_coupon_payload($args); // Validate required fields (only amount is required for default creation). if (empty($args['amount'])) { if ($logger) { $logger->product_feed('โŒ Missing required field: amount for default coupon', ActiveTrail_Logger::LEVEL_ERROR); } wp_die('Missing required field: amount', 'ActiveTrail Coupon Error', array('response' => 400)); } // Validate amount. $amount = floatval($args['amount']); if ($amount <= 0) { if ($logger) { $logger->product_feed('โŒ Invalid amount for default coupon: ' . $args['amount'], ActiveTrail_Logger::LEVEL_ERROR); } wp_die('Amount must be greater than 0', 'ActiveTrail Coupon Error', array('response' => 400)); } // Check for existing coupon by external_id (idempotency). if (!empty($args['external_id'])) { $existing_coupon_id = activetrail_wc_find_coupon_by_external_id($args['external_id']); if ($existing_coupon_id) { // Update existing coupon. $result = activetrail_wc_update_coupon($existing_coupon_id, $args); if ($result['success']) { if ($logger) { $logger->product_feed('โœ… Default coupon updated successfully: ' . $result['coupon_code'] . ' (ID: ' . $existing_coupon_id . ')', ActiveTrail_Logger::LEVEL_INFO); } wp_send_json($result, 200); } else { if ($logger) { $logger->product_feed('โŒ Failed to update default coupon: ' . $result['message'], ActiveTrail_Logger::LEVEL_ERROR); } wp_send_json($result, 500); } } } // Create new coupon with defaults. $result = activetrail_wc_create_coupon_with_defaults($args); if ($result['success']) { if ($logger) { $logger->product_feed('โœ… Default coupon created successfully: ' . $result['coupon_code'] . ' (ID: ' . $result['coupon_id'] . ')', ActiveTrail_Logger::LEVEL_INFO); } wp_send_json($result, 201); } else { if ($logger) { $logger->product_feed('โŒ Failed to create default coupon: ' . $result['message'], ActiveTrail_Logger::LEVEL_ERROR); } wp_send_json($result, 500); } } // Hook the default coupon creation handler. add_action('init', 'activetrail_wc_handle_coupon_request_defaults'); /** * Handle ActiveTrail coupon creation with custom prefix via GET request */ function activetrail_wc_handle_coupon_request_prefix() { // Check if this is a prefix coupon creation request. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately $activetrail_coupon = isset($_GET['activetrail_coupon']) ? sanitize_text_field(wp_unslash($_GET['activetrail_coupon'])) : ''; if ($activetrail_coupon !== 'createPrefix') { return; } // Ensure WooCommerce is fully loaded. if (!function_exists('wc_get_product')) { if (class_exists('WooCommerce')) { WC(); } else { wp_die('WooCommerce is not available', 'ActiveTrail Coupon Error', array('response' => 500)); } } // Initialize logger if available. $logger = function_exists('activetrail_logger') ? activetrail_logger() : null; if ($logger) { $logger->product_feed('๐Ÿš€ ActiveTrail prefix coupon creation request received', ActiveTrail_Logger::LEVEL_INFO, array( 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : 'unknown', 'ip' => isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : 'unknown', 'request_uri' => isset($_SERVER['REQUEST_URI']) ? esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])) : 'unknown' )); } // Check if args parameter exists. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External feed endpoint, authentication handled separately if (!isset($_GET['args'])) { if ($logger) { $logger->product_feed('โŒ Missing args parameter for prefix coupon', ActiveTrail_Logger::LEVEL_ERROR); } wp_die('Missing args parameter', 'ActiveTrail Coupon Error', array('response' => 400)); } // Decode the args parameter. // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- External feed endpoint, args are base64 encoded and decoded $args = isset($_GET['args']) ? wp_unslash($_GET['args']) : ''; $args = json_decode(base64_decode($args), true); if (!$args) { if ($logger) { $logger->product_feed('โŒ Invalid args parameter for prefix coupon - failed to decode', ActiveTrail_Logger::LEVEL_ERROR); } wp_die('Invalid args parameter', 'ActiveTrail Coupon Error', array('response' => 400)); } // Sanitize decoded data. if (is_array($args)) { $args = activetrail_wc_sanitize_array($args); } $args = activetrail_wc_normalize_coupon_payload($args); // Check if prefix is provided. if (empty($args['prefix'])) { if ($logger) { $logger->product_feed('โŒ Missing prefix parameter for prefix coupon', ActiveTrail_Logger::LEVEL_ERROR); } wp_die('Missing prefix parameter', 'ActiveTrail Coupon Error', array('response' => 400)); } // Validate required fields (only amount is required for prefix creation). if (empty($args['amount'])) { if ($logger) { $logger->product_feed('โŒ Missing required field: amount for prefix coupon', ActiveTrail_Logger::LEVEL_ERROR); } wp_die('Missing required field: amount', 'ActiveTrail Coupon Error', array('response' => 400)); } // Validate amount. $amount = floatval($args['amount']); if ($amount <= 0) { if ($logger) { $logger->product_feed('โŒ Invalid amount for prefix coupon: ' . $args['amount'], ActiveTrail_Logger::LEVEL_ERROR); } wp_die('Amount must be greater than 0', 'ActiveTrail Coupon Error', array('response' => 400)); } // Check for existing coupon by external_id (idempotency). if (!empty($args['external_id'])) { $existing_coupon_id = activetrail_wc_find_coupon_by_external_id($args['external_id']); if ($existing_coupon_id) { // Update existing coupon. $result = activetrail_wc_update_coupon($existing_coupon_id, $args); if ($result['success']) { if ($logger) { $logger->product_feed('โœ… Prefix coupon updated successfully: ' . $result['coupon_code'] . ' (ID: ' . $existing_coupon_id . ')', ActiveTrail_Logger::LEVEL_INFO); } wp_send_json($result, 200); } else { if ($logger) { $logger->product_feed('โŒ Failed to update prefix coupon: ' . $result['message'], ActiveTrail_Logger::LEVEL_ERROR); } wp_send_json($result, 500); } } } // Create new coupon with custom prefix. $result = activetrail_wc_create_coupon_with_prefix($args['prefix'], $args); if ($result['success']) { if ($logger) { $logger->product_feed('โœ… Prefix coupon created successfully: ' . $result['coupon_code'] . ' (ID: ' . $result['coupon_id'] . ')', ActiveTrail_Logger::LEVEL_INFO); } wp_send_json($result, 201); } else { if ($logger) { $logger->product_feed('โŒ Failed to create prefix coupon: ' . $result['message'], ActiveTrail_Logger::LEVEL_ERROR); } wp_send_json($result, 500); } } // Hook the prefix coupon creation handler. add_action('init', 'activetrail_wc_handle_coupon_request_prefix'); /** * Handle ActiveTrail coupon creation with automatic code via GET request */ function activetrail_wc_handle_coupon_request_auto() { // Check if this is an auto code coupon creation request. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately $activetrail_coupon = isset($_GET['activetrail_coupon']) ? sanitize_text_field(wp_unslash($_GET['activetrail_coupon'])) : ''; if ($activetrail_coupon !== 'createAuto') { return; } // Ensure WooCommerce is fully loaded. if (!function_exists('wc_get_product')) { if (class_exists('WooCommerce')) { WC(); } else { wp_die('WooCommerce is not available', 'ActiveTrail Coupon Error', array('response' => 500)); } } // Initialize logger if available. $logger = function_exists('activetrail_logger') ? activetrail_logger() : null; if ($logger) { $logger->product_feed('๐Ÿš€ ActiveTrail auto code coupon creation request received', ActiveTrail_Logger::LEVEL_INFO, array( 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : 'unknown', 'ip' => isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : 'unknown', 'request_uri' => isset($_SERVER['REQUEST_URI']) ? esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])) : 'unknown' )); } // Check if args parameter exists. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External feed endpoint, authentication handled separately if (!isset($_GET['args'])) { if ($logger) { $logger->product_feed('โŒ Missing args parameter for auto code coupon', ActiveTrail_Logger::LEVEL_ERROR); } wp_die('Missing args parameter', 'ActiveTrail Coupon Error', array('response' => 400)); } // Decode the args parameter. // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- External feed endpoint, args are base64 encoded and decoded $args = isset($_GET['args']) ? wp_unslash($_GET['args']) : ''; $args = json_decode(base64_decode($args), true); if (!$args) { if ($logger) { $logger->product_feed('โŒ Invalid args parameter for auto code coupon - failed to decode', ActiveTrail_Logger::LEVEL_ERROR); } wp_die('Invalid args parameter', 'ActiveTrail Coupon Error', array('response' => 400)); } // Sanitize decoded data. if (is_array($args)) { $args = activetrail_wc_sanitize_array($args); } $args = activetrail_wc_normalize_coupon_payload($args); // Validate required fields (only amount is required for auto code creation). if (empty($args['amount'])) { if ($logger) { $logger->product_feed('โŒ Missing required field: amount for auto code coupon', ActiveTrail_Logger::LEVEL_ERROR); } wp_die('Missing required field: amount', 'ActiveTrail Coupon Error', array('response' => 400)); } // Validate amount. $amount = floatval($args['amount']); if ($amount <= 0) { if ($logger) { $logger->product_feed('โŒ Invalid amount for auto code coupon: ' . $args['amount'], ActiveTrail_Logger::LEVEL_ERROR); } wp_die('Amount must be greater than 0', 'ActiveTrail Coupon Error', array('response' => 400)); } // Check for existing coupon by external_id (idempotency). if (!empty($args['external_id'])) { $existing_coupon_id = activetrail_wc_find_coupon_by_external_id($args['external_id']); if ($existing_coupon_id) { // Update existing coupon. $result = activetrail_wc_update_coupon($existing_coupon_id, $args); if ($result['success']) { if ($logger) { $logger->product_feed('โœ… Auto code coupon updated successfully: ' . $result['coupon_code'] . ' (ID: ' . $existing_coupon_id . ')', ActiveTrail_Logger::LEVEL_INFO); } wp_send_json($result, 200); } else { if ($logger) { $logger->product_feed('โŒ Failed to update auto code coupon: ' . $result['message'], ActiveTrail_Logger::LEVEL_ERROR); } wp_send_json($result, 500); } } } // Create new coupon with automatic code generation. $result = activetrail_wc_create_coupon_auto_code($args); if ($result['success']) { if ($logger) { $logger->product_feed('โœ… Auto code coupon created successfully: ' . $result['coupon_code'] . ' (ID: ' . $result['coupon_id'] . ')', ActiveTrail_Logger::LEVEL_INFO); } wp_send_json($result, 201); } else { if ($logger) { $logger->product_feed('โŒ Failed to create auto code coupon: ' . $result['message'], ActiveTrail_Logger::LEVEL_ERROR); } wp_send_json($result, 500); } } // Hook the auto code coupon creation handler. add_action('init', 'activetrail_wc_handle_coupon_request_auto'); /** * Handle ActiveTrail coupon creation with custom defaults via GET request */ function activetrail_wc_handle_coupon_request_custom_defaults() { // Check if this is a custom defaults coupon creation request. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately $activetrail_coupon = isset($_GET['activetrail_coupon']) ? sanitize_text_field(wp_unslash($_GET['activetrail_coupon'])) : ''; if ($activetrail_coupon !== 'createCustom') { return; } // Ensure WooCommerce is fully loaded. if (!function_exists('wc_get_product')) { if (class_exists('WooCommerce')) { WC(); } else { wp_die('WooCommerce is not available', 'ActiveTrail Coupon Error', array('response' => 500)); } } // Initialize logger if available. $logger = function_exists('activetrail_logger') ? activetrail_logger() : null; if ($logger) { $logger->product_feed('๐Ÿš€ ActiveTrail custom defaults coupon creation request received', ActiveTrail_Logger::LEVEL_INFO, array( 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : 'unknown', 'ip' => isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : 'unknown', 'request_uri' => isset($_SERVER['REQUEST_URI']) ? esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])) : 'unknown' )); } // Check if args parameter exists. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External feed endpoint, authentication handled separately if (!isset($_GET['args'])) { if ($logger) { $logger->product_feed('โŒ Missing args parameter for custom defaults coupon', ActiveTrail_Logger::LEVEL_ERROR); } wp_die('Missing args parameter', 'ActiveTrail Coupon Error', array('response' => 400)); } // Decode the args parameter. // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- External feed endpoint, args are base64 encoded and decoded $args = isset($_GET['args']) ? wp_unslash($_GET['args']) : ''; $args = json_decode(base64_decode($args), true); if (!$args) { if ($logger) { $logger->product_feed('โŒ Invalid args parameter for custom defaults coupon - failed to decode', ActiveTrail_Logger::LEVEL_ERROR); } wp_die('Invalid args parameter', 'ActiveTrail Coupon Error', array('response' => 400)); } // Sanitize decoded data. if (is_array($args)) { $args = activetrail_wc_sanitize_array($args); } $args = activetrail_wc_normalize_coupon_payload($args); // Check if custom_defaults parameter exists. if (!isset($args['custom_defaults'])) { if ($logger) { $logger->product_feed('โŒ Missing custom_defaults parameter for custom defaults coupon', ActiveTrail_Logger::LEVEL_ERROR); } wp_die('Missing custom_defaults parameter', 'ActiveTrail Coupon Error', array('response' => 400)); } // Validate required fields (only amount is required for custom defaults creation). if (empty($args['amount'])) { if ($logger) { $logger->product_feed('โŒ Missing required field: amount for custom defaults coupon', ActiveTrail_Logger::LEVEL_ERROR); } wp_die('Missing required field: amount', 'ActiveTrail Coupon Error', array('response' => 400)); } // Validate amount. $amount = floatval($args['amount']); if ($amount <= 0) { if ($logger) { $logger->product_feed('โŒ Invalid amount for custom defaults coupon: ' . $args['amount'], ActiveTrail_Logger::LEVEL_ERROR); } wp_die('Amount must be greater than 0', 'ActiveTrail Coupon Error', array('response' => 400)); } // Check for existing coupon by external_id (idempotency). if (!empty($args['external_id'])) { $existing_coupon_id = activetrail_wc_find_coupon_by_external_id($args['external_id']); if ($existing_coupon_id) { // Update existing coupon. $result = activetrail_wc_update_coupon($existing_coupon_id, $args); if ($result['success']) { if ($logger) { $logger->product_feed('โœ… Custom defaults coupon updated successfully: ' . $result['coupon_code'] . ' (ID: ' . $existing_coupon_id . ')', ActiveTrail_Logger::LEVEL_INFO); } wp_send_json($result, 200); } else { if ($logger) { $logger->product_feed('โŒ Failed to update custom defaults coupon: ' . $result['message'], ActiveTrail_Logger::LEVEL_ERROR); } wp_send_json($result, 500); } } } // Extract custom defaults. $custom_defaults = $args['custom_defaults']; unset($args['custom_defaults']); // Remove from main data // Create new coupon with custom defaults. $result = activetrail_wc_create_coupon_custom_defaults($args, $custom_defaults); if ($result['success']) { if ($logger) { $logger->product_feed('โœ… Custom defaults coupon created successfully: ' . $result['coupon_code'] . ' (ID: ' . $result['coupon_id'] . ')', ActiveTrail_Logger::LEVEL_INFO); } wp_send_json($result, 201); } else { if ($logger) { $logger->product_feed('โŒ Failed to create custom defaults coupon: ' . $result['message'], ActiveTrail_Logger::LEVEL_ERROR); } wp_send_json($result, 500); } } // Hook the custom defaults coupon creation handler. add_action('init', 'activetrail_wc_handle_coupon_request_custom_defaults'); /** * Generate unique coupon code * * @param string $prefix Prefix for the coupon code * @return string Unique coupon code */ function activetrail_wc_generate_unique_code($prefix = 'AT') { $code = $prefix . wp_generate_password(6, false); // Check if code already exists. if (!empty(wc_get_coupon_id_by_code($code))) { return activetrail_wc_generate_unique_code($prefix); // Recursive call until unique code found } return $code; } /** * Create coupon with default values * * @param array $data Coupon data * @return array Result */ function activetrail_wc_create_coupon_with_defaults($data) { // Set default values. $defaults = array( 'discount_type' => 'fixed_cart', 'amount' => 0, 'usage_limit' => 1, 'usage_limit_per_user' => 1, 'expiry_date' => gmdate('Y-m-d', strtotime('+371 days')), 'free_shipping' => false, 'product_ids' => null, 'individual_use' => false, 'exclude_product_ids' => '', 'minimum_amount' => '', 'maximum_amount' => '', 'product_categories' => '', 'exclude_product_categories' => '', 'exclude_sale_items' => true, 'description' => '' ); // Merge with provided data. $merged = array_merge($defaults, $data); // Generate unique code if not provided. if (empty($merged['coupon_code'])) { $prefix = $merged['prefix'] ?? 'AT'; $merged['coupon_code'] = activetrail_wc_generate_unique_code($prefix); } // Create the coupon. return activetrail_wc_create_coupon($merged); } /** * Create coupon with custom prefix * * @param string $prefix Custom prefix for coupon code * @param array $data Coupon data * @return array Result */ function activetrail_wc_create_coupon_with_prefix($prefix, $data) { // Set the prefix. $data['prefix'] = $prefix; // Create coupon with defaults. return activetrail_wc_create_coupon_with_defaults($data); } /** * Create coupon with automatic unique code * * @param array $data Coupon data * @return array Result */ function activetrail_wc_create_coupon_auto_code($data) { // Generate unique code automatically. $prefix = $data['prefix'] ?? 'AT'; $data['coupon_code'] = activetrail_wc_generate_unique_code($prefix); // Create the coupon. return activetrail_wc_create_coupon($data); } /** * Create coupon with custom defaults * * @param array $data Coupon data * @param array $custom_defaults Custom default values * @return array Result */ function activetrail_wc_create_coupon_custom_defaults($data, $custom_defaults = array()) { // Set custom default values. $defaults = array( 'discount_type' => 'fixed_cart', 'amount' => 0, 'usage_limit' => 1, 'usage_limit_per_user' => 1, 'expiry_date' => gmdate('Y-m-d', strtotime('+371 days')), 'free_shipping' => false, 'product_ids' => null, 'individual_use' => false, 'exclude_product_ids' => '', 'minimum_amount' => '', 'maximum_amount' => '', 'product_categories' => '', 'exclude_product_categories' => '', 'exclude_sale_items' => true, 'description' => '' ); // Merge with custom defaults. $defaults = array_merge($defaults, $custom_defaults); // Merge with provided data. $merged = array_merge($defaults, $data); // Generate unique code if not provided. if (empty($merged['coupon_code'])) { $prefix = $merged['prefix'] ?? 'AT'; $merged['coupon_code'] = activetrail_wc_generate_unique_code($prefix); } // Create the coupon. return activetrail_wc_create_coupon($merged); } /** * Handle product feed export requests */ function activetrail_wc_handle_export_request() { // Check if this is an export request. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External feed endpoint, authentication handled separately if (!isset($_GET['activetrail_export']) || !isset($_GET['key'])) { return; } // Ensure WooCommerce is fully loaded. if (!function_exists('wc_get_product')) { // Force load WooCommerce if not loaded. if (class_exists('WooCommerce')) { WC(); } else { wp_die('WooCommerce is not available', 'ActiveTrail Export Error', array('response' => 500)); } } // Initialize logger if available. $logger = function_exists('activetrail_logger') ? activetrail_logger() : null; if ($logger) { $logger->product_feed('๐Ÿ”ง WooCommerce initialization check', ActiveTrail_Logger::LEVEL_DEBUG, array( 'wc_get_product_exists' => function_exists('wc_get_product'), 'woocommerce_class_exists' => class_exists('WooCommerce'), 'wc_function_exists' => function_exists('WC') )); } if ($logger) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External feed endpoint, authentication handled separately $logger->product_feed('๐Ÿš€ Product feed export request received', ActiveTrail_Logger::LEVEL_INFO, array( // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External feed endpoint, authentication handled separately 'export_type' => isset($_GET['activetrail_export']) ? sanitize_text_field(wp_unslash($_GET['activetrail_export'])) : 'unknown', // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External feed endpoint, authentication handled separately 'key_provided' => !empty($_GET['key']), 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : 'unknown', 'ip' => isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : 'unknown', 'request_uri' => isset($_SERVER['REQUEST_URI']) ? esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])) : 'unknown' )); } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External feed endpoint, authentication handled separately $export_type = isset($_GET['activetrail_export']) ? sanitize_text_field(wp_unslash($_GET['activetrail_export'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External feed endpoint, authentication handled separately $provided_key = isset($_GET['key']) ? sanitize_text_field(wp_unslash($_GET['key'])) : ''; if ($logger) { $logger->product_feed('๐Ÿ“‹ Request parameters validated', ActiveTrail_Logger::LEVEL_DEBUG, array( 'export_type' => $export_type, 'key_length' => strlen($provided_key) )); } // Validate export type. if (!in_array($export_type, array('google', 'facebook'))) { if ($logger) { $logger->product_feed('โŒ Invalid export type provided', ActiveTrail_Logger::LEVEL_ERROR, array( 'provided_type' => $export_type, 'allowed_types' => array('google', 'facebook') )); } wp_die('Invalid export type', 'ActiveTrail Export Error', array('response' => 400)); } // Get the stored key for this export type. $stored_key = get_option('activetrail_' . $export_type . '_feed_key', ''); if ($logger) { $logger->product_feed('๐Ÿ”‘ Checking stored key', ActiveTrail_Logger::LEVEL_DEBUG, array( 'export_type' => $export_type, 'stored_key_exists' => !empty($stored_key), 'stored_key_length' => strlen($stored_key), 'keys_match' => ($provided_key === $stored_key) )); } // Validate the key. if (empty($stored_key) || $provided_key !== $stored_key) { if ($logger) { $logger->product_feed('โŒ Key validation failed', ActiveTrail_Logger::LEVEL_ERROR, array( 'stored_key_empty' => empty($stored_key), 'keys_match' => ($provided_key === $stored_key), 'export_type' => $export_type )); } wp_die('Invalid or expired key', 'ActiveTrail Export Error', array('response' => 403)); } // Check if product feed is enabled. $settings = get_option('activetrail_wc_settings', array()); $enable_product_feed = true; // Always enabled if ($logger) { $logger->product_feed('โš™๏ธ Checking product feed settings', ActiveTrail_Logger::LEVEL_DEBUG, array( 'enable_product_feed' => $enable_product_feed, 'settings_exist' => !empty($settings), 'settings_keys' => array_keys($settings) )); } if (!$enable_product_feed) { if ($logger) { $logger->product_feed('โŒ Product feed is disabled in settings', ActiveTrail_Logger::LEVEL_WARNING); } wp_die('Product feed is disabled', 'ActiveTrail Export Error', array('response' => 403)); } // Load the product feed template. $template_file = ACTIVETRAIL_WC_PLUGIN_DIR . 'templates/product-feed.php'; if ($logger) { $logger->product_feed('๐Ÿ“ Checking template file', ActiveTrail_Logger::LEVEL_DEBUG, array( 'template_path' => $template_file, 'file_exists' => file_exists($template_file), 'file_readable' => is_readable($template_file) )); } if (!file_exists($template_file)) { if ($logger) { $logger->product_feed('โŒ Template file not found', ActiveTrail_Logger::LEVEL_ERROR, array( 'expected_path' => $template_file )); } wp_die('Feed template not found', 'ActiveTrail Export Error', array('response' => 500)); } if ($logger) { $logger->product_feed('โœ… All validations passed, generating feed', ActiveTrail_Logger::LEVEL_INFO, array( 'export_type' => $export_type )); } // Check for format parameter (allows XML with Facebook or JSON with Google). // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External feed endpoint, authentication handled separately $format = isset($_GET['format']) ? sanitize_text_field(wp_unslash($_GET['format'])) : ''; // Determine feed format based on format parameter or default behavior. if ($format === 'xml') { $feed_format = 'xml'; } elseif ($format === 'json') { $feed_format = 'json'; } else { // Default behavior: both google and facebook = XML. $feed_format = 'xml'; } // Set appropriate headers based on feed format. if ($feed_format === 'xml') { header('Content-Type: application/xml; charset=utf-8'); $filename = ($export_type === 'google') ? 'google-products.xml' : 'facebook-products.xml'; header('Content-Disposition: inline; filename="' . $filename . '"'); if ($logger) { $logger->product_feed('๐Ÿ“„ Set XML headers', ActiveTrail_Logger::LEVEL_DEBUG, array( 'export_type' => $export_type, 'format' => $format )); } } else { header('Content-Type: application/json; charset=utf-8'); $filename = ($export_type === 'facebook') ? 'facebook-products.json' : 'google-products.json'; header('Content-Disposition: inline; filename="' . $filename . '"'); if ($logger) { $logger->product_feed('๐Ÿ“„ Set JSON headers', ActiveTrail_Logger::LEVEL_DEBUG, array( 'export_type' => $export_type, 'format' => $format )); } } // Prevent caching. header('Cache-Control: no-cache, must-revalidate'); header('Expires: Sat, 26 Jul 1997 05:00:00 GMT'); // Update last export time. update_option('activetrail_feed_last_update', current_time('mysql')); if ($logger) { $logger->product_feed('๐Ÿ•’ Updated last export time', ActiveTrail_Logger::LEVEL_DEBUG, array( 'timestamp' => current_time('mysql') )); } // Include the template and pass the export type and format. $GLOBALS['activetrail_export_type'] = $export_type; $GLOBALS['activetrail_feed_format'] = $feed_format; if ($logger) { $logger->product_feed('๐ŸŽฏ Including template file to generate feed', ActiveTrail_Logger::LEVEL_INFO, array( 'template_file' => $template_file, 'export_type' => $export_type )); } // Include template to generate feed content. // Template stores content in $GLOBALS['activetrail_feed_output'] instead of echoing. // Use output buffering to catch any accidental output. ob_start(); include $template_file; $ob_content = ob_get_clean(); // Get the generated content from global variable (preferred method). $feed_content = isset( $GLOBALS['activetrail_feed_output'] ) ? $GLOBALS['activetrail_feed_output'] : ''; // If global variable is empty but output buffer has content, use that (fallback). if ( empty( $feed_content ) && ! empty( $ob_content ) ) { $feed_content = $ob_content; } if ($logger) { $logger->product_feed('โœ… Feed generation completed successfully', ActiveTrail_Logger::LEVEL_INFO, array( 'export_type' => $export_type, 'content_length' => strlen($feed_content), 'content_preview' => substr($feed_content, 0, 500) . '...', 'content_empty' => empty($feed_content), 'starts_with_xml' => strpos($feed_content, ' strpos($feed_content, ' isset($GLOBALS['activetrail_feed_output']), 'from_buffer' => !empty($ob_content), 'first_100_chars' => substr($feed_content, 0, 100) )); } // Also log to logger for immediate debugging. if (function_exists('activetrail_logger')) { $logger = activetrail_logger(); $logger->product_feed('Feed generated with length: ' . strlen($feed_content), 'DEBUG'); $logger->product_feed('Content preview: ' . substr($feed_content, 0, 200), 'DEBUG'); } // Output the feed content. // Check if there was an error (stored in global variable). $is_error = isset( $GLOBALS['activetrail_feed_error'] ) && $GLOBALS['activetrail_feed_error']; if ( empty( $feed_content ) ) { // No content generated - output error. if ( $feed_format === 'json' ) { status_header( 500 ); wp_send_json_error( array( 'error' => 'Feed generation failed - no content generated' ) ); } else { header( 'Content-Type: application/xml; charset=utf-8' ); status_header( 500 ); echo 'Feed generation failed - no content generated'; wp_die(); } } // For JSON feeds: output JSON string directly with proper headers (feed is pre-encoded with specific flags). // For XML feeds: output XML directly with proper headers and exit. if ( $feed_format === 'json' ) { // JSON feed is pre-encoded by generate_facebook_json() with JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE. // Output directly to preserve formatting and encoding flags. if ( $is_error ) { status_header( 500 ); } header( 'Content-Type: application/json; charset=utf-8' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON feed content is generated server-side using wp_json_encode() and must be output as-is for external feed parsers. Escaping would break JSON structure. echo $feed_content; // Use exit instead of wp_die() for JSON feeds - wp_die() outputs HTML which breaks JSON structure. exit; } else { // XML feed - output directly with proper headers. // Headers are already set above, but ensure Content-Type is correct. if ( $is_error ) { status_header( 500 ); } header( 'Content-Type: application/xml; charset=utf-8' ); // Ensure feed content starts with XML declaration (safety check). if ( ! empty( $feed_content ) && strpos( $feed_content, 'product_feed( 'โš ๏ธ WARNING: Feed content does not start with XML declaration', ActiveTrail_Logger::LEVEL_WARNING, array( 'first_100_chars' => substr( $feed_content, 0, 100 ), 'content_length' => strlen( $feed_content ) ) ); } // If content doesn't start with XML, something is wrong - don't output it. wp_die( 'Error: Feed content is invalid', 'Feed Error', array( 'response' => 500 ) ); } // Clear any output that might have been sent before. if ( ob_get_level() > 0 ) { ob_clean(); } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- XML feed content is generated server-side and must be output as-is for external feed parsers. Escaping would break XML structure. echo $feed_content; // Use exit instead of wp_die() for XML feeds - wp_die() outputs HTML which breaks XML structure. exit; } } // Hook after WooCommerce registers its post types and taxonomies (priority 5) so wc_get_product() works correctly. add_action('init', 'activetrail_wc_handle_export_request', 10); /** * Alternative hook for export requests - even earlier */ function activetrail_wc_handle_export_request_early() { // Check if this is an export request. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External feed endpoint, authentication handled separately if (!isset($_GET['activetrail_export']) || !isset($_GET['key'])) { return; } // Force load WooCommerce early. if (!function_exists('wc_get_product')) { // Try to load WooCommerce. if (file_exists(WP_PLUGIN_DIR . '/woocommerce/woocommerce.php')) { include_once WP_PLUGIN_DIR . '/woocommerce/woocommerce.php'; } // Initialize WooCommerce if class exists. if (class_exists('WooCommerce') && !function_exists('wc_get_product')) { // Force initialize WooCommerce. // Note: $woocommerce is a core WooCommerce global variable, not our plugin's variable. if (!isset($GLOBALS['woocommerce']) || !$GLOBALS['woocommerce']) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- Core WooCommerce global variable $GLOBALS['woocommerce'] = new WooCommerce(); } WC(); } } } // Hook even earlier to ensure WooCommerce is loaded. add_action('plugins_loaded', 'activetrail_wc_handle_export_request_early', 5); /** * Add custom cron schedules * * Register cron schedules early, before any event scheduling */ function activetrail_wc_add_cron_schedules($schedules) { $schedules['every_minute'] = array( 'interval' => 60, // 1 minute in seconds // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch -- Text domain 'activetrail-for-woocommerce' is correct per WordPress review requirements 'display' => esc_html__('Every Minute', 'activetrail-for-woocommerce') ); $schedules['every_five_minutes'] = array( 'interval' => 300, // 5 minutes in seconds // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch -- Text domain 'activetrail-for-woocommerce' is correct per WordPress review requirements 'display' => esc_html__('Every 5 Minutes', 'activetrail-for-woocommerce') ); $schedules['every_fifteen_minutes'] = array( 'interval' => 900, // 15 minutes in seconds // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch -- Text domain 'activetrail-for-woocommerce' is correct per WordPress review requirements 'display' => esc_html__('Every 15 Minutes', 'activetrail-for-woocommerce') ); return $schedules; } // Use priority 5 to ensure schedules are registered before any events are scheduled. add_filter('cron_schedules', 'activetrail_wc_add_cron_schedules', 5); /** * Plugin activation hook */ function activetrail_wc_activate() { // Check if WooCommerce is active. if (!activetrail_wc_check_woocommerce()) { deactivate_plugins(plugin_basename(__FILE__)); wp_die( // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch -- Text domain 'activetrail-for-woocommerce' is correct per WordPress review requirements esc_html__('ActiveTrail for WooCommerce requires WooCommerce to be installed and active.', 'activetrail-for-woocommerce'), // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch -- Text domain 'activetrail-for-woocommerce' is correct per WordPress review requirements esc_html__('Plugin Activation Error', 'activetrail-for-woocommerce'), array('back_link' => true) ); } // Set default options. $default_options = array( 'api_key' => '', 'group_id' => '', 'order_statuses' => array( 'completed' ), 'enable_checkout_consent' => true, 'consent_text' => 'I agree to receive marketing emails', // Will be translated when displayed 'consent_checked_by_default' => false, 'consent_required' => false, 'enable_registration_consent' => false, 'sync_without_consent' => false, ); add_option( 'activetrail_wc_settings', $default_options ); // Create or update custom database tables and store DB schema version. activetrail_wc_create_tables(); update_option( 'activetrail_wc_db_version', ACTIVETRAIL_WC_VERSION ); // Flush rewrite rules so /activetrail-feed.xml resolves immediately after activation. add_rewrite_rule( '^activetrail-feed\.xml$', 'index.php?activetrail_feed=xml', 'top' ); add_rewrite_rule( '^activetrail-feed\.json$', 'index.php?activetrail_feed=json', 'top' ); flush_rewrite_rules(); } /** * Ensure database tables are up to date on plugin load. * * This runs on existing installations as well (not ืจืง ื‘ื”ืคืขืœื” ืจืืฉื•ื ื”), * ื•ืžืคืขื™ืœ ืฉื•ื‘ ืืช dbDelta ืื ื’ืจืกืช ื”ืกื›ืžื” ื”ืฉืชื ืชื” โ€“ ื›ืš ืžืชื•ื•ืกืคื•ืช ืขืžื•ื“ื•ืช ื—ื“ืฉื•ืช * ื›ืžื• sync_attempts, ื‘ืœื™ ืœืื‘ื“ ื ืชื•ื ื™ื, ื‘ื›ืœ ืกื‘ื™ื‘ื” ืฉื‘ื” ืžืชืงื™ื ื™ื ืื• ืžืขื“ื›ื ื™ื ืืช ื”ืชื•ืกืฃ. */ function activetrail_wc_maybe_update_tables() { $installed_version = get_option( 'activetrail_wc_db_version' ); if ( $installed_version === ACTIVETRAIL_WC_VERSION ) { return; } // (Re)create/upgrade tables โ€“ dbDelta ื™ื•ืกื™ืฃ ืจืง ืžื” ืฉื—ืกืจ. activetrail_wc_create_tables(); update_option( 'activetrail_wc_db_version', ACTIVETRAIL_WC_VERSION ); } // Run after plugins_loaded so ืฉืœึพ$wpdb ื™ืฉ ืืช ื›ืœ ื”ืžื™ื“ืข, ืื‘ืœ ืœืคื ื™ ืฉืื ื—ื ื• ืžืชื—ื™ืœื™ื ืœื”ืฉืชืžืฉ ื‘ื˜ื‘ืœืื•ืช. add_action( 'plugins_loaded', 'activetrail_wc_maybe_update_tables', 20 ); /** * One-time clear of old 2-arg debounced cart sync cron events. * New code schedules events with 3 args (session_id, snapshot_time, cart_data) so sync works without session. * Run once per site so old scheduled events are removed and only 3-arg events run. */ function activetrail_wc_clear_legacy_debounced_cron() { if ( get_option( 'activetrail_wc_debounced_cron_v2', '' ) === 'yes' ) { return; } wp_clear_scheduled_hook( 'activetrail_debounced_cart_sync' ); update_option( 'activetrail_wc_debounced_cron_v2', 'yes' ); } add_action( 'plugins_loaded', 'activetrail_wc_clear_legacy_debounced_cron', 21 ); /** * Schedule plugin cron events * * This function is called after cron schedules are registered * to ensure all events use valid schedules */ function activetrail_wc_schedule_events() { // Schedule abandoned cart sync if not already scheduled. if (!wp_next_scheduled('activetrail_sync_abandoned_carts')) { wp_schedule_event(time(), 'every_minute', 'activetrail_sync_abandoned_carts'); } // Schedule event queue processing if not already scheduled. if (!wp_next_scheduled('activetrail_process_event_queue')) { wp_schedule_event(time(), 'every_five_minutes', 'activetrail_process_event_queue'); } // Schedule expired coupons cleanup if not already scheduled. if (!wp_next_scheduled('activetrail_clean_expired_coupons')) { wp_schedule_event(time(), 'daily', 'activetrail_clean_expired_coupons'); } } // Schedule events after cron schedules are registered. add_action('init', 'activetrail_wc_schedule_events', 20); /** * Send disconnect API call to ActiveTrail * Helper function for deactivation and uninstall hooks */ function activetrail_wc_send_disconnect() { // Get API key from settings. require_once ACTIVETRAIL_WC_PLUGIN_DIR . 'includes/settings/class-settings.php'; $settings = ActiveTrail_Settings::get_all(); $api_key = isset( $settings['api_key'] ) ? $settings['api_key'] : ''; if ( empty( $api_key ) ) { // No API key, nothing to disconnect. if ( function_exists( 'activetrail_logger' ) ) { // Log explicitly so ืฉื ื•ื›ืœ ืœืจืื•ืช ื‘ืœื•ื’ ืœืžื” ืœื ื ืฉืœื— disconnect. activetrail_logger()->info( 'Skip store disconnect: no API key configured', 'SETTINGS' ); } return; } // Load API client. require_once ACTIVETRAIL_WC_PLUGIN_DIR . 'includes/api/class-activetrail-api-client.php'; $api_client = new ActiveTrail_API_Client( $api_key ); // Log ืœืคื ื™ ื”ืงืจื™ืื” ื‘ืคื•ืขืœ ื›ื“ื™ ืฉื ื“ืข ืฉโ€‘deactivate/uninstall ื ื™ืกื” ืœืฉืœื•ื— disconnect. if ( function_exists( 'activetrail_logger' ) ) { activetrail_logger()->info( 'Attempting store disconnect on plugin deactivation/uninstall', 'SETTINGS' ); } // Send disconnect call with short timeout (fail silently if it fails). $result = $api_client->disconnect_store(); // Log ืชื•ืฆืืช ื”ืงืจื™ืื” (ื‘ืœื™ ืœื—ืฉื•ืฃ ืคืจื˜ื™ื ืจื’ื™ืฉื™ื). if ( function_exists( 'activetrail_logger' ) ) { $success_flag = ( is_array( $result ) && isset( $result['success'] ) ) ? ( $result['success'] ? 'success' : 'failure' ) : 'unknown'; $extra_message = ( is_array( $result ) && ! empty( $result['message'] ) ) ? ' | message: ' . $result['message'] : ''; activetrail_logger()->info( 'Store disconnect finished with status: ' . $success_flag . $extra_message, 'SETTINGS' ); } // Result is logged internally, we don't need to do anything else here. // Failures are handled silently to avoid blocking admin UX. } /** * Plugin deactivation hook */ function activetrail_wc_deactivate() { global $wpdb; // Send disconnect API call. activetrail_wc_send_disconnect(); // Clear all scheduled events. wp_clear_scheduled_hook('activetrail_sync_abandoned_carts'); wp_clear_scheduled_hook('activetrail_process_event_queue'); wp_clear_scheduled_hook('activetrail_clean_expired_coupons'); wp_clear_scheduled_hook('activetrail_debounced_cart_sync'); // Delete all plugin options. $options = array( 'activetrail_wc_settings', 'activetrail_api_key', 'activetrail_group_id', 'activetrail_store_status', 'activetrail_language', 'activetrail_order_statuses', 'activetrail_enable_checkout_consent', 'activetrail_consent_text', 'activetrail_consent_checked_by_default', 'activetrail_consent_required', 'activetrail_sync_without_consent', 'activetrail_enable_cart_tracking', 'activetrail_enable_product_feed', 'activetrail_feed_title', 'activetrail_feed_description', 'activetrail_debug_mode', 'activetrail_historical_sync_status', 'activetrail_status_mapping', ); foreach ( $options as $option ) { delete_option( $option ); } // Delete any remaining options with the activetrail prefix. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE 'activetrail_%'" ); // Remove transients. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_activetrail_%'" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_activetrail_%'" ); } /** * Create custom database tables */ function activetrail_wc_create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); // Cart tracking table (enhanced). $cart_table = $wpdb->prefix . 'activetrail_cart_tracking'; $cart_sql = "CREATE TABLE $cart_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, session_id varchar(255) NOT NULL, user_id bigint(20) DEFAULT NULL, cart_data longtext NOT NULL, email varchar(100) DEFAULT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, synced tinyint(1) DEFAULT 0, sync_attempts int(11) DEFAULT 0, last_sync_attempt datetime DEFAULT NULL, abandonment_sent tinyint(1) DEFAULT 0, recovery_email_sent tinyint(1) DEFAULT 0, PRIMARY KEY (id), KEY session_id (session_id), KEY user_id (user_id), KEY email (email), KEY synced (synced), KEY sync_attempts (sync_attempts), KEY abandonment_sent (abandonment_sent) ) $charset_collate;"; // Events tracking table. $events_table = $wpdb->prefix . 'activetrail_events'; $events_sql = "CREATE TABLE $events_table ( id bigint(20) NOT NULL AUTO_INCREMENT, event_type varchar(50) NOT NULL, customer_email varchar(100) DEFAULT NULL, customer_id bigint(20) DEFAULT NULL, session_id varchar(255) DEFAULT NULL, product_id bigint(20) DEFAULT NULL, order_id bigint(20) DEFAULT NULL, event_data longtext DEFAULT NULL, event_value decimal(10,2) DEFAULT NULL, currency varchar(3) DEFAULT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, synced tinyint(1) DEFAULT 0, sync_attempts int(11) DEFAULT 0, last_sync_attempt datetime DEFAULT NULL, PRIMARY KEY (id), KEY event_type (event_type), KEY customer_email (customer_email), KEY customer_id (customer_id), KEY product_id (product_id), KEY synced (synced), KEY created_at (created_at) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($cart_sql); dbDelta($events_sql); } // Register activation and deactivation hooks. register_activation_hook(__FILE__, 'activetrail_wc_activate'); register_deactivation_hook(__FILE__, 'activetrail_wc_deactivate'); /** * Add plugin action links */ function activetrail_wc_plugin_action_links($links) { // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch -- Text domain 'activetrail-for-woocommerce' is correct per WordPress review requirements $settings_link = '' . esc_html__('Settings', 'activetrail-for-woocommerce') . ''; array_unshift($links, $settings_link); return $links; } add_filter('plugin_action_links_' . plugin_basename(__FILE__), 'activetrail_wc_plugin_action_links');