added additional plugins
This commit is contained in:
parent
c85895d306
commit
00e60ec1b7
132 changed files with 27514 additions and 0 deletions
|
|
@ -0,0 +1,544 @@
|
|||
<?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');
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue