api_key = $this->get_api_key(); } /** * Get decrypted API key - SECURITY ENHANCEMENT */ private function get_api_key() { // Check for encrypted key first $encrypted = get_option('ticket_tailor_api_key_encrypted', ''); if (!empty($encrypted)) { return $this->decrypt_api_key($encrypted); } // Fallback to plain text key (for migration) $plain_key = get_option('ticket_tailor_api_key', ''); // If plain key exists, encrypt it and migrate if (!empty($plain_key)) { $this->migrate_api_key($plain_key); return $plain_key; } return ''; } /** * Encrypt API key - SECURITY ENHANCEMENT */ private function encrypt_api_key($key) { if (!function_exists('openssl_encrypt') || !function_exists('wp_salt')) { // Fallback if OpenSSL or wp_salt not available return base64_encode($key); } $method = 'AES-256-CBC'; $salt = wp_salt('auth'); $encryption_key = substr(hash('sha256', $salt), 0, 32); $iv = substr(hash('sha256', wp_salt('nonce')), 0, 16); $encrypted = openssl_encrypt($key, $method, $encryption_key, 0, $iv); return base64_encode($encrypted); } /** * Decrypt API key - SECURITY ENHANCEMENT */ private function decrypt_api_key($encrypted) { if (!function_exists('openssl_decrypt') || !function_exists('wp_salt')) { // Fallback if OpenSSL or wp_salt not available return base64_decode($encrypted); } $method = 'AES-256-CBC'; $salt = wp_salt('auth'); $encryption_key = substr(hash('sha256', $salt), 0, 32); $iv = substr(hash('sha256', wp_salt('nonce')), 0, 16); $decrypted = openssl_decrypt(base64_decode($encrypted), $method, $encryption_key, 0, $iv); return $decrypted !== false ? $decrypted : ''; } /** * Migrate plain text API key to encrypted - SECURITY ENHANCEMENT */ private function migrate_api_key($plain_key) { $encrypted = $this->encrypt_api_key($plain_key); update_option('ticket_tailor_api_key_encrypted', $encrypted); // Remove plain text key delete_option('ticket_tailor_api_key'); } /** * Check if API is configured */ public function is_configured() { return !empty($this->api_key); } /** * Make API request with retry logic - ENTERPRISE: Exponential backoff */ private function request($endpoint, $method = 'GET', $data = array()) { if (!$this->is_configured()) { return new WP_Error('no_api_key', __('API key not configured', 'ticket-tailor')); } $attempt = 0; $last_error = null; while ($attempt < $this->max_retries) { $attempt++; $result = $this->make_request_attempt($endpoint, $method, $data, $attempt); // Success - return immediately if (!is_wp_error($result)) { return $result; } $last_error = $result; $error_data = $result->get_error_data(); // Don't retry on client errors (4xx except 429) if (isset($error_data['status'])) { $status = $error_data['status']; // Retry only on: 429 (rate limit), 5xx (server errors), network errors if ($status >= 400 && $status < 500 && $status !== 429) { // Client error - don't retry error_log(sprintf( 'Ticket Tailor API: Client error %d on %s, not retrying', $status, $endpoint )); return $result; } } // If we have more retries, wait with exponential backoff if ($attempt < $this->max_retries) { $delay = $this->calculate_backoff_delay($attempt, $error_data); error_log(sprintf( 'Ticket Tailor API: Attempt %d/%d failed for %s, retrying in %dms', $attempt, $this->max_retries, $endpoint, $delay )); usleep($delay * 1000); // Convert ms to microseconds } } // All retries exhausted error_log(sprintf( 'Ticket Tailor API: All %d attempts failed for %s: %s', $this->max_retries, $endpoint, $last_error->get_error_message() )); return $last_error; } /** * Make single API request attempt - ENTERPRISE: Separated for retry logic */ private function make_request_attempt($endpoint, $method, $data, $attempt_number) { $url = $this->base_url . ltrim($endpoint, '/'); $args = array( 'method' => $method, 'headers' => array( 'Accept' => 'application/json', 'Authorization' => 'Basic ' . base64_encode($this->api_key . ':'), 'X-TT-Request-ID' => wp_generate_uuid4(), // ENTERPRISE: Request tracking ), 'timeout' => 30, ); if (!empty($data)) { if ($method === 'GET') { $url = add_query_arg($data, $url); } else { $args['body'] = wp_json_encode($data); $args['headers']['Content-Type'] = 'application/json'; } } $response = wp_remote_request($url, $args); // Handle network errors if (is_wp_error($response)) { return new WP_Error( 'network_error', sprintf('Network error: %s', $response->get_error_message()), array( 'attempt' => $attempt_number, 'endpoint' => $endpoint, 'original_error' => $response->get_error_code() ) ); } // Track rate limits $headers = wp_remote_retrieve_headers($response); if (isset($headers['x-rate-limit-remaining'])) { $this->rate_limit_remaining = (int) $headers['x-rate-limit-remaining']; } if (isset($headers['x-rate-limit-reset'])) { $this->rate_limit_reset = (int) $headers['x-rate-limit-reset']; } $code = wp_remote_retrieve_response_code($response); $body = wp_remote_retrieve_body($response); // Handle HTTP errors if ($code < 200 || $code >= 300) { $error_data = json_decode($body, true); $message = isset($error_data['message']) ? $error_data['message'] : 'API request failed'; return new WP_Error( 'api_error', sprintf('%s (HTTP %d)', $message, $code), array( 'status' => $code, 'response' => $error_data, 'attempt' => $attempt_number, 'endpoint' => $endpoint, 'rate_limit_remaining' => $this->rate_limit_remaining ) ); } $decoded = json_decode($body, true); if (json_last_error() !== JSON_ERROR_NONE) { return new WP_Error( 'json_decode_error', sprintf('Failed to decode API response: %s', json_last_error_msg()), array( 'attempt' => $attempt_number, 'endpoint' => $endpoint, 'body_preview' => substr($body, 0, 200) ) ); } return $decoded; } /** * Calculate backoff delay - ENTERPRISE: Exponential backoff with jitter */ private function calculate_backoff_delay($attempt, $error_data = array()) { // For rate limits, use the reset time if available if (isset($error_data['status']) && $error_data['status'] === 429) { if ($this->rate_limit_reset) { $wait_time = max(0, $this->rate_limit_reset - time()); return min($wait_time * 1000, 60000); // Max 60 seconds } } // Exponential backoff: delay = base * (2 ^ attempt) $delay = $this->retry_delay_ms * pow(2, $attempt - 1); // Add jitter (random ±25%) $jitter = $delay * (rand(75, 125) / 100); // Cap at 30 seconds return min($jitter, 30000); } /** * Get all events */ public function get_events($args = array()) { $defaults = array( 'limit' => 100, ); $args = wp_parse_args($args, $defaults); return $this->request('events', 'GET', $args); } /** * Get single event - SECURITY FIX: Added length validation */ public function get_event($event_id) { $event_id = sanitize_text_field($event_id); // SECURITY FIX: Validate ID length (reasonable limit) if (strlen($event_id) > 100) { return new WP_Error('invalid_id', __('Invalid event ID', 'ticket-tailor')); } return $this->request("events/{$event_id}"); } /** * Get event ticket types - SECURITY FIX: Added length validation */ public function get_ticket_types($event_id) { $event_id = sanitize_text_field($event_id); // SECURITY FIX: Validate ID length if (strlen($event_id) > 100) { return new WP_Error('invalid_id', __('Invalid event ID', 'ticket-tailor')); } return $this->request("events/{$event_id}/ticket_types"); } /** * Get orders */ public function get_orders($args = array()) { $defaults = array( 'limit' => 100, ); $args = wp_parse_args($args, $defaults); return $this->request('orders', 'GET', $args); } /** * Get single order - SECURITY FIX: Added length validation */ public function get_order($order_id) { $order_id = sanitize_text_field($order_id); // SECURITY FIX: Validate ID length if (strlen($order_id) > 100) { return new WP_Error('invalid_id', __('Invalid order ID', 'ticket-tailor')); } return $this->request("orders/{$order_id}"); } /** * Get issued tickets for an order - SECURITY FIX: Added length validation */ public function get_order_tickets($order_id) { $order_id = sanitize_text_field($order_id); // SECURITY FIX: Validate ID length if (strlen($order_id) > 100) { return new WP_Error('invalid_id', __('Invalid order ID', 'ticket-tailor')); } return $this->request("orders/{$order_id}/issued_tickets"); } /** * Get box office overview */ public function get_overview() { return $this->request('overview'); } /** * Create a hold - SECURITY FIX: Added validation */ public function create_hold($ticket_type_id, $quantity) { $ticket_type_id = sanitize_text_field($ticket_type_id); $quantity = absint($quantity); // SECURITY FIX: Validate inputs if (strlen($ticket_type_id) > 100) { return new WP_Error('invalid_id', __('Invalid ticket type ID', 'ticket-tailor')); } if ($quantity < 1 || $quantity > 100) { return new WP_Error('invalid_quantity', __('Invalid quantity (must be 1-100)', 'ticket-tailor')); } $data = array( 'ticket_type_id' => $ticket_type_id, 'quantity' => $quantity, ); return $this->request('holds', 'POST', $data); } /** * Get voucher codes */ public function get_voucher_codes($args = array()) { return $this->request('voucher_codes', 'GET', $args); } /** * Get discount codes */ public function get_discount_codes($args = array()) { return $this->request('discount_codes', 'GET', $args); } /** * Paginate through all results - PERFORMANCE FIX: Removed array_merge memory leak */ public function get_all_paginated($endpoint, $args = array()) { $all_results = array(); $args['limit'] = 100; $iteration_count = 0; $max_iterations = 100; // 100 * 100 = 10,000 items max $start_time = time(); $max_execution_time = 60; // 60 seconds max do { $response = $this->request($endpoint, 'GET', $args); if (is_wp_error($response)) { return $response; } // PERFORMANCE FIX: Use array push instead of array_merge if (isset($response['data']) && is_array($response['data'])) { foreach ($response['data'] as $item) { $all_results[] = $item; } } // Check for next page $has_more = false; if (isset($response['pagination']['has_more']) && $response['pagination']['has_more']) { $last_item = end($response['data']); if (isset($last_item['id'])) { $args['starting_after'] = $last_item['id']; $has_more = true; } else { break; } } // Safety checks to prevent infinite loops $iteration_count++; if ($iteration_count >= $max_iterations) { error_log('Ticket Tailor API: Reached maximum iterations (' . $max_iterations . ')'); break; } // Timeout protection if ((time() - $start_time) >= $max_execution_time) { error_log('Ticket Tailor API: Reached maximum execution time (' . $max_execution_time . 's)'); break; } // Item count protection if (count($all_results) >= 10000) { error_log('Ticket Tailor API: Reached maximum items (10000)'); break; } } while ($has_more); return $all_results; } /** * Test API connection */ public function test_connection() { $response = $this->request('events', 'GET', array('limit' => 1)); if (is_wp_error($response)) { return false; } return true; } /** * Get rate limit info */ public function get_rate_limit_info() { return array( 'remaining' => $this->rate_limit_remaining, 'reset' => $this->rate_limit_reset, ); } /** * Set API key - SECURITY ENHANCEMENT: Store encrypted */ public function set_api_key($api_key) { $api_key = sanitize_text_field($api_key); // Get old key hash for logging $old_key_hash = hash('sha256', $this->api_key); $this->api_key = $api_key; // Store encrypted $encrypted = $this->encrypt_api_key($api_key); update_option('ticket_tailor_api_key_encrypted', $encrypted); // Remove plain text key if it exists delete_option('ticket_tailor_api_key'); // Log the change $security_logger = new Ticket_Tailor_Security_Logger(); $security_logger->log_api_key_change($old_key_hash, hash('sha256', $api_key)); // Test the connection return $this->test_connection(); } /** * Clear API key - SECURITY ENHANCEMENT: Clear encrypted key */ public function clear_api_key() { $this->api_key = ''; delete_option('ticket_tailor_api_key_encrypted'); delete_option('ticket_tailor_api_key'); } }