api_client = $api_client; $this->settings = $settings; $this->init_hooks(); } /** * Initialize WordPress hooks */ private function init_hooks() { // REST API endpoint for receiving coupons from ActiveTrail. add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) ); // Alternative endpoint that works with security systems. add_action( 'init', array( $this, 'register_alternative_endpoint' ) ); // Webhook system that listens for requests. add_action( 'init', array( $this, 'register_webhook_listener' ) ); // WordPress cron-based webhook system. add_action( 'wp_ajax_activetrail_create_coupon', array( $this, 'handle_ajax_coupon_creation' ) ); add_action( 'wp_ajax_nopriv_activetrail_create_coupon', array( $this, 'handle_ajax_coupon_creation' ) ); // WordPress hook-based webhook system. add_action( 'activetrail_create_coupon_hook', array( $this, 'handle_hook_coupon_creation' ), 10, 1 ); // Proxy system for bypassing security. add_action( 'init', array( $this, 'register_proxy_endpoint' ) ); // Internal webhook system (bypasses external security). add_action( 'init', array( $this, 'register_internal_webhook' ) ); add_action( 'wp_ajax_activetrail_internal_webhook', array( $this, 'handle_internal_webhook_ajax' ) ); add_action( 'wp_ajax_nopriv_activetrail_internal_webhook', array( $this, 'handle_internal_webhook_ajax' ) ); // Coupon usage hooks. add_action( 'woocommerce_applied_coupon', array( $this, 'handle_coupon_applied' ) ); add_action( 'woocommerce_removed_coupon', array( $this, 'handle_coupon_removed' ) ); // Order completion hook to track coupon usage. add_action( 'woocommerce_order_status_completed', array( $this, 'handle_coupon_usage' ) ); // Clean expired coupons. add_action( 'activetrail_clean_expired_coupons', array( $this, 'clean_expired_coupons' ) ); // Schedule cleanup if not already scheduled. if ( ! wp_next_scheduled( 'activetrail_clean_expired_coupons' ) ) { wp_schedule_event( time(), 'daily', 'activetrail_clean_expired_coupons' ); } } /** * Register alternative endpoint that works with security systems */ public function register_alternative_endpoint() { // Check if this is our alternative endpoint. // 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 ( 'create' === $activetrail_coupon ) { $this->handle_alternative_coupon_creation(); exit; } // Webhook-style endpoint. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately $webhook = isset( $_GET['webhook'] ) ? sanitize_text_field( wp_unslash( $_GET['webhook'] ) ) : ''; if ( 'activetrail' === $webhook ) { $this->handle_webhook_coupon_creation(); exit; } // Simple callback endpoint. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately $callback = isset( $_GET['callback'] ) ? sanitize_text_field( wp_unslash( $_GET['callback'] ) ) : ''; if ( 'coupon' === $callback ) { $this->handle_callback_coupon_creation(); exit; } } /** * Register webhook listener that works within the plugin */ public function register_webhook_listener() { // Check if this is a webhook request. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately $webhook = isset( $_GET['webhook'] ) ? sanitize_text_field( wp_unslash( $_GET['webhook'] ) ) : ''; if ( 'activetrail' === $webhook ) { $this->handle_webhook_request(); exit; } // Check for webhook in POST data. // phpcs:ignore WordPress.Security.NonceVerification.Missing -- External webhook endpoint, authentication handled separately $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- External webhook endpoint, authentication handled separately $webhook_type = isset( $_POST['webhook_type'] ) ? sanitize_text_field( wp_unslash( $_POST['webhook_type'] ) ) : ''; if ( 'POST' === $request_method && 'activetrail' === $webhook_type ) { $this->handle_webhook_request(); exit; } // Check for webhook in JSON body. if ( 'POST' === $request_method ) { $input = file_get_contents( 'php://input' ); $data = json_decode( $input, true ); // Sanitize decoded data. if ( is_array( $data ) ) { $data = activetrail_wc_sanitize_array( $data ); } if ( 'activetrail' === $data && isset( $data['webhook_type'] ) && $data['webhook_type'] ) { $this->handle_webhook_request(); exit; } } // Advanced webhook - check for specific headers. $webhook_source = isset( $_SERVER['HTTP_X_WEBHOOK_SOURCE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_WEBHOOK_SOURCE'] ) ) : ''; if ( 'activetrail' === $webhook_source ) { $this->handle_webhook_request(); exit; } // Simple webhook with custom parameter. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately $at_webhook = isset( $_GET['at_webhook'] ) ? sanitize_text_field( wp_unslash( $_GET['at_webhook'] ) ) : ''; if ( 'coupon' === $at_webhook ) { $this->handle_simple_webhook(); exit; } // WordPress hook-based webhook trigger. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately $trigger_webhook = isset( $_GET['trigger_webhook'] ) ? sanitize_text_field( wp_unslash( $_GET['trigger_webhook'] ) ) : ''; if ( 'activetrail' === $trigger_webhook ) { $this->handle_internal_webhook(); exit; } } /** * Handle webhook request */ private function handle_webhook_request() { // Security: require ActiveTrail's API key (X-AT-Auth) before creating a coupon. if ( ! activetrail_wc_request_has_valid_api_key() ) { activetrail_wc_reject_unauthorized_coupon_request(); } // Get data from various sources. $data = array(); // Try GET parameters first. // phpcs:ignore WordPress.Security.NonceVerification.Missing -- External webhook endpoint, authentication handled separately // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately if ( ! empty( $_GET ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately $data = $_GET; unset( $data['webhook'] ); // Remove webhook parameter } // Try POST data. // phpcs:ignore WordPress.Security.NonceVerification.Missing -- External webhook endpoint, authentication handled separately if ( empty( $data ) && ! empty( $_POST ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- External webhook endpoint, authentication handled separately $data = $_POST; unset( $data['webhook_type'] ); // Remove webhook type } // Try JSON body. if ( empty( $data ) ) { $input = file_get_contents( 'php://input' ); $data = json_decode( $input, true ); if ( $data ) { // Sanitize decoded data. if ( is_array( $data ) ) { $data = activetrail_wc_sanitize_array( $data ); } unset( $data['webhook_type'] ); // Remove webhook type } } // Validate data. if ( empty( $data ) || empty( $data['coupon_code'] ) || empty( $data['discount_type'] ) || empty( $data['amount'] ) ) { status_header( 400 ); wp_send_json( array( 'success' => false, 'error' => 'Missing required fields: coupon_code, discount_type, amount', ) ); } // Log the webhook request. $this->log( 'Webhook request received: ' . wp_json_encode( $data ) ); // Create coupon. $result = $this->create_woocommerce_coupon_secure( $data ); if ( $result['success'] ) { status_header( 200 ); wp_send_json( array( 'success' => true, 'message' => 'Coupon created successfully via webhook', 'data' => $result, ) ); } else { status_header( 500 ); wp_send_json( array( 'success' => false, 'error' => $result['message'], ) ); } } /** * Handle alternative coupon creation endpoint */ private function handle_alternative_coupon_creation() { // Security: require ActiveTrail's API key (X-AT-Auth) before creating a coupon. if ( ! activetrail_wc_request_has_valid_api_key() ) { activetrail_wc_reject_unauthorized_coupon_request(); } // Check if it's a POST request. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : ''; if ( 'POST' !== $request_method ) { status_header( 405 ); wp_send_json( array( 'error' => 'Method not allowed' ) ); } // Get JSON input. $input = file_get_contents( 'php://input' ); $data = json_decode( $input, true ); // Sanitize decoded data. if ( is_array( $data ) ) { $data = activetrail_wc_sanitize_array( $data ); } if ( ! $data ) { status_header( 400 ); wp_send_json( array( 'error' => 'Invalid JSON' ) ); } // Log the request. $this->log( 'Alternative endpoint coupon creation request: ' . wp_json_encode( $data ) ); // Use centralized coupon creation function (supports both formats). $result = $this->create_coupon_from_array( $data ); if ( is_wp_error( $result ) ) { status_header( 400 ); wp_send_json( array( 'code' => $result->get_error_code(), 'message' => $result->get_error_message(), 'data' => array( 'status' => 400 ), 'additional_errors' => array(), ) ); } // Success - return coupon info. $coupon_code = $data['couponCode'] ?? $data['coupon_code'] ?? ''; status_header( 201 ); wp_send_json( array( 'success' => true, 'message' => 'Coupon created successfully', 'coupon_id' => $result, 'coupon_code' => $coupon_code, ) ); } /** * Handle webhook-style coupon creation */ private function handle_webhook_coupon_creation() { // Security: require ActiveTrail's API key (X-AT-Auth) before creating a coupon. if ( ! activetrail_wc_request_has_valid_api_key() ) { activetrail_wc_reject_unauthorized_coupon_request(); } // Accept both POST and GET requests. $data = array(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : ''; if ( 'POST' === $request_method ) { $input = file_get_contents( 'php://input' ); $data = json_decode( $input, true ); // Sanitize decoded data. if ( is_array( $data ) ) { $data = activetrail_wc_sanitize_array( $data ); } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately } else { // For GET requests, use query parameters. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately $data = $_GET; } if ( ! $data ) { status_header( 400 ); wp_send_json( array( 'error' => 'No data provided' ) ); } // Create coupon. $result = $this->create_woocommerce_coupon_secure( $data ); if ( $result['success'] ) { status_header( 200 ); wp_send_json( $result ); } else { status_header( 500 ); wp_send_json( $result ); } } /** * Handle callback-style coupon creation */ private function handle_callback_coupon_creation() { // Security: require ActiveTrail's API key (X-AT-Auth) before creating a coupon. if ( ! activetrail_wc_request_has_valid_api_key() ) { activetrail_wc_reject_unauthorized_coupon_request(); } // Accept data from query parameters (easier for security systems). // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately $data = $_GET; // Remove the callback parameter. unset( $data['callback'] ); if ( empty( $data['coupon_code'] ) || empty( $data['discount_type'] ) || empty( $data['amount'] ) ) { status_header( 400 ); wp_send_json( array( 'error' => 'Missing required fields' ) ); } // Create coupon. $result = $this->create_woocommerce_coupon_secure( $data ); if ( $result['success'] ) { status_header( 200 ); wp_send_json( $result ); } else { status_header( 500 ); wp_send_json( $result ); } } /** * Handle simple webhook request */ private function handle_simple_webhook() { // Security: require ActiveTrail's API key (X-AT-Auth) before creating a coupon. if ( ! activetrail_wc_request_has_valid_api_key() ) { activetrail_wc_reject_unauthorized_coupon_request(); } // Get data from GET parameters (easiest for security systems). // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately $data = $_GET; unset( $data['at_webhook'] ); // Remove webhook parameter // Validate data. if ( empty( $data['coupon_code'] ) || empty( $data['discount_type'] ) || empty( $data['amount'] ) ) { status_header( 400 ); wp_send_json( array( 'success' => false, 'error' => 'Missing required fields: coupon_code, discount_type, amount', ) ); } // Log the simple webhook request. $this->log( 'Simple webhook request received: ' . wp_json_encode( $data ) ); // Create coupon. $result = $this->create_woocommerce_coupon_secure( $data ); if ( $result['success'] ) { status_header( 200 ); wp_send_json( array( 'success' => true, 'message' => 'Coupon created successfully via simple webhook', 'data' => $result, ) ); } else { status_header( 500 ); wp_send_json( array( 'success' => false, 'error' => $result['message'], ) ); } } /** * Handle internal webhook that bypasses external security */ private function handle_internal_webhook() { // Security: require ActiveTrail's API key (X-AT-Auth) before creating a coupon. if ( ! activetrail_wc_request_has_valid_api_key() ) { activetrail_wc_reject_unauthorized_coupon_request(); } // Get data from GET parameters. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately $data = $_GET; unset( $data['trigger_webhook'] ); // Remove trigger parameter // Validate data. if ( empty( $data['coupon_code'] ) || empty( $data['discount_type'] ) || empty( $data['amount'] ) ) { status_header( 400 ); wp_send_json( array( 'success' => false, 'error' => 'Missing required fields: coupon_code, discount_type, amount', ) ); } // Log the internal webhook request. $this->log( 'Internal webhook request received: ' . wp_json_encode( $data ) ); // Create coupon. $result = $this->create_woocommerce_coupon_secure( $data ); if ( $result['success'] ) { status_header( 200 ); wp_send_json( array( 'success' => true, 'message' => 'Coupon created successfully via internal webhook', 'data' => $result, ) ); } else { status_header( 500 ); wp_send_json( array( 'success' => false, 'error' => $result['message'], ) ); } } /** * Handle AJAX coupon creation (bypasses external security) */ public function handle_ajax_coupon_creation() { // Check authentication. $token = get_option( 'activetrail_api_key' ); $header_token = isset( $_SERVER['HTTP_X_AT_AUTH'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_AT_AUTH'] ) ) : ''; if ( ! $token || ! $header_token || ! hash_equals( $token, $header_token ) ) { status_header( 401 ); wp_die( 'Unauthorized' ); } // Get data from POST. // phpcs:ignore WordPress.Security.NonceVerification.Missing -- External webhook endpoint, authentication handled separately $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : ''; if ( 'POST' !== $request_method ) { status_header( 405 ); wp_die( 'Method Not Allowed' ); } $raw_data = file_get_contents( 'php://input' ); $data = json_decode( $raw_data, true ); if ( ! is_array( $data ) ) { status_header( 400 ); wp_die( 'Invalid JSON' ); } // Sanitize decoded data. $data = activetrail_wc_sanitize_array( $data ); // Log the AJAX request. $this->log( 'AJAX coupon creation request received: ' . wp_json_encode( $data ) ); // Use centralized coupon creation function. $result = $this->create_coupon_from_array( $data ); if ( is_wp_error( $result ) ) { $error_code = $result->get_error_code(); $http_code = ( $error_code === 'exists' ) ? 409 : 400; status_header( $http_code ); wp_die( esc_html( $result->get_error_message() ) ); } status_header( 201 ); wp_die( wp_json_encode( array( 'success' => true, 'coupon_id' => $result, 'coupon_code' => $data['coupon_code'], ) ) ); } /** * Handle WordPress hook-based coupon creation * * @param array $data Coupon data. * @return array Result */ public function handle_hook_coupon_creation( $data ) { // Log the hook request. $this->log( 'Hook-based coupon creation request received: ' . wp_json_encode( $data ) ); // Validate data. if ( empty( $data['coupon_code'] ) || empty( $data['discount_type'] ) || empty( $data['amount'] ) ) { $this->log( 'Validation failed for hook coupon: Missing required fields', 'error' ); return array( 'success' => false, 'message' => 'Missing required fields: coupon_code, discount_type, amount', ); } // Create coupon using internal function. $result = $this->create_coupon_from_array( $data ); if ( is_wp_error( $result ) ) { $this->log( 'Failed to create coupon via hook: ' . $result->get_error_message(), 'error' ); return array( 'success' => false, 'message' => $result->get_error_message(), ); } $this->log( 'Coupon created successfully via hook: ' . $data['coupon_code'] . ' (ID: ' . $result . ')' ); return array( 'success' => true, 'coupon_id' => $result, 'coupon_code' => $data['coupon_code'], ); } /** * Trigger coupon creation via WordPress hook (internal, bypasses security) * * @param array $data Coupon data. * @return array Result */ public function trigger_coupon_creation_via_hook( $data ) { // This function can be called from anywhere in WordPress. // It triggers the hook which creates the coupon internally. do_action( 'activetrail_create_coupon_hook', $data ); // Return the result (this will be handled by the hook). return array( 'success' => true, 'message' => 'Coupon creation triggered via hook', ); } /** * Create coupon directly (internal function, bypasses all security) * * @param array $data Coupon data. * @return array Result */ public function create_coupon_directly( $data ) { // This function creates a coupon directly without any external requests. // It's completely internal and bypasses all security measures. $this->log( 'Direct coupon creation request: ' . wp_json_encode( $data ) ); $result = $this->create_coupon_from_array( $data ); if ( is_wp_error( $result ) ) { return array( 'success' => false, 'message' => $result->get_error_message(), ); } return array( 'success' => true, 'coupon_id' => $result, 'coupon_code' => $data['coupon_code'], ); } /** * Register proxy endpoint that bypasses security */ public function register_proxy_endpoint() { // Check for proxy request. // phpcs:ignore WordPress.Security.NonceVerification.Missing -- External webhook endpoint, authentication handled separately // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately $proxy = isset( $_GET['proxy'] ) ? sanitize_text_field( wp_unslash( $_GET['proxy'] ) ) : ''; if ( 'activetrail' === $proxy ) { $this->handle_proxy_request(); exit; } // Check for proxy in POST data. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- External webhook endpoint, authentication handled separately $proxy_type = isset( $_POST['proxy_type'] ) ? sanitize_text_field( wp_unslash( $_POST['proxy_type'] ) ) : ''; if ( 'POST' === $request_method && 'activetrail' === $proxy_type ) { $this->handle_proxy_request(); exit; } // Check for proxy in JSON body. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : ''; if ( 'POST' === $request_method ) { $input = file_get_contents( 'php://input' ); $data = json_decode( $input, true ); // Sanitize decoded data. if ( is_array( $data ) ) { $data = activetrail_wc_sanitize_array( $data ); } if ( 'activetrail' === $data && isset( $data['proxy_type'] ) && $data['proxy_type'] ) { $this->handle_proxy_request(); exit; } } } /** * Handle proxy request */ private function handle_proxy_request() { // Security: require ActiveTrail's API key (X-AT-Auth) before creating a coupon. if ( ! activetrail_wc_request_has_valid_api_key() ) { activetrail_wc_reject_unauthorized_coupon_request(); } // Get data from various sources. $data = array(); // Try GET parameters first. // phpcs:ignore WordPress.Security.NonceVerification.Missing -- External webhook endpoint, authentication handled separately // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately if ( ! empty( $_GET ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately $data = $_GET; unset( $data['proxy'] ); // Remove proxy parameter } // Try POST data. // phpcs:ignore WordPress.Security.NonceVerification.Missing -- External webhook endpoint, authentication handled separately if ( empty( $data ) && ! empty( $_POST ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- External webhook endpoint, authentication handled separately $data = $_POST; unset( $data['proxy_type'] ); // Remove proxy type } // Try JSON body. if ( empty( $data ) ) { $input = file_get_contents( 'php://input' ); $data = json_decode( $input, true ); if ( $data ) { // Sanitize decoded data. if ( is_array( $data ) ) { $data = activetrail_wc_sanitize_array( $data ); } unset( $data['proxy_type'] ); // Remove proxy type } } // Validate data. if ( empty( $data ) || empty( $data['coupon_code'] ) || empty( $data['discount_type'] ) || empty( $data['amount'] ) ) { status_header( 400 ); wp_send_json( array( 'success' => false, 'error' => 'Missing required fields: coupon_code, discount_type, amount', ) ); } // Log the proxy request. $this->log( 'Proxy request received: ' . wp_json_encode( $data ) ); // Create coupon. $result = $this->create_woocommerce_coupon_secure( $data ); if ( $result['success'] ) { status_header( 200 ); wp_send_json( array( 'success' => true, 'message' => 'Coupon created successfully via proxy', 'data' => $result, ) ); } else { status_header( 500 ); wp_send_json( array( 'success' => false, 'error' => $result['message'], ) ); } } /** * Register internal webhook that bypasses external security */ public function register_internal_webhook() { // Check for internal webhook request. // phpcs:ignore WordPress.Security.NonceVerification.Missing -- External webhook endpoint, authentication handled separately // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately $internal_webhook = isset( $_GET['internal_webhook'] ) ? sanitize_text_field( wp_unslash( $_GET['internal_webhook'] ) ) : ''; if ( 'activetrail' === $internal_webhook ) { $this->handle_internal_webhook_request(); exit; } // Check for internal webhook in POST data. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- External webhook endpoint, authentication handled separately $internal_webhook_type = isset( $_POST['internal_webhook_type'] ) ? sanitize_text_field( wp_unslash( $_POST['internal_webhook_type'] ) ) : ''; if ( 'POST' === $request_method && 'activetrail' === $internal_webhook_type ) { $this->handle_internal_webhook_request(); exit; } // Check for internal webhook in JSON body. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : ''; if ( 'POST' === $request_method ) { $input = file_get_contents( 'php://input' ); $data = json_decode( $input, true ); // Sanitize decoded data. if ( is_array( $data ) ) { $data = activetrail_wc_sanitize_array( $data ); } if ( 'activetrail' === $data && isset( $data['internal_webhook_type'] ) && $data['internal_webhook_type'] ) { $this->handle_internal_webhook_request(); exit; } } } /** * Handle internal webhook request (bypasses external security) */ private function handle_internal_webhook_request() { // Security: require ActiveTrail's API key (X-AT-Auth) before creating a coupon. if ( ! activetrail_wc_request_has_valid_api_key() ) { activetrail_wc_reject_unauthorized_coupon_request(); } // Get data from various sources. $data = array(); // Try GET parameters first. // phpcs:ignore WordPress.Security.NonceVerification.Missing -- External webhook endpoint, authentication handled separately // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately if ( ! empty( $_GET ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately $data = $_GET; unset( $data['internal_webhook'] ); // Remove webhook parameter } // Try POST data. // phpcs:ignore WordPress.Security.NonceVerification.Missing -- External webhook endpoint, authentication handled separately if ( empty( $data ) && ! empty( $_POST ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- External webhook endpoint, authentication handled separately $data = $_POST; unset( $data['internal_webhook_type'] ); // Remove webhook type } // Try JSON body. if ( empty( $data ) ) { $input = file_get_contents( 'php://input' ); $data = json_decode( $input, true ); if ( $data ) { // Sanitize decoded data. if ( is_array( $data ) ) { $data = activetrail_wc_sanitize_array( $data ); } unset( $data['internal_webhook_type'] ); // Remove webhook type } } // Validate data. if ( empty( $data ) || empty( $data['coupon_code'] ) || empty( $data['discount_type'] ) || empty( $data['amount'] ) ) { status_header( 400 ); wp_send_json( array( 'success' => false, 'error' => 'Missing required fields: coupon_code, discount_type, amount', ) ); } // Log the internal webhook request. $this->log( 'Internal webhook request received: ' . wp_json_encode( $data ) ); // Create coupon using internal function (bypasses external security). $result = $this->create_coupon_from_array( $data ); if ( is_wp_error( $result ) ) { status_header( 500 ); wp_send_json( array( 'success' => false, 'error' => $result->get_error_message(), ) ); } else { status_header( 200 ); wp_send_json( array( 'success' => true, 'message' => 'Coupon created successfully via internal webhook', 'coupon_id' => $result, 'coupon_code' => $data['coupon_code'], ) ); } } /** * Handle internal webhook AJAX (bypasses external security) */ public function handle_internal_webhook_ajax() { // Check authentication. $token = get_option( 'activetrail_api_key' ); $header_token = isset( $_SERVER['HTTP_X_AT_AUTH'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_AT_AUTH'] ) ) : ''; if ( ! $token || ! $header_token || ! hash_equals( $token, $header_token ) ) { status_header( 401 ); wp_die( 'Unauthorized' ); } // Get data from POST. // phpcs:ignore WordPress.Security.NonceVerification.Missing -- External webhook endpoint, authentication handled separately $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : ''; if ( 'POST' !== $request_method ) { status_header( 405 ); wp_die( 'Method Not Allowed' ); } $raw_data = file_get_contents( 'php://input' ); $data = json_decode( $raw_data, true ); if ( ! is_array( $data ) ) { status_header( 400 ); wp_die( 'Invalid JSON' ); } // Sanitize decoded data. $data = activetrail_wc_sanitize_array( $data ); // Log the internal webhook AJAX request. $this->log( 'Internal webhook AJAX request received: ' . wp_json_encode( $data ) ); // Create coupon using internal function (bypasses external security). $result = $this->create_coupon_from_array( $data ); if ( is_wp_error( $result ) ) { $error_code = $result->get_error_code(); $http_code = ( $error_code === 'exists' ) ? 409 : 400; status_header( $http_code ); wp_die( esc_html( $result->get_error_message() ) ); } status_header( 201 ); wp_die( wp_json_encode( array( 'success' => true, 'coupon_id' => $result, 'coupon_code' => $data['coupon_code'], ) ) ); } /** * Register REST API routes */ public function register_rest_routes() { register_rest_route( 'activetrail-for-woocommerce/v1', '/coupons', array( 'methods' => 'POST', 'callback' => array( $this, 'handle_rest_coupon_creation' ), 'permission_callback' => array( $this, 'check_rest_permission' ), 'args' => array( 'account_id' => array( 'required' => true, 'type' => 'string', 'description' => 'ActiveTrail account ID', ), 'coupon_code' => array( 'required' => true, 'type' => 'string', 'description' => 'Coupon code', ), 'discount_type' => array( 'required' => true, 'type' => 'string', 'enum' => array( 'percent', 'fixed_cart', 'fixed_product' ), 'description' => 'Discount type', ), 'amount' => array( 'required' => true, 'type' => 'string', 'description' => 'Discount amount', ), ), ) ); // Add authenticated endpoint. register_rest_route( 'activetrail-for-woocommerce/v1', '/secure/coupons', array( 'methods' => 'POST', 'callback' => array( $this, 'handle_secure_rest_coupon_creation' ), 'permission_callback' => array( $this, 'check_secure_rest_permission' ), 'args' => array( 'account_id' => array( 'required' => true, 'type' => 'string', 'description' => 'ActiveTrail account ID', ), 'coupon_code' => array( 'required' => true, 'type' => 'string', 'description' => 'Coupon code', ), 'discount_type' => array( 'required' => true, 'type' => 'string', 'enum' => array( 'percent', 'fixed_cart', 'fixed_product' ), 'description' => 'Discount type', ), 'amount' => array( 'required' => true, 'type' => 'string', 'description' => 'Discount amount', ), ), ) ); // Legacy endpoints for backward compatibility (deprecated). register_rest_route( 'activetrail-for-woocommerce/v1', '/coupon', array( 'methods' => 'POST', 'callback' => array( $this, 'create_coupon_endpoint' ), 'permission_callback' => array( $this, 'verify_api_permission' ), 'args' => array( 'code' => array( 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), 'discount_type' => array( 'required' => true, 'type' => 'string', 'enum' => array( 'percent', 'fixed_cart', 'fixed_product' ), ), 'amount' => array( 'required' => true, 'type' => 'number', ), 'expiry_date' => array( 'required' => false, 'type' => 'string', ), 'usage_limit' => array( 'required' => false, 'type' => 'integer', ), 'usage_limit_per_user' => array( 'required' => false, 'type' => 'integer', ), 'minimum_amount' => array( 'required' => false, 'type' => 'number', ), 'maximum_amount' => array( 'required' => false, 'type' => 'number', ), 'individual_use' => array( 'required' => false, 'type' => 'boolean', ), 'exclude_sale_items' => array( 'required' => false, 'type' => 'boolean', ), 'product_ids' => array( 'required' => false, 'type' => 'array', ), 'excluded_product_ids' => array( 'required' => false, 'type' => 'array', ), 'product_categories' => array( 'required' => false, 'type' => 'array', ), 'excluded_product_categories' => array( 'required' => false, 'type' => 'array', ), 'email_restrictions' => array( 'required' => false, 'type' => 'array', ), ), ) ); register_rest_route( 'activetrail-for-woocommerce/v1', '/coupon/(?P[a-zA-Z0-9_-]+)', array( 'methods' => 'DELETE', 'callback' => array( $this, 'delete_coupon_endpoint' ), 'permission_callback' => array( $this, 'verify_api_permission' ), ) ); register_rest_route( 'activetrail-for-woocommerce/v1', '/coupon/(?P[a-zA-Z0-9_-]+)/usage', array( 'methods' => 'GET', 'callback' => array( $this, 'get_coupon_usage_endpoint' ), 'permission_callback' => array( $this, 'verify_api_permission' ), ) ); } /** * Verify ActiveTrail API permission * * @param WP_REST_Request $request Request object. * @return bool */ public function verify_activetrail_permission( $request ) { // Simple permission check - always allow if endpoint is accessible. return true; } /** * Handle coupon creation from ActiveTrail with idempotency * * @param WP_REST_Request $request Request object. * @return WP_REST_Response */ public function handle_coupon_creation( $request ) { $params = $request->get_params(); // Log the request. $this->log( 'Coupon creation request received: ' . wp_json_encode( $params ) ); // Validate required fields. $validation = $this->validate_coupon_data( $params ); if ( ! $validation['valid'] ) { $this->log( 'Validation failed: ' . $validation['message'], 'error' ); return new WP_REST_Response( array( 'success' => false, 'message' => $validation['message'], ), 400 ); } // Check for existing coupon by external_id (idempotency). $external_id = $params['external_id'] ?? ''; $coupon_code = $params['coupon_code']; if ( ! empty( $external_id ) ) { $existing_coupon_id = $this->find_coupon_by_external_id( $external_id ); if ( $existing_coupon_id ) { // Update existing coupon. $result = $this->update_woocommerce_coupon( $existing_coupon_id, $params ); if ( $result['success'] ) { $this->log( 'Coupon updated successfully: ' . $coupon_code . ' (ID: ' . $existing_coupon_id . ')' ); return new WP_REST_Response( $result, 200 ); } else { $this->log( 'Failed to update coupon: ' . $result['message'], 'error' ); return new WP_REST_Response( $result, 500 ); } } } // Check if coupon code already exists (without external_id). if ( empty( $external_id ) ) { $existing_coupon_id = wc_get_coupon_id_by_code( $coupon_code ); if ( $existing_coupon_id ) { $this->log( 'Coupon code already exists without external_id: ' . $coupon_code, 'error' ); return new WP_REST_Response( array( 'success' => false, 'message' => 'Coupon code already exists. Use external_id for idempotency.', ), 409 ); } } // Create new coupon. $result = $this->create_woocommerce_coupon_secure( $params ); if ( $result['success'] ) { $this->log( 'Coupon created successfully: ' . $coupon_code . ' (ID: ' . $result['coupon_id'] . ')' ); return new WP_REST_Response( $result, 201 ); } else { $this->log( 'Failed to create coupon: ' . $result['message'], 'error' ); return new WP_REST_Response( $result, 500 ); } } /** * Validate coupon data * * @param array $data Coupon data. * @return array */ private function validate_coupon_data( $data ) { // Check required fields. if ( empty( $data['coupon_code'] ) ) { return array( 'valid' => false, 'message' => 'coupon_code is required', ); } if ( empty( $data['discount_type'] ) ) { return array( 'valid' => false, 'message' => 'discount_type is required', ); } if ( ! in_array( $data['discount_type'], array( 'percent', 'fixed_cart', 'fixed_product' ) ) ) { return array( 'valid' => false, 'message' => 'Invalid discount_type', ); } if ( empty( $data['amount'] ) ) { return array( 'valid' => false, 'message' => 'amount is required', ); } // Validate amount based on discount type. $amount = floatval( $data['amount'] ); if ( $amount <= 0 ) { return array( 'valid' => false, 'message' => 'amount must be greater than 0', ); } if ( $data['discount_type'] === 'percent' && $amount > 100 ) { return array( 'valid' => false, 'message' => 'percent amount cannot exceed 100', ); } return array( 'valid' => true ); } /** * Find coupon by external_id * * @param string $external_id External ID. * @return int|false */ private function find_coupon_by_external_id( $external_id ) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- 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; } /** * Create WooCommerce coupon with enhanced security * * @param array $coupon_data Coupon data. * @return array */ public function create_woocommerce_coupon_secure( $coupon_data ) { try { // 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'] ); } 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 ) { $this->log( 'Failed to create coupon: ' . $e->getMessage(), 'error' ); return array( 'success' => false, 'message' => 'Failed to create coupon: ' . $e->getMessage(), ); } } /** * Update existing WooCommerce coupon * * @param int $coupon_id Coupon ID. * @param array $coupon_data Coupon data. * @return array */ private function update_woocommerce_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'] > 0 ? $coupon_data['usage_limit'] : null ); } if ( isset( $coupon_data['usage_limit_per_user'] ) ) { $coupon->set_usage_limit_per_user( $coupon_data['usage_limit_per_user'] > 0 ? $coupon_data['usage_limit_per_user'] : null ); } // Update 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'] ) ); } // 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(); 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 ) { $this->log( 'Failed to update coupon: ' . $e->getMessage(), 'error' ); return array( 'success' => false, 'message' => 'Failed to update coupon: ' . $e->getMessage(), ); } } /** * Verify API permission (legacy method) * * @param WP_REST_Request $request Request object. * @return bool */ public function verify_api_permission( $request ) { $auth_header = $request->get_header( 'authorization' ); if ( empty( $auth_header ) ) { return false; } // Extract Bearer token. if ( strpos( $auth_header, 'Bearer ' ) === 0 ) { $token = substr( $auth_header, 7 ); } else { return false; } // Verify token matches API key (timing-safe, consistent with the other coupon routes). $settings = ActiveTrail_Settings::get_all(); $api_key = isset( $settings['api_key'] ) ? $settings['api_key'] : ''; return ( ! empty( $api_key ) && ! empty( $token ) && hash_equals( (string) $api_key, (string) $token ) ); } /** * Create coupon endpoint (legacy) * * @param WP_REST_Request $request Request object. * @return WP_REST_Response */ public function create_coupon_endpoint( $request ) { $params = $request->get_params(); $result = $this->create_woocommerce_coupon( $params ); if ( $result['success'] ) { return new WP_REST_Response( $result, 201 ); } else { return new WP_REST_Response( $result, 400 ); } } /** * Delete coupon endpoint * * @param WP_REST_Request $request Request object. * @return WP_REST_Response */ public function delete_coupon_endpoint( $request ) { $code = $request->get_param( 'code' ); $result = $this->delete_woocommerce_coupon( $code ); if ( $result['success'] ) { return new WP_REST_Response( $result, 200 ); } else { return new WP_REST_Response( $result, 404 ); } } /** * Get coupon usage endpoint * * @param WP_REST_Request $request Request object. * @return WP_REST_Response */ public function get_coupon_usage_endpoint( $request ) { $code = $request->get_param( 'code' ); $usage_data = $this->get_coupon_usage_data( $code ); if ( $usage_data ) { return new WP_REST_Response( $usage_data, 200 ); } else { return new WP_REST_Response( array( 'success' => false, 'message' => 'Coupon not found', ), 404 ); } } /** * Create WooCommerce coupon (legacy method) * * @param array $coupon_data Coupon data. * @return array */ public function create_woocommerce_coupon( $coupon_data ) { try { // Check if coupon already exists. $existing_coupon = new WC_Coupon( $coupon_data['code'] ); if ( $existing_coupon->get_id() ) { return array( 'success' => false, 'message' => 'Coupon already exists', 'coupon_id' => $existing_coupon->get_id(), ); } // Create new coupon. $coupon = new WC_Coupon(); $coupon->set_code( $coupon_data['code'] ); $coupon->set_discount_type( $coupon_data['discount_type'] ); $coupon->set_amount( $coupon_data['amount'] ); // Set optional properties. if ( ! empty( $coupon_data['expiry_date'] ) ) { $coupon->set_date_expires( $coupon_data['expiry_date'] ); } 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'] ); } if ( isset( $coupon_data['minimum_amount'] ) ) { $coupon->set_minimum_amount( $coupon_data['minimum_amount'] ); } if ( isset( $coupon_data['maximum_amount'] ) ) { $coupon->set_maximum_amount( $coupon_data['maximum_amount'] ); } if ( isset( $coupon_data['individual_use'] ) ) { $coupon->set_individual_use( $coupon_data['individual_use'] ); } if ( isset( $coupon_data['exclude_sale_items'] ) ) { $coupon->set_exclude_sale_items( $coupon_data['exclude_sale_items'] ); } 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'] ); } if ( ! empty( $coupon_data['email_restrictions'] ) ) { $coupon->set_email_restrictions( $coupon_data['email_restrictions'] ); } // Save coupon. $coupon_id = $coupon->save(); // Mark as ActiveTrail coupon. update_post_meta( $coupon_id, '_activetrail_coupon', 'yes' ); update_post_meta( $coupon_id, '_activetrail_created_date', current_time( 'mysql' ) ); $this->log( 'Coupon created successfully: ' . $coupon_data['code'] . ' (ID: ' . $coupon_id . ')' ); return array( 'success' => true, 'message' => 'Coupon created successfully', 'coupon_id' => $coupon_id, 'coupon_code' => $coupon_data['code'], ); } catch ( Exception $e ) { $this->log( 'Failed to create coupon: ' . $e->getMessage(), 'error' ); return array( 'success' => false, 'message' => 'Failed to create coupon: ' . $e->getMessage(), ); } } /** * Delete WooCommerce coupon * * @param string $code Coupon code. * @return array */ public function delete_woocommerce_coupon( $code ) { try { $coupon = new WC_Coupon( $code ); if ( ! $coupon->get_id() ) { return array( 'success' => false, 'message' => 'Coupon not found', ); } // Check if it's an ActiveTrail coupon. // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for coupon data $is_activetrail = get_post_meta( $coupon->get_id(), '_activetrail_coupon', true ); if ( 'yes' !== $is_activetrail ) { return array( 'success' => false, 'message' => 'Can only delete ActiveTrail coupons', ); } // Delete coupon. wp_delete_post( $coupon->get_id(), true ); $this->log( 'Coupon deleted successfully: ' . $code ); return array( 'success' => true, 'message' => 'Coupon deleted successfully', ); } catch ( Exception $e ) { $this->log( 'Failed to delete coupon: ' . $e->getMessage(), 'error' ); return array( 'success' => false, 'message' => 'Failed to delete coupon: ' . $e->getMessage(), ); } } /** * Get coupon usage data * * @param string $code Coupon code. * @return array|false */ public function get_coupon_usage_data( $code ) { $coupon = new WC_Coupon( $code ); if ( ! $coupon->get_id() ) { return false; } return array( 'coupon_id' => $coupon->get_id(), 'code' => $coupon->get_code(), 'discount_type' => $coupon->get_discount_type(), 'amount' => $coupon->get_amount(), 'usage_count' => $coupon->get_usage_count(), 'usage_limit' => $coupon->get_usage_limit(), 'usage_limit_per_user' => $coupon->get_usage_limit_per_user(), 'expiry_date' => $coupon->get_date_expires() ? $coupon->get_date_expires()->format( 'Y-m-d H:i:s' ) : null, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for coupon data 'is_activetrail' => get_post_meta( $coupon->get_id(), '_activetrail_coupon', true ) === 'yes', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for coupon data 'created_date' => get_post_meta( $coupon->get_id(), '_activetrail_created_date', true ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for coupon data 'external_id' => get_post_meta( $coupon->get_id(), '_activetrail_external_id', true ), ); } /** * Handle coupon applied * * @param string $coupon_code Coupon code. */ public function handle_coupon_applied( $coupon_code ) { $this->log( 'Coupon applied: ' . $coupon_code ); // Track coupon application. $this->track_coupon_event( $coupon_code, 'applied' ); } /** * Handle coupon removed * * @param string $coupon_code Coupon code. */ public function handle_coupon_removed( $coupon_code ) { $this->log( 'Coupon removed: ' . $coupon_code ); // Track coupon removal. $this->track_coupon_event( $coupon_code, 'removed' ); } /** * Handle coupon usage on order completion * * @param int $order_id Order ID. */ public function handle_coupon_usage( $order_id ) { $order = wc_get_order( $order_id ); if ( ! $order ) { return; } foreach ( $order->get_coupon_codes() as $coupon_code ) { $this->log( 'Coupon used in completed order: ' . $coupon_code . ' (Order: ' . $order_id . ')' ); // Track coupon usage. $this->track_coupon_event( $coupon_code, 'used', $order_id ); } } /** * Track coupon event * * @param string $coupon_code Coupon code. * @param string $event Event type. * @param int $order_id Order ID (optional) */ private function track_coupon_event( $coupon_code, $event, $order_id = null ) { $coupon = new WC_Coupon( $coupon_code ); if ( ! $coupon->get_id() ) { return; } // Prepare event data. $event_data = array( 'coupon_code' => $coupon_code, 'coupon_id' => $coupon->get_id(), 'coupon_type' => $coupon->get_discount_type(), 'coupon_amount' => $coupon->get_amount(), 'discount_amount' => 0, 'event' => $event, 'timestamp' => current_time( 'mysql' ), 'user_id' => get_current_user_id(), 'order_id' => $order_id, 'customer_email' => $this->get_customer_email(), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for coupon data 'is_activetrail_coupon' => get_post_meta( $coupon->get_id(), '_activetrail_coupon', true ) === 'yes', ); // Get discount amount if order is provided. if ( $order_id ) { $order = wc_get_order( $order_id ); if ( $order ) { $event_data['discount_amount'] = $order->get_discount_total(); $event_data['order_total'] = $order->get_total(); } } // Track event using the Event Handler if available. if ( class_exists( 'ActiveTrail_Plugin' ) ) { $plugin = ActiveTrail_Plugin::get_instance(); if ( $plugin && method_exists( $plugin, 'get_event_handler' ) ) { $event_handler = $plugin->get_event_handler(); if ( $event_handler ) { $event_handler->track_event( 'coupon_' . $event, $event_data, $event_data['discount_amount'] ); } } } // Send to ActiveTrail if API is configured. if ( ! empty( $this->settings['api_key'] ) ) { $this->send_coupon_event( $event_data ); } } /** * Send coupon event to ActiveTrail * * @param array $event_data Event data. */ private function send_coupon_event( $event_data ) { // This would be implemented based on ActiveTrail's specific API for coupon tracking. // For now, we'll log it. $this->log( 'Coupon event tracked: ' . wp_json_encode( $event_data ) ); } /** * Get customer email * * @return string */ private function get_customer_email() { $email = ''; // Try to get from current user. if ( is_user_logged_in() ) { $user = wp_get_current_user(); $email = $user->user_email; } // Try to get from WooCommerce customer. if ( empty( $email ) && WC()->customer ) { $email = WC()->customer->get_billing_email(); } return $email; } /** * Clean expired coupons */ public function clean_expired_coupons() { $args = array( 'post_type' => 'shop_coupon', 'post_status' => 'publish', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query necessary for filtering ActiveTrail coupons 'meta_query' => array( array( 'key' => '_activetrail_coupon', 'value' => 'yes', 'compare' => '=', ), array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key,WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Meta queries necessary for filtering expired coupons 'key' => 'date_expires', 'value' => current_time( 'timestamp' ), 'compare' => '<', ), ), 'posts_per_page' => -1, ); $expired_coupons = get_posts( $args ); $cleaned_count = 0; foreach ( $expired_coupons as $coupon_post ) { $coupon = new WC_Coupon( $coupon_post->ID ); // Only delete if it has expired and is an ActiveTrail coupon. if ( $coupon->get_date_expires() && $coupon->get_date_expires()->getTimestamp() < current_time( 'timestamp' ) ) { wp_delete_post( $coupon_post->ID, true ); ++$cleaned_count; $this->log( 'Cleaned expired coupon: ' . $coupon->get_code() ); } } $this->log( "Cleaned {$cleaned_count} expired ActiveTrail coupons" ); return $cleaned_count; } /** * Get ActiveTrail coupons * * @param int $limit Limit. * @return array */ public function get_activetrail_coupons( $limit = 50 ) { $args = array( 'post_type' => 'shop_coupon', 'post_status' => 'publish', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query necessary for filtering ActiveTrail coupons 'meta_query' => array( array( 'key' => '_activetrail_coupon', 'value' => 'yes', 'compare' => '=', ), ), 'posts_per_page' => $limit, 'orderby' => 'date', 'order' => 'DESC', ); $coupon_posts = get_posts( $args ); $coupons = array(); foreach ( $coupon_posts as $coupon_post ) { $coupon = new WC_Coupon( $coupon_post->ID ); $coupons[] = array( 'id' => $coupon->get_id(), 'code' => $coupon->get_code(), 'discount_type' => $coupon->get_discount_type(), 'amount' => $coupon->get_amount(), 'usage_count' => $coupon->get_usage_count(), 'usage_limit' => $coupon->get_usage_limit(), 'expiry_date' => $coupon->get_date_expires() ? $coupon->get_date_expires()->format( 'Y-m-d H:i:s' ) : null, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for coupon data 'created_date' => get_post_meta( $coupon->get_id(), '_activetrail_created_date', true ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for coupon data 'external_id' => get_post_meta( $coupon->get_id(), '_activetrail_external_id', true ), ); } return $coupons; } /** * Log message * * @param string $message Log message. * @param string $level Log level. */ private function log( $message, $level = 'info' ) { if ( function_exists( 'activetrail_logger' ) ) { $level_map = array( 'info' => 'INFO', 'debug' => 'DEBUG', 'warning' => 'WARNING', 'error' => 'ERROR', ); $mapped_level = isset( $level_map[ $level ] ) ? $level_map[ $level ] : 'INFO'; activetrail_logger()->log( $message, $mapped_level, 'GENERAL' ); } } /** * Handle REST coupon creation (new secure endpoint) * * @param WP_REST_Request $request Request object. * @return WP_REST_Response */ public function handle_secure_rest_coupon_creation( $request ) { $params = $request->get_params(); // Log the request. $this->log( 'Secure REST coupon creation request received: ' . wp_json_encode( $params ) ); // Validate required fields. $validation = $this->validate_coupon_data( $params ); if ( ! $validation['valid'] ) { $this->log( 'Validation failed for secure REST coupon: ' . $validation['message'], 'error' ); return new WP_REST_Response( array( 'success' => false, 'message' => $validation['message'], ), 400 ); } // Check for existing coupon by external_id (idempotency). $external_id = $params['external_id'] ?? ''; $coupon_code = $params['coupon_code']; if ( ! empty( $external_id ) ) { $existing_coupon_id = $this->find_coupon_by_external_id( $external_id ); if ( $existing_coupon_id ) { // Update existing coupon. $result = $this->update_woocommerce_coupon( $existing_coupon_id, $params ); if ( $result['success'] ) { $this->log( 'Coupon updated successfully via secure REST: ' . $coupon_code . ' (ID: ' . $existing_coupon_id . ')' ); return new WP_REST_Response( $result, 200 ); } else { $this->log( 'Failed to update coupon via secure REST: ' . $result['message'], 'error' ); return new WP_REST_Response( $result, 500 ); } } } // Check if coupon code already exists (without external_id). if ( empty( $external_id ) ) { $existing_coupon_id = wc_get_coupon_id_by_code( $coupon_code ); if ( $existing_coupon_id ) { $this->log( 'Coupon code already exists without external_id via secure REST: ' . $coupon_code, 'error' ); return new WP_REST_Response( array( 'success' => false, 'message' => 'Coupon code already exists. Use external_id for idempotency.', ), 409 ); } } // Create new coupon. $result = $this->create_woocommerce_coupon_secure( $params ); if ( $result['success'] ) { $this->log( 'Coupon created successfully via secure REST: ' . $coupon_code . ' (ID: ' . $result['coupon_id'] . ')' ); return new WP_REST_Response( $result, 201 ); } else { $this->log( 'Failed to create coupon via secure REST: ' . $result['message'], 'error' ); return new WP_REST_Response( $result, 500 ); } } /** * Check permission for secure REST endpoint * * @param WP_REST_Request $request Request object. * @return bool */ public function check_secure_rest_permission( $request ) { // Verify API key from header. $token = get_option( 'activetrail_api_key' ); $header_token = $request->get_header( 'x-at-auth' ); if ( ! $token || ! $header_token || ! hash_equals( $token, $header_token ) ) { return false; } return true; } /** * Handle REST coupon creation (new endpoint) * * @param WP_REST_Request $request Request object. * @return WP_REST_Response */ public function handle_rest_coupon_creation( $request ) { $params = $request->get_params(); // Log the request. $this->log( 'REST coupon creation request received: ' . wp_json_encode( $params ) ); // Use centralized coupon creation function. $result = $this->create_coupon_from_array( $params ); if ( is_wp_error( $result ) ) { $error_code = $result->get_error_code(); $http_code = ( $error_code === 'exists' ) ? 409 : 400; $this->log( 'REST coupon creation failed: ' . $result->get_error_message(), 'error' ); return new WP_REST_Response( array( 'success' => false, 'error' => $result->get_error_message(), ), $http_code ); } $this->log( 'Coupon created successfully via REST: ' . $params['coupon_code'] . ' (ID: ' . $result . ')' ); return new WP_REST_Response( array( 'success' => true, 'coupon_id' => $result, 'coupon_code' => $params['coupon_code'], ), 201 ); } /** * Check permission for new REST endpoint * * @param WP_REST_Request $request Request object. * @return bool|WP_Error */ public function check_rest_permission( $request ) { $token = get_option( 'activetrail_api_key' ); $header_token = $request->get_header( 'x-at-auth' ); if ( ! $token || ! $header_token || ! hash_equals( $token, $header_token ) ) { return new WP_Error( 'at_auth', 'Unauthorized', array( 'status' => 401 ) ); } // Optional: HMAC signature verification. $signature = $request->get_header( 'x-at-signature' ); if ( $signature && strpos( $signature, 'sha256=' ) === 0 ) { $calculated = 'sha256=' . hash_hmac( 'sha256', $request->get_body(), $token ); if ( ! hash_equals( $calculated, $signature ) ) { return new WP_Error( 'at_sig', 'Bad signature', array( 'status' => 401 ) ); } } return true; } /** * Detect if data is in camelCase format (new ActiveTrail format) * * @param array $data Coupon data. * @return bool */ private function is_camel_case_format( $data ) { return isset( $data['couponCode'] ) || isset( $data['discountType'] ); } /** * Convert ActiveTrail camelCase format to WooCommerce snake_case format * * @param array $data Coupon data in camelCase. * @return array Coupon data in snake_case */ private function convert_activetrail_format( $data ) { $converted = array(); // Map camelCase to snake_case. $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', ); // Convert field names. foreach ( $field_map as $camel => $snake ) { if ( isset( $data[ $camel ] ) ) { $converted[ $snake ] = $data[ $camel ]; } } // Handle discount type conversion. if ( isset( $converted['discount_type'] ) ) { $converted['discount_type'] = $this->map_discount_type( $converted['discount_type'] ); // Special handling for free_shipping discount type. if ( 'free_shipping' === $data['discountType'] ) { $converted['amount'] = '0'; $converted['free_shipping'] = true; $converted['discount_type'] = 'fixed_cart'; } } $this->log( 'Converted camelCase format to snake_case: ' . wp_json_encode( $converted ) ); return $converted; } /** * Map ActiveTrail discount types to WooCommerce discount types * * @param string $type ActiveTrail discount type. * @return string WooCommerce discount type */ private function map_discount_type( $type ) { $type_map = array( 'percentage' => 'percent', 'fixed_price' => 'fixed_cart', 'free_shipping' => 'fixed_cart', // Will be handled specially // Keep existing types for backward compatibility. 'percent' => 'percent', 'fixed_cart' => 'fixed_cart', 'fixed_product' => 'fixed_product', ); return $type_map[ $type ] ?? $type; } /** * Centralized coupon creation function for all endpoints * * @param array $data Coupon data. * @return int|WP_Error Coupon ID on success, WP_Error on failure */ public function create_coupon_from_array( $data ) { // Auto-detect and convert format if needed. if ( $this->is_camel_case_format( $data ) ) { $this->log( 'Detected camelCase format, converting to snake_case' ); $data = $this->convert_activetrail_format( $data ); } // Basic validation. $required_fields = array( 'coupon_code', 'discount_type' ); foreach ( $required_fields as $field ) { if ( empty( $data[ $field ] ) ) { return new WP_Error( 'bad_request', "Missing required field: $field" ); } } // Amount is not required for free_shipping coupons. if ( empty( $data['amount'] ) && ( ! isset( $data['free_shipping'] ) || ! $data['free_shipping'] ) ) { return new WP_Error( 'bad_request', 'Missing required field: amount' ); } // Validate discount type. $valid_types = array( 'percent', 'fixed_cart', 'fixed_product' ); if ( ! in_array( $data['discount_type'], $valid_types, true ) ) { return new WP_Error( 'bad_request', 'Invalid discount_type. Must be one of: ' . implode( ', ', $valid_types ) ); } // Idempotency check by external_id. if ( ! empty( $data['external_id'] ) ) { $existing_coupon_id = $this->find_coupon_by_external_id( $data['external_id'] ); if ( $existing_coupon_id ) { // Update existing coupon. $update_result = $this->update_woocommerce_coupon( $existing_coupon_id, $data ); if ( $update_result['success'] ) { $this->log( 'Coupon updated via idempotency: ' . $data['coupon_code'] . ' (ID: ' . $existing_coupon_id . ')' ); return $existing_coupon_id; } else { return new WP_Error( 'update_failed', 'Failed to update existing coupon: ' . $update_result['message'] ); } } } // Check for existing coupon code (without external_id). if ( empty( $data['external_id'] ) ) { $existing_coupon_id = wc_get_coupon_id_by_code( $data['coupon_code'] ); if ( $existing_coupon_id ) { return new WP_Error( 'exists', 'Coupon code already exists. Use external_id for idempotency.' ); } } // Create new coupon. $coupon = new WC_Coupon(); $coupon->set_code( $data['coupon_code'] ); $coupon->set_description( $data['description'] ?? '' ); $coupon->set_discount_type( $data['discount_type'] ); $coupon->set_amount( $data['amount'] ); // Set expiration date. if ( ! empty( $data['date_expires'] ) ) { $coupon->set_date_expires( $data['date_expires'] ); } // Set usage limits. if ( isset( $data['usage_limit'] ) ) { $coupon->set_usage_limit( (int) $data['usage_limit'] ); } if ( isset( $data['usage_limit_per_user'] ) ) { $coupon->set_usage_limit_per_user( (int) $data['usage_limit_per_user'] ); } // Set individual use. if ( isset( $data['individual_use'] ) ) { $coupon->set_individual_use( (bool) $data['individual_use'] ); } // Set amount limits. if ( isset( $data['minimum_amount'] ) ) { $coupon->set_minimum_amount( (string) $data['minimum_amount'] ); } if ( isset( $data['maximum_amount'] ) && $data['maximum_amount'] !== null ) { $coupon->set_maximum_amount( (string) $data['maximum_amount'] ); } // Set free shipping. if ( isset( $data['free_shipping'] ) ) { $coupon->set_free_shipping( (bool) $data['free_shipping'] ); } // Set product restrictions. if ( ! empty( $data['product_ids'] ) ) { $coupon->set_product_ids( array_map( 'intval', $data['product_ids'] ) ); } if ( ! empty( $data['excluded_product_ids'] ) ) { $coupon->set_excluded_product_ids( array_map( 'intval', $data['excluded_product_ids'] ) ); } if ( ! empty( $data['product_categories'] ) ) { $coupon->set_product_categories( array_map( 'intval', $data['product_categories'] ) ); } if ( ! empty( $data['excluded_product_categories'] ) ) { $coupon->set_excluded_product_categories( array_map( 'intval', $data['excluded_product_categories'] ) ); } // Save the coupon. $coupon_id = $coupon->save(); if ( ! $coupon_id ) { return new WP_Error( 'save_failed', 'Failed to save coupon' ); } // Store external_id for idempotency. if ( ! empty( $data['external_id'] ) ) { update_post_meta( $coupon_id, '_at_external_id', sanitize_text_field( $data['external_id'] ) ); } // Store account_id. if ( ! empty( $data['account_id'] ) ) { update_post_meta( $coupon_id, '_at_account_id', sanitize_text_field( $data['account_id'] ) ); } $this->log( 'Coupon created successfully: ' . $data['coupon_code'] . ' (ID: ' . $coupon_id . ')' ); return $coupon_id; } }