544 lines
16 KiB
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');
|
|
}
|
|
}
|