monorepo/native/wordpress/ticket-tailor-wp-max/includes/class-api-client.php

544 lines
16 KiB
PHP

<?php
/**
* Ticket Tailor API Client
*
* Handles all communication with the Ticket Tailor API
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
class Ticket_Tailor_API_Client {
/**
* API Base URL
*/
private $base_url = 'https://api.tickettailor.com/v1/';
/**
* API Key
*/
private $api_key;
/**
* Rate limit tracking
*/
private $rate_limit_remaining;
private $rate_limit_reset;
/**
* Retry configuration - ENTERPRISE: Exponential backoff
*/
private $max_retries = 3;
private $retry_delay_ms = 1000; // 1 second base delay
/**
* Constructor - SECURITY ENHANCEMENT: Use encrypted API key
*/
public function __construct() {
$this->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');
}
}