added additional plugins
This commit is contained in:
parent
c85895d306
commit
00e60ec1b7
132 changed files with 27514 additions and 0 deletions
1436
native/wordpress/ticket-tailor-wp-max/includes/class-admin.php
Normal file
1436
native/wordpress/ticket-tailor-wp-max/includes/class-admin.php
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
547
native/wordpress/ticket-tailor-wp-max/includes/class-blocks.php
Normal file
547
native/wordpress/ticket-tailor-wp-max/includes/class-blocks.php
Normal file
|
|
@ -0,0 +1,547 @@
|
|||
<?php
|
||||
/**
|
||||
* Ticket Tailor Blocks
|
||||
* Enhanced with full-width support and responsive columns
|
||||
* FIXED: Removed spacers that were making cards too tall
|
||||
*/
|
||||
|
||||
// Exit if accessed directly
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Ticket_Tailor_Blocks {
|
||||
|
||||
/**
|
||||
* Event Manager
|
||||
*/
|
||||
private $events;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct($events) {
|
||||
$this->events = $events;
|
||||
|
||||
add_action('init', array($this, 'register_blocks'));
|
||||
add_action('enqueue_block_editor_assets', array($this, 'enqueue_editor_assets'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register blocks
|
||||
*/
|
||||
public function register_blocks() {
|
||||
// Event Widget Block (Original)
|
||||
register_block_type('ticket-tailor/event-widget', array(
|
||||
'render_callback' => array($this, 'render_event_widget_block'),
|
||||
'attributes' => array(
|
||||
'url' => array('type' => 'string', 'default' => ''),
|
||||
'minimal' => array('type' => 'boolean', 'default' => false),
|
||||
'bgFill' => array('type' => 'boolean', 'default' => true),
|
||||
'showLogo' => array('type' => 'boolean', 'default' => true),
|
||||
'ref' => array('type' => 'string', 'default' => 'website_widget'),
|
||||
'align' => array('type' => 'string', 'default' => ''),
|
||||
),
|
||||
));
|
||||
|
||||
// Event Listing Block - ENHANCED
|
||||
register_block_type('ticket-tailor/event-listing', array(
|
||||
'render_callback' => array($this, 'render_event_listing_block'),
|
||||
'attributes' => array(
|
||||
'limit' => array('type' => 'number', 'default' => 10),
|
||||
'layout' => array('type' => 'string', 'default' => 'grid'),
|
||||
'columns' => array('type' => 'number', 'default' => 3),
|
||||
'columnsMode' => array('type' => 'string', 'default' => 'fixed'),
|
||||
'showPast' => array('type' => 'boolean', 'default' => false),
|
||||
'showImage' => array('type' => 'boolean', 'default' => true),
|
||||
'imageType' => array('type' => 'string', 'default' => 'thumbnail'),
|
||||
'fullWidth' => array('type' => 'boolean', 'default' => true),
|
||||
'maxCardWidth' => array('type' => 'string', 'default' => 'none'),
|
||||
'align' => array('type' => 'string', 'default' => ''),
|
||||
'className' => array('type' => 'string', 'default' => ''),
|
||||
),
|
||||
));
|
||||
|
||||
// Single Event Block - ENHANCED
|
||||
register_block_type('ticket-tailor/single-event', array(
|
||||
'render_callback' => array($this, 'render_single_event_block'),
|
||||
'attributes' => array(
|
||||
'eventId' => array('type' => 'string', 'default' => ''),
|
||||
'showDescription' => array('type' => 'boolean', 'default' => true),
|
||||
'showTickets' => array('type' => 'boolean', 'default' => true),
|
||||
'showImage' => array('type' => 'boolean', 'default' => true),
|
||||
'imageType' => array('type' => 'string', 'default' => 'header'),
|
||||
'fullWidth' => array('type' => 'boolean', 'default' => false),
|
||||
'maxWidth' => array('type' => 'string', 'default' => '800px'),
|
||||
'align' => array('type' => 'string', 'default' => ''),
|
||||
),
|
||||
));
|
||||
|
||||
// Category Events Block - ENHANCED
|
||||
register_block_type('ticket-tailor/category-events', array(
|
||||
'render_callback' => array($this, 'render_category_events_block'),
|
||||
'attributes' => array(
|
||||
'category' => array('type' => 'string', 'default' => ''),
|
||||
'limit' => array('type' => 'number', 'default' => 10),
|
||||
'layout' => array('type' => 'string', 'default' => 'grid'),
|
||||
'columns' => array('type' => 'number', 'default' => 3),
|
||||
'columnsMode' => array('type' => 'string', 'default' => 'fixed'),
|
||||
'showImage' => array('type' => 'boolean', 'default' => true),
|
||||
'imageType' => array('type' => 'string', 'default' => 'thumbnail'),
|
||||
'fullWidth' => array('type' => 'boolean', 'default' => true),
|
||||
'align' => array('type' => 'string', 'default' => ''),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue editor assets
|
||||
*/
|
||||
public function enqueue_editor_assets() {
|
||||
wp_enqueue_script(
|
||||
'ticket-tailor-blocks',
|
||||
TICKET_TAILOR_PLUGIN_URL . 'assets/js/blocks.js',
|
||||
array('wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-data'),
|
||||
TICKET_TAILOR_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
// Pass events data to editor
|
||||
$events = $this->events->get_events();
|
||||
if (!is_wp_error($events)) {
|
||||
$event_options = array();
|
||||
foreach ($events as $event) {
|
||||
$event_options[] = array(
|
||||
'value' => $event['id'],
|
||||
'label' => $event['name'],
|
||||
);
|
||||
}
|
||||
|
||||
wp_localize_script('ticket-tailor-blocks', 'ticketTailorData', array(
|
||||
'events' => $event_options,
|
||||
));
|
||||
}
|
||||
|
||||
wp_enqueue_style(
|
||||
'ticket-tailor-blocks-editor',
|
||||
TICKET_TAILOR_PLUGIN_URL . 'assets/css/blocks-editor.css',
|
||||
array('wp-edit-blocks'),
|
||||
TICKET_TAILOR_VERSION
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render event widget block
|
||||
*/
|
||||
public function render_event_widget_block($attributes) {
|
||||
$url = $attributes['url'] ?? '';
|
||||
$align = $attributes['align'] ?? '';
|
||||
|
||||
if (empty($url)) {
|
||||
return '<div class="tt-block-placeholder">' . esc_html__('Please enter an event URL', 'ticket-tailor') . '</div>';
|
||||
}
|
||||
|
||||
$minimal = $attributes['minimal'] ?? false;
|
||||
$bg_fill = $attributes['bgFill'] ?? true;
|
||||
$show_logo = $attributes['showLogo'] ?? true;
|
||||
$ref = $attributes['ref'] ?? 'website_widget';
|
||||
|
||||
// Add alignment class
|
||||
$wrapper_class = 'tt-widget-block';
|
||||
if (!empty($align)) {
|
||||
$wrapper_class .= ' align' . esc_attr($align);
|
||||
}
|
||||
|
||||
// Use shortcode renderer
|
||||
$shortcode = new Ticket_Tailor_Shortcodes($this->events, null);
|
||||
$widget_html = $shortcode->event_widget_shortcode(array(
|
||||
'url' => $url,
|
||||
'minimal' => $minimal ? 'true' : 'false',
|
||||
'bg_fill' => $bg_fill ? 'true' : 'false',
|
||||
'show_logo' => $show_logo ? 'true' : 'false',
|
||||
'ref' => $ref,
|
||||
));
|
||||
|
||||
return '<div class="' . esc_attr($wrapper_class) . '">' . $widget_html . '</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render event listing block - ENHANCED
|
||||
*/
|
||||
public function render_event_listing_block($attributes) {
|
||||
$limit = $attributes['limit'] ?? 10;
|
||||
$layout = $attributes['layout'] ?? 'grid';
|
||||
$columns = $attributes['columns'] ?? 3;
|
||||
$columns_mode = $attributes['columnsMode'] ?? 'fixed';
|
||||
$show_past = $attributes['showPast'] ?? false;
|
||||
$show_image = $attributes['showImage'] ?? true;
|
||||
$image_type = $attributes['imageType'] ?? 'thumbnail';
|
||||
$full_width = $attributes['fullWidth'] ?? true;
|
||||
$max_card_width = $attributes['maxCardWidth'] ?? 'none';
|
||||
$align = $attributes['align'] ?? '';
|
||||
$className = $attributes['className'] ?? '';
|
||||
|
||||
// Sanitize image type
|
||||
if (!in_array($image_type, array('thumbnail', 'header'))) {
|
||||
$image_type = 'thumbnail';
|
||||
}
|
||||
|
||||
$events = $show_past ? $this->events->get_past_events($limit) : $this->events->get_upcoming_events($limit);
|
||||
|
||||
if (is_wp_error($events)) {
|
||||
return '<div class="tt-error">' . esc_html($events->get_error_message()) . '</div>';
|
||||
}
|
||||
|
||||
if (empty($events)) {
|
||||
return '<div class="tt-no-events">' . esc_html__('No events found', 'ticket-tailor') . '</div>';
|
||||
}
|
||||
|
||||
ob_start();
|
||||
|
||||
// Build wrapper classes
|
||||
$wrapper_classes = array('tt-event-listing-wrapper');
|
||||
if ($full_width) {
|
||||
$wrapper_classes[] = 'tt-full-width';
|
||||
}
|
||||
if (!empty($align)) {
|
||||
$wrapper_classes[] = 'align' . esc_attr($align);
|
||||
}
|
||||
if (!empty($className)) {
|
||||
$wrapper_classes[] = esc_attr($className);
|
||||
}
|
||||
|
||||
// Build listing classes
|
||||
$class = 'tt-event-listing tt-layout-' . esc_attr($layout);
|
||||
if ($layout === 'grid') {
|
||||
if ($columns_mode === 'responsive') {
|
||||
$class .= ' tt-columns-responsive tt-max-columns-' . esc_attr($columns);
|
||||
} else {
|
||||
$class .= ' tt-columns-' . esc_attr($columns);
|
||||
}
|
||||
}
|
||||
|
||||
// Add inline styles ONLY for responsive mode, not for fixed mode
|
||||
$style = '';
|
||||
if ($layout === 'grid' && $columns_mode === 'responsive') {
|
||||
$min_width = $max_card_width === 'small' ? '280px' :
|
||||
($max_card_width === 'medium' ? '350px' :
|
||||
($max_card_width === 'large' ? '450px' : '320px'));
|
||||
$style = 'style="grid-template-columns: repeat(auto-fit, minmax(' . esc_attr($min_width) . ', 1fr));"';
|
||||
}
|
||||
// No inline styles for fixed mode - let CSS classes handle it
|
||||
?>
|
||||
<div class="<?php echo esc_attr(implode(' ', $wrapper_classes)); ?>">
|
||||
<div class="<?php echo esc_attr($class); ?>" <?php echo $style; ?>>
|
||||
<?php foreach ($events as $event) : ?>
|
||||
<div class="tt-event-card">
|
||||
<?php if ($show_image && !empty($event['images'][$image_type])) : ?>
|
||||
<div class="tt-event-image tt-image-<?php echo esc_attr($image_type); ?>">
|
||||
<img src="<?php echo esc_url($event['images'][$image_type]); ?>"
|
||||
alt="<?php echo esc_attr($event['name']); ?>"
|
||||
loading="lazy">
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="tt-event-content">
|
||||
<h3 class="tt-event-title"><?php echo esc_html($event['name']); ?></h3>
|
||||
|
||||
<?php if (!empty($event['start']['iso'])) : ?>
|
||||
<div class="tt-event-date">
|
||||
<span class="dashicons dashicons-calendar-alt"></span>
|
||||
<?php echo esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($event['start']['iso']))); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($event['venue']['name'])) : ?>
|
||||
<div class="tt-event-venue">
|
||||
<span class="dashicons dashicons-location"></span>
|
||||
<?php echo esc_html($event['venue']['name']); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($layout === 'list' && !empty($event['description'])) : ?>
|
||||
<div class="tt-event-excerpt">
|
||||
<?php echo esc_html(wp_trim_words($event['description'], 20)); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php /* REMOVED SPACER - was making cards too tall
|
||||
<!-- Spacer pushes button to bottom -->
|
||||
<div class="tt-event-spacer"></div>
|
||||
*/ ?>
|
||||
|
||||
<!-- Button container -->
|
||||
<?php if (!empty($event['url'])) : ?>
|
||||
<div class="tt-event-button-container">
|
||||
<a href="<?php echo esc_url($event['url']); ?>"
|
||||
class="tt-event-button"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
<?php esc_html_e('Get Tickets', 'ticket-tailor'); ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render single event block - ENHANCED
|
||||
*/
|
||||
public function render_single_event_block($attributes) {
|
||||
$event_id = $attributes['eventId'] ?? '';
|
||||
$show_description = $attributes['showDescription'] ?? true;
|
||||
$show_tickets = $attributes['showTickets'] ?? true;
|
||||
$show_image = $attributes['showImage'] ?? true;
|
||||
$image_type = $attributes['imageType'] ?? 'header';
|
||||
$full_width = $attributes['fullWidth'] ?? false;
|
||||
$max_width = $attributes['maxWidth'] ?? '800px';
|
||||
$align = $attributes['align'] ?? '';
|
||||
|
||||
if (empty($event_id)) {
|
||||
return '<div class="tt-block-placeholder">' . esc_html__('Please select an event', 'ticket-tailor') . '</div>';
|
||||
}
|
||||
|
||||
// Sanitize image type
|
||||
if (!in_array($image_type, array('thumbnail', 'header'))) {
|
||||
$image_type = 'thumbnail';
|
||||
}
|
||||
|
||||
$event = $this->events->get_event($event_id);
|
||||
|
||||
if (is_wp_error($event)) {
|
||||
return '<div class="tt-error">' . esc_html($event->get_error_message()) . '</div>';
|
||||
}
|
||||
|
||||
ob_start();
|
||||
|
||||
// Build wrapper classes
|
||||
$wrapper_classes = array('tt-single-event-wrapper');
|
||||
if (!empty($align)) {
|
||||
$wrapper_classes[] = 'align' . esc_attr($align);
|
||||
}
|
||||
|
||||
// Build inline styles
|
||||
$style = '';
|
||||
if (!$full_width && !empty($max_width)) {
|
||||
$style = 'style="max-width: ' . esc_attr($max_width) . ';"';
|
||||
}
|
||||
?>
|
||||
<div class="<?php echo esc_attr(implode(' ', $wrapper_classes)); ?>">
|
||||
<div class="tt-single-event" <?php echo $style; ?>>
|
||||
<?php if ($show_image && !empty($event['images'][$image_type])) : ?>
|
||||
<div class="tt-event-header-image">
|
||||
<img src="<?php echo esc_url($event['images'][$image_type]); ?>"
|
||||
alt="<?php echo esc_attr($event['name']); ?>"
|
||||
loading="lazy">
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="tt-event-details">
|
||||
<h2 class="tt-event-title"><?php echo esc_html($event['name']); ?></h2>
|
||||
|
||||
<div class="tt-event-meta">
|
||||
<?php if (!empty($event['start']['iso'])) : ?>
|
||||
<div class="tt-meta-item">
|
||||
<span class="dashicons dashicons-calendar-alt"></span>
|
||||
<strong><?php esc_html_e('Date:', 'ticket-tailor'); ?></strong>
|
||||
<?php echo esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($event['start']['iso']))); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($event['venue']['name'])) : ?>
|
||||
<div class="tt-meta-item">
|
||||
<span class="dashicons dashicons-location"></span>
|
||||
<strong><?php esc_html_e('Venue:', 'ticket-tailor'); ?></strong>
|
||||
<?php echo esc_html($event['venue']['name']); ?>
|
||||
<?php if (!empty($event['venue']['address'])) : ?>
|
||||
<br><small style="margin-left: 28px;"><?php echo esc_html($event['venue']['address']); ?></small>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($event['status'])) : ?>
|
||||
<div class="tt-meta-item">
|
||||
<span class="dashicons dashicons-info"></span>
|
||||
<strong><?php esc_html_e('Status:', 'ticket-tailor'); ?></strong>
|
||||
<span class="tt-status tt-status-<?php echo esc_attr($event['status']); ?>">
|
||||
<?php echo esc_html($event['status']); ?>
|
||||
</span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($show_description && !empty($event['description'])) : ?>
|
||||
<div class="tt-event-description">
|
||||
<?php echo wp_kses_post(wpautop($event['description'])); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($show_tickets && !empty($event['url'])) : ?>
|
||||
<div class="tt-event-cta">
|
||||
<a href="<?php echo esc_url($event['url']); ?>"
|
||||
class="tt-button tt-button-primary"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
<?php esc_html_e('Buy Tickets', 'ticket-tailor'); ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render category events block - ENHANCED
|
||||
*/
|
||||
public function render_category_events_block($attributes) {
|
||||
$category = $attributes['category'] ?? '';
|
||||
$limit = $attributes['limit'] ?? 10;
|
||||
$layout = $attributes['layout'] ?? 'grid';
|
||||
$columns = $attributes['columns'] ?? 3;
|
||||
$columns_mode = $attributes['columnsMode'] ?? 'fixed';
|
||||
$show_image = $attributes['showImage'] ?? true;
|
||||
$image_type = $attributes['imageType'] ?? 'thumbnail';
|
||||
$full_width = $attributes['fullWidth'] ?? true;
|
||||
$align = $attributes['align'] ?? '';
|
||||
|
||||
if (empty($category)) {
|
||||
return '<div class="tt-block-placeholder">' . esc_html__('Please select a category', 'ticket-tailor') . '</div>';
|
||||
}
|
||||
|
||||
// Sanitize image type
|
||||
if (!in_array($image_type, array('thumbnail', 'header'))) {
|
||||
$image_type = 'thumbnail';
|
||||
}
|
||||
|
||||
// Get events by category
|
||||
$events = $this->events->get_events_by_category($category);
|
||||
|
||||
if (is_wp_error($events)) {
|
||||
return '<div class="tt-error">' . esc_html($events->get_error_message()) . '</div>';
|
||||
}
|
||||
|
||||
// Limit results
|
||||
$events = array_slice($events, 0, $limit);
|
||||
|
||||
if (empty($events)) {
|
||||
return '<div class="tt-no-events">' .
|
||||
sprintf(
|
||||
/* translators: %s: category name */
|
||||
esc_html__('No events found in category: %s', 'ticket-tailor'),
|
||||
esc_html($category)
|
||||
) .
|
||||
'</div>';
|
||||
}
|
||||
|
||||
ob_start();
|
||||
|
||||
// Build wrapper classes
|
||||
$wrapper_classes = array('tt-category-events-wrapper');
|
||||
if ($full_width) {
|
||||
$wrapper_classes[] = 'tt-full-width';
|
||||
}
|
||||
if (!empty($align)) {
|
||||
$wrapper_classes[] = 'align' . esc_attr($align);
|
||||
}
|
||||
|
||||
// Build listing classes
|
||||
$class = 'tt-event-listing tt-category-listing tt-layout-' . esc_attr($layout);
|
||||
if ($layout === 'grid') {
|
||||
if ($columns_mode === 'responsive') {
|
||||
$class .= ' tt-columns-responsive tt-max-columns-' . esc_attr($columns);
|
||||
} else {
|
||||
$class .= ' tt-columns-' . esc_attr($columns);
|
||||
}
|
||||
}
|
||||
|
||||
// Add inline styles ONLY for responsive mode, not for fixed mode
|
||||
$style = '';
|
||||
if ($layout === 'grid' && $columns_mode === 'responsive') {
|
||||
$style = 'style="grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));"';
|
||||
}
|
||||
// No inline styles for fixed mode - let CSS classes handle it
|
||||
?>
|
||||
<div class="<?php echo esc_attr(implode(' ', $wrapper_classes)); ?>" data-category="<?php echo esc_attr($category); ?>">
|
||||
<div class="tt-category-header">
|
||||
<h2 class="tt-category-title">
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s: category name */
|
||||
esc_html__('%s Events', 'ticket-tailor'),
|
||||
esc_html(ucfirst($category))
|
||||
);
|
||||
?>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="<?php echo esc_attr($class); ?>" <?php echo $style; ?>>
|
||||
<?php foreach ($events as $event) : ?>
|
||||
<div class="tt-event-card">
|
||||
<?php if ($show_image && !empty($event['images'][$image_type])) : ?>
|
||||
<div class="tt-event-image tt-image-<?php echo esc_attr($image_type); ?>">
|
||||
<img src="<?php echo esc_url($event['images'][$image_type]); ?>"
|
||||
alt="<?php echo esc_attr($event['name']); ?>"
|
||||
loading="lazy">
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="tt-event-content">
|
||||
<h3 class="tt-event-title"><?php echo esc_html($event['name']); ?></h3>
|
||||
|
||||
<?php if (!empty($event['start']['iso'])) : ?>
|
||||
<div class="tt-event-date">
|
||||
<span class="dashicons dashicons-calendar-alt"></span>
|
||||
<?php echo esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($event['start']['iso']))); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($event['venue']['name'])) : ?>
|
||||
<div class="tt-event-venue">
|
||||
<span class="dashicons dashicons-location"></span>
|
||||
<?php echo esc_html($event['venue']['name']); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($layout === 'list' && !empty($event['description'])) : ?>
|
||||
<div class="tt-event-excerpt">
|
||||
<?php echo esc_html(wp_trim_words($event['description'], 20)); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php /* REMOVED SPACER - was making cards too tall
|
||||
<!-- Spacer pushes button to bottom -->
|
||||
<div class="tt-event-spacer"></div>
|
||||
*/ ?>
|
||||
|
||||
<!-- Button container -->
|
||||
<?php if (!empty($event['url'])) : ?>
|
||||
<div class="tt-event-button-container">
|
||||
<a href="<?php echo esc_url($event['url']); ?>"
|
||||
class="tt-event-button"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
<?php esc_html_e('Get Tickets', 'ticket-tailor'); ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
|
||||
return ob_get_clean();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,516 @@
|
|||
<?php
|
||||
/**
|
||||
* Ticket Tailor Event Manager
|
||||
*
|
||||
* Handles event caching, syncing, and retrieval
|
||||
*/
|
||||
|
||||
// Exit if accessed directly
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Ticket_Tailor_Event_Manager {
|
||||
|
||||
/**
|
||||
* API Client
|
||||
*/
|
||||
private $api;
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
private $table;
|
||||
|
||||
/**
|
||||
* Cache duration (in seconds)
|
||||
*/
|
||||
private $cache_duration;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct($api) {
|
||||
global $wpdb;
|
||||
|
||||
$this->api = $api;
|
||||
$this->table = $wpdb->prefix . 'ticket_tailor_events';
|
||||
$this->cache_duration = get_option('ticket_tailor_cache_duration', 3600); // 1 hour default
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all events (from cache or API) - PERFORMANCE FIX: Added limit parameter
|
||||
*/
|
||||
public function get_events($force_refresh = false, $limit = null) {
|
||||
if ($force_refresh) {
|
||||
return $this->sync_all_events();
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
// Check if cache is still valid
|
||||
$cache_valid_time = gmdate('Y-m-d H:i:s', time() - $this->cache_duration);
|
||||
|
||||
// Build query with optional limit
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT event_data FROM {$this->table} WHERE last_synced > %s ORDER BY last_synced DESC",
|
||||
$cache_valid_time
|
||||
);
|
||||
|
||||
// PERFORMANCE FIX: Add LIMIT if specified
|
||||
if ($limit !== null && is_numeric($limit)) {
|
||||
$query .= $wpdb->prepare(" LIMIT %d", absint($limit));
|
||||
}
|
||||
|
||||
$cached_events = $wpdb->get_results($query);
|
||||
|
||||
if (!empty($cached_events)) {
|
||||
$events = array();
|
||||
foreach ($cached_events as $row) {
|
||||
$event_data = json_decode($row->event_data, true);
|
||||
if ($event_data) {
|
||||
$events[] = $event_data;
|
||||
}
|
||||
}
|
||||
return $events;
|
||||
}
|
||||
|
||||
// Cache is expired or empty, fetch from API
|
||||
$all_events = $this->sync_all_events();
|
||||
|
||||
// Apply limit to synced events if specified
|
||||
if ($limit !== null && is_array($all_events)) {
|
||||
return array_slice($all_events, 0, absint($limit));
|
||||
}
|
||||
|
||||
return $all_events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get single event
|
||||
*/
|
||||
public function get_event($event_id, $force_refresh = false) {
|
||||
$event_id = sanitize_text_field($event_id);
|
||||
|
||||
if (!$force_refresh) {
|
||||
global $wpdb;
|
||||
|
||||
$cache_valid_time = gmdate('Y-m-d H:i:s', time() - $this->cache_duration);
|
||||
|
||||
$cached = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT event_data FROM {$this->table} WHERE event_id = %s AND last_synced > %s",
|
||||
$event_id,
|
||||
$cache_valid_time
|
||||
)
|
||||
);
|
||||
|
||||
if ($cached) {
|
||||
$event_data = json_decode($cached, true);
|
||||
if ($event_data) {
|
||||
return $event_data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch from API
|
||||
$event = $this->api->get_event($event_id);
|
||||
|
||||
if (is_wp_error($event)) {
|
||||
return $event;
|
||||
}
|
||||
|
||||
// Cache it
|
||||
$this->cache_event($event);
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync all events from API - ENTERPRISE: Race condition protection
|
||||
*/
|
||||
public function sync_all_events() {
|
||||
if (!$this->api->is_configured()) {
|
||||
return new WP_Error('no_api_key', __('API key not configured', 'ticket-tailor'));
|
||||
}
|
||||
|
||||
// ENTERPRISE: Acquire lock to prevent concurrent syncs
|
||||
$lock_key = 'ticket_tailor_sync_events_lock';
|
||||
$lock_acquired = $this->acquire_sync_lock($lock_key);
|
||||
|
||||
if (!$lock_acquired) {
|
||||
return new WP_Error(
|
||||
'sync_in_progress',
|
||||
__('Event sync already in progress. Please wait.', 'ticket-tailor'),
|
||||
array('lock_key' => $lock_key)
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
$events = $this->api->get_all_paginated('events');
|
||||
|
||||
if (is_wp_error($events)) {
|
||||
$this->release_sync_lock($lock_key);
|
||||
return $events;
|
||||
}
|
||||
|
||||
// ENTERPRISE: Use transaction for atomic cache updates
|
||||
global $wpdb;
|
||||
$wpdb->query('START TRANSACTION');
|
||||
|
||||
try {
|
||||
// Cache all events
|
||||
$cached_count = 0;
|
||||
foreach ($events as $event) {
|
||||
if ($this->cache_event($event)) {
|
||||
$cached_count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Update last sync time
|
||||
update_option('ticket_tailor_last_event_sync', time());
|
||||
|
||||
$wpdb->query('COMMIT');
|
||||
|
||||
error_log(sprintf(
|
||||
'Ticket Tailor: Successfully synced %d events (%d cached)',
|
||||
count($events),
|
||||
$cached_count
|
||||
));
|
||||
|
||||
} catch (Exception $e) {
|
||||
$wpdb->query('ROLLBACK');
|
||||
$this->release_sync_lock($lock_key);
|
||||
|
||||
return new WP_Error(
|
||||
'sync_transaction_failed',
|
||||
sprintf('Event sync transaction failed: %s', $e->getMessage())
|
||||
);
|
||||
}
|
||||
|
||||
$this->release_sync_lock($lock_key);
|
||||
return $events;
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->release_sync_lock($lock_key);
|
||||
|
||||
return new WP_Error(
|
||||
'sync_failed',
|
||||
sprintf('Event sync failed: %s', $e->getMessage())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquire sync lock - ENTERPRISE: Prevent concurrent syncs
|
||||
*/
|
||||
private function acquire_sync_lock($lock_key, $timeout = 300) {
|
||||
// Try to set transient with 5-minute expiry
|
||||
// If it already exists, returns false
|
||||
$acquired = set_transient($lock_key, time(), $timeout);
|
||||
|
||||
if (!$acquired) {
|
||||
// Check if lock is stale (older than timeout)
|
||||
$lock_time = get_transient($lock_key);
|
||||
if ($lock_time && (time() - $lock_time) > $timeout) {
|
||||
// Stale lock, force release and reacquire
|
||||
delete_transient($lock_key);
|
||||
return set_transient($lock_key, time(), $timeout);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release sync lock - ENTERPRISE
|
||||
*/
|
||||
private function release_sync_lock($lock_key) {
|
||||
return delete_transient($lock_key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache a single event
|
||||
*/
|
||||
private function cache_event($event) {
|
||||
if (empty($event['id'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
$event_id = sanitize_text_field($event['id']);
|
||||
$event_data = wp_json_encode($event);
|
||||
|
||||
// Check if exists
|
||||
$exists = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT id FROM {$this->table} WHERE event_id = %s",
|
||||
$event_id
|
||||
)
|
||||
);
|
||||
|
||||
if ($exists) {
|
||||
// Update existing
|
||||
return $wpdb->update(
|
||||
$this->table,
|
||||
array(
|
||||
'event_data' => $event_data,
|
||||
'last_synced' => current_time('mysql', 1),
|
||||
),
|
||||
array('event_id' => $event_id),
|
||||
array('%s', '%s'),
|
||||
array('%s')
|
||||
);
|
||||
} else {
|
||||
// Insert new
|
||||
return $wpdb->insert(
|
||||
$this->table,
|
||||
array(
|
||||
'event_id' => $event_id,
|
||||
'event_data' => $event_data,
|
||||
'last_synced' => current_time('mysql', 1),
|
||||
),
|
||||
array('%s', '%s', '%s')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get upcoming events
|
||||
*/
|
||||
public function get_upcoming_events($limit = 10) {
|
||||
$events = $this->get_events();
|
||||
|
||||
if (is_wp_error($events)) {
|
||||
return $events;
|
||||
}
|
||||
|
||||
// Filter and sort by start date
|
||||
$upcoming = array_filter($events, function($event) {
|
||||
if (!isset($event['start']['iso'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$start_time = strtotime($event['start']['iso']);
|
||||
return $start_time >= time();
|
||||
});
|
||||
|
||||
// Sort by start date
|
||||
usort($upcoming, function($a, $b) {
|
||||
$time_a = strtotime($a['start']['iso'] ?? 0);
|
||||
$time_b = strtotime($b['start']['iso'] ?? 0);
|
||||
return $time_a - $time_b;
|
||||
});
|
||||
|
||||
return array_slice($upcoming, 0, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get past events
|
||||
*/
|
||||
public function get_past_events($limit = 10) {
|
||||
$events = $this->get_events();
|
||||
|
||||
if (is_wp_error($events)) {
|
||||
return $events;
|
||||
}
|
||||
|
||||
// Filter and sort by start date
|
||||
$past = array_filter($events, function($event) {
|
||||
if (!isset($event['start']['iso'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$start_time = strtotime($event['start']['iso']);
|
||||
return $start_time < time();
|
||||
});
|
||||
|
||||
// Sort by start date (newest first)
|
||||
usort($past, function($a, $b) {
|
||||
$time_a = strtotime($a['start']['iso'] ?? 0);
|
||||
$time_b = strtotime($b['start']['iso'] ?? 0);
|
||||
return $time_b - $time_a;
|
||||
});
|
||||
|
||||
return array_slice($past, 0, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search events
|
||||
*/
|
||||
public function search_events($search_term) {
|
||||
$events = $this->get_events();
|
||||
|
||||
if (is_wp_error($events)) {
|
||||
return $events;
|
||||
}
|
||||
|
||||
$search_term = strtolower($search_term);
|
||||
|
||||
return array_filter($events, function($event) use ($search_term) {
|
||||
$name = strtolower($event['name'] ?? '');
|
||||
$description = strtolower($event['description'] ?? '');
|
||||
|
||||
return strpos($name, $search_term) !== false ||
|
||||
strpos($description, $search_term) !== false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get events by category
|
||||
*/
|
||||
public function get_events_by_category($category) {
|
||||
$events = $this->get_events();
|
||||
|
||||
if (is_wp_error($events)) {
|
||||
return $events;
|
||||
}
|
||||
|
||||
$category = strtolower($category);
|
||||
|
||||
return array_filter($events, function($event) use ($category) {
|
||||
$event_category = strtolower($event['category'] ?? '');
|
||||
return $event_category === $category;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get event ticket types
|
||||
*/
|
||||
public function get_event_tickets($event_id) {
|
||||
$event_id = sanitize_text_field($event_id);
|
||||
|
||||
// Try to get from transient first
|
||||
$cache_key = 'tt_tickets_' . $event_id;
|
||||
$cached = get_transient($cache_key);
|
||||
|
||||
if ($cached !== false) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
// Fetch from API
|
||||
$tickets = $this->api->get_ticket_types($event_id);
|
||||
|
||||
if (is_wp_error($tickets)) {
|
||||
return $tickets;
|
||||
}
|
||||
|
||||
// Cache for 15 minutes (tickets change less frequently)
|
||||
set_transient($cache_key, $tickets, 900);
|
||||
|
||||
return $tickets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear event cache - SECURITY FIX: Use prepared statements
|
||||
*/
|
||||
public function clear_cache($event_id = null) {
|
||||
global $wpdb;
|
||||
|
||||
if ($event_id) {
|
||||
$event_id = sanitize_text_field($event_id);
|
||||
$wpdb->delete($this->table, array('event_id' => $event_id), array('%s'));
|
||||
|
||||
// Also clear ticket cache
|
||||
delete_transient('tt_tickets_' . $event_id);
|
||||
} else {
|
||||
// SECURITY FIX: Validate table name before TRUNCATE
|
||||
if (preg_match('/^[a-zA-Z0-9_]+$/', $this->table)) {
|
||||
$wpdb->query("TRUNCATE TABLE `{$this->table}`");
|
||||
}
|
||||
|
||||
// Clear all ticket transients - SECURITY FIX: Use prepared statement
|
||||
$wpdb->query($wpdb->prepare(
|
||||
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
|
||||
$wpdb->esc_like('_transient_tt_tickets_') . '%'
|
||||
));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache statistics
|
||||
*/
|
||||
public function get_cache_stats() {
|
||||
global $wpdb;
|
||||
|
||||
$total_events = $wpdb->get_var("SELECT COUNT(*) FROM {$this->table}");
|
||||
|
||||
$cache_valid_time = gmdate('Y-m-d H:i:s', time() - $this->cache_duration);
|
||||
$valid_events = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$this->table} WHERE last_synced > %s",
|
||||
$cache_valid_time
|
||||
)
|
||||
);
|
||||
|
||||
$last_sync = get_option('ticket_tailor_last_event_sync', 0);
|
||||
|
||||
return array(
|
||||
'total_cached' => (int) $total_events,
|
||||
'valid_cached' => (int) $valid_events,
|
||||
'expired_cached' => (int) $total_events - (int) $valid_events,
|
||||
'last_sync' => $last_sync,
|
||||
'cache_duration' => $this->cache_duration,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get event statistics efficiently - PERFORMANCE OPTIMIZATION
|
||||
* Uses aggregated database queries instead of loading all events
|
||||
*/
|
||||
public function get_event_statistics() {
|
||||
global $wpdb;
|
||||
|
||||
$cache_key = 'tt_event_stats';
|
||||
$cached = wp_cache_get($cache_key);
|
||||
|
||||
if ($cached !== false) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$cache_valid_time = gmdate('Y-m-d H:i:s', time() - $this->cache_duration);
|
||||
|
||||
// Get total events count
|
||||
$total_events = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$this->table} WHERE last_synced > %s",
|
||||
$cache_valid_time
|
||||
)
|
||||
);
|
||||
|
||||
// Get upcoming events count (parse JSON to check dates)
|
||||
$all_events = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT event_data FROM {$this->table} WHERE last_synced > %s",
|
||||
$cache_valid_time
|
||||
)
|
||||
);
|
||||
|
||||
$upcoming_count = 0;
|
||||
$now = time();
|
||||
|
||||
foreach ($all_events as $row) {
|
||||
$event = json_decode($row->event_data, true);
|
||||
if (!empty($event['start']['iso'])) {
|
||||
$start_time = strtotime($event['start']['iso']);
|
||||
if ($start_time >= $now) {
|
||||
$upcoming_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$stats = array(
|
||||
'total_events' => (int) $total_events,
|
||||
'upcoming_events' => $upcoming_count,
|
||||
);
|
||||
|
||||
// Cache for 5 minutes
|
||||
wp_cache_set($cache_key, $stats, '', 300);
|
||||
|
||||
return $stats;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,253 @@
|
|||
<?php
|
||||
/**
|
||||
* Ticket Tailor Health Check
|
||||
*
|
||||
* Enterprise-grade health check endpoint for monitoring
|
||||
* ENTERPRISE: Provides comprehensive health status for monitoring systems
|
||||
*/
|
||||
|
||||
// Exit if accessed directly
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Ticket_Tailor_Health_Check {
|
||||
|
||||
/**
|
||||
* API Client
|
||||
*/
|
||||
private $api;
|
||||
|
||||
/**
|
||||
* Event Manager
|
||||
*/
|
||||
private $events;
|
||||
|
||||
/**
|
||||
* Order Manager
|
||||
*/
|
||||
private $orders;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct($api, $events, $orders) {
|
||||
$this->api = $api;
|
||||
$this->events = $events;
|
||||
$this->orders = $orders;
|
||||
|
||||
// Register health check endpoint
|
||||
add_action('rest_api_init', array($this, 'register_health_endpoint'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register health check REST endpoint
|
||||
*/
|
||||
public function register_health_endpoint() {
|
||||
register_rest_route('ticket-tailor/v1', '/health', array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array($this, 'get_health_status'),
|
||||
'permission_callback' => array($this, 'health_check_permission'),
|
||||
));
|
||||
|
||||
register_rest_route('ticket-tailor/v1', '/health/detailed', array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array($this, 'get_detailed_health_status'),
|
||||
'permission_callback' => array($this, 'detailed_health_check_permission'),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission callback for basic health check
|
||||
* ENTERPRISE: Can be accessed without authentication for monitoring
|
||||
*/
|
||||
public function health_check_permission() {
|
||||
// Allow unauthenticated access for monitoring tools
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission callback for detailed health check
|
||||
* ENTERPRISE: Requires authentication
|
||||
*/
|
||||
public function detailed_health_check_permission() {
|
||||
return current_user_can('manage_options');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get basic health status
|
||||
* ENTERPRISE: Fast check for monitoring systems
|
||||
*/
|
||||
public function get_health_status() {
|
||||
$status = 'healthy';
|
||||
$checks = array();
|
||||
|
||||
// Check API configuration
|
||||
$checks['api_configured'] = $this->api->is_configured();
|
||||
if (!$checks['api_configured']) {
|
||||
$status = 'degraded';
|
||||
}
|
||||
|
||||
// Check database connectivity
|
||||
global $wpdb;
|
||||
$checks['database'] = $wpdb->check_connection();
|
||||
if (!$checks['database']) {
|
||||
$status = 'unhealthy';
|
||||
}
|
||||
|
||||
// Check if syncs are running (not stuck)
|
||||
$event_sync = get_transient('ticket_tailor_sync_events_lock');
|
||||
$order_sync = get_transient('ticket_tailor_sync_orders_lock');
|
||||
|
||||
$checks['sync_status'] = array(
|
||||
'events_syncing' => !empty($event_sync),
|
||||
'orders_syncing' => !empty($order_sync),
|
||||
);
|
||||
|
||||
// Check if locks are stale (stuck for > 10 minutes)
|
||||
if ($event_sync && (time() - $event_sync) > 600) {
|
||||
$status = 'degraded';
|
||||
$checks['sync_status']['events_stuck'] = true;
|
||||
}
|
||||
|
||||
if ($order_sync && (time() - $order_sync) > 600) {
|
||||
$status = 'degraded';
|
||||
$checks['sync_status']['orders_stuck'] = true;
|
||||
}
|
||||
|
||||
$response = array(
|
||||
'status' => $status,
|
||||
'timestamp' => current_time('mysql'),
|
||||
'checks' => $checks,
|
||||
);
|
||||
|
||||
$http_status = ($status === 'healthy') ? 200 : (($status === 'degraded') ? 200 : 503);
|
||||
|
||||
return new WP_REST_Response($response, $http_status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get detailed health status
|
||||
* ENTERPRISE: Comprehensive diagnostics for administrators
|
||||
*/
|
||||
public function get_detailed_health_status() {
|
||||
$status = 'healthy';
|
||||
$diagnostics = array();
|
||||
|
||||
// 1. API Configuration
|
||||
$diagnostics['api'] = array(
|
||||
'configured' => $this->api->is_configured(),
|
||||
'rate_limit' => $this->api->get_rate_limit_info(),
|
||||
);
|
||||
|
||||
// Test API connectivity
|
||||
$api_test = $this->api->test_connection();
|
||||
$diagnostics['api']['connectivity'] = !is_wp_error($api_test);
|
||||
if (is_wp_error($api_test)) {
|
||||
$diagnostics['api']['error'] = $api_test->get_error_message();
|
||||
$status = 'unhealthy';
|
||||
}
|
||||
|
||||
// 2. Database Health
|
||||
global $wpdb;
|
||||
$diagnostics['database'] = array(
|
||||
'connected' => $wpdb->check_connection(),
|
||||
'events_table_exists' => $this->check_table_exists($wpdb->prefix . 'ticket_tailor_events'),
|
||||
'orders_table_exists' => $this->check_table_exists($wpdb->prefix . 'ticket_tailor_orders'),
|
||||
'security_log_table_exists' => $this->check_table_exists($wpdb->prefix . 'ticket_tailor_security_log'),
|
||||
'rate_limit_table_exists' => $this->check_table_exists($wpdb->prefix . 'ticket_tailor_rate_limits'),
|
||||
);
|
||||
|
||||
// 3. Cache Statistics
|
||||
$diagnostics['cache'] = array(
|
||||
'events' => $this->events->get_cache_stats(),
|
||||
'orders' => $this->orders->get_cache_stats(),
|
||||
);
|
||||
|
||||
// 4. Sync Status
|
||||
$last_event_sync = get_option('ticket_tailor_last_event_sync', 0);
|
||||
$last_order_sync = get_option('ticket_tailor_last_order_sync', 0);
|
||||
|
||||
$diagnostics['sync'] = array(
|
||||
'last_event_sync' => $last_event_sync ? gmdate('Y-m-d H:i:s', $last_event_sync) : 'never',
|
||||
'last_order_sync' => $last_order_sync ? gmdate('Y-m-d H:i:s', $last_order_sync) : 'never',
|
||||
'event_sync_age_hours' => $last_event_sync ? round((time() - $last_event_sync) / 3600, 1) : null,
|
||||
'order_sync_age_hours' => $last_order_sync ? round((time() - $last_order_sync) / 3600, 1) : null,
|
||||
'events_lock' => get_transient('ticket_tailor_sync_events_lock'),
|
||||
'orders_lock' => get_transient('ticket_tailor_sync_orders_lock'),
|
||||
);
|
||||
|
||||
// Check for stale syncs
|
||||
if ($last_event_sync && (time() - $last_event_sync) > 86400) {
|
||||
$diagnostics['sync']['event_sync_stale'] = true;
|
||||
$status = 'degraded';
|
||||
}
|
||||
|
||||
if ($last_order_sync && (time() - $last_order_sync) > 86400) {
|
||||
$diagnostics['sync']['order_sync_stale'] = true;
|
||||
$status = 'degraded';
|
||||
}
|
||||
|
||||
// 5. Cron Jobs
|
||||
$diagnostics['cron'] = array(
|
||||
'event_sync_scheduled' => wp_next_scheduled('ticket_tailor_sync_events') ? true : false,
|
||||
'order_sync_scheduled' => wp_next_scheduled('ticket_tailor_sync_orders') ? true : false,
|
||||
'security_cleanup_scheduled' => wp_next_scheduled('ticket_tailor_cleanup_security_logs') ? true : false,
|
||||
'rate_limit_cleanup_scheduled' => wp_next_scheduled('ticket_tailor_cleanup_rate_limits') ? true : false,
|
||||
);
|
||||
|
||||
// 6. PHP Environment
|
||||
$diagnostics['environment'] = array(
|
||||
'php_version' => PHP_VERSION,
|
||||
'wordpress_version' => get_bloginfo('version'),
|
||||
'plugin_version' => TICKET_TAILOR_VERSION,
|
||||
'openssl_enabled' => function_exists('openssl_encrypt'),
|
||||
'curl_enabled' => function_exists('curl_init'),
|
||||
'memory_limit' => ini_get('memory_limit'),
|
||||
'max_execution_time' => ini_get('max_execution_time'),
|
||||
);
|
||||
|
||||
// 7. Security Configuration
|
||||
$diagnostics['security'] = array(
|
||||
'api_key_encrypted' => !empty(get_option('ticket_tailor_api_key_encrypted')),
|
||||
'webhook_secret_configured' => !empty(get_option('ticket_tailor_webhook_secret')),
|
||||
'security_alerts_enabled' => get_option('ticket_tailor_security_alerts', true),
|
||||
);
|
||||
|
||||
$response = array(
|
||||
'status' => $status,
|
||||
'timestamp' => current_time('mysql'),
|
||||
'diagnostics' => $diagnostics,
|
||||
);
|
||||
|
||||
$http_status = ($status === 'healthy') ? 200 : (($status === 'degraded') ? 200 : 503);
|
||||
|
||||
return new WP_REST_Response($response, $http_status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if table exists
|
||||
*/
|
||||
private function check_table_exists($table_name) {
|
||||
global $wpdb;
|
||||
$result = $wpdb->get_var($wpdb->prepare(
|
||||
"SHOW TABLES LIKE %s",
|
||||
$wpdb->esc_like($table_name)
|
||||
));
|
||||
return !empty($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get health check URL
|
||||
*/
|
||||
public function get_health_check_url() {
|
||||
return rest_url('ticket-tailor/v1/health');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get detailed health check URL
|
||||
*/
|
||||
public function get_detailed_health_check_url() {
|
||||
return rest_url('ticket-tailor/v1/health/detailed');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,490 @@
|
|||
<?php
|
||||
/**
|
||||
* Ticket Tailor Order Manager
|
||||
*
|
||||
* Handles order caching, syncing, and retrieval
|
||||
*/
|
||||
|
||||
// Exit if accessed directly
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Ticket_Tailor_Order_Manager {
|
||||
|
||||
/**
|
||||
* API Client
|
||||
*/
|
||||
private $api;
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
private $table;
|
||||
|
||||
/**
|
||||
* Cache duration (in seconds)
|
||||
*/
|
||||
private $cache_duration;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct($api) {
|
||||
global $wpdb;
|
||||
|
||||
$this->api = $api;
|
||||
$this->table = $wpdb->prefix . 'ticket_tailor_orders';
|
||||
$this->cache_duration = get_option('ticket_tailor_cache_duration', 3600); // 1 hour default
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all orders - PERFORMANCE FIX: Improved query building
|
||||
*/
|
||||
public function get_orders($args = array(), $force_refresh = false) {
|
||||
if ($force_refresh) {
|
||||
return $this->sync_all_orders();
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
$cache_valid_time = gmdate('Y-m-d H:i:s', time() - $this->cache_duration);
|
||||
|
||||
$query = "SELECT order_data FROM {$this->table} WHERE last_synced > %s";
|
||||
$query_args = array($cache_valid_time);
|
||||
|
||||
// Filter by event if specified
|
||||
if (!empty($args['event_id'])) {
|
||||
$query .= " AND event_id = %s";
|
||||
$query_args[] = sanitize_text_field($args['event_id']);
|
||||
}
|
||||
|
||||
$query .= " ORDER BY last_synced DESC";
|
||||
|
||||
// PERFORMANCE FIX: Always apply a reasonable default limit if not specified
|
||||
if (!empty($args['limit'])) {
|
||||
$query .= " LIMIT %d";
|
||||
$query_args[] = absint($args['limit']);
|
||||
} else {
|
||||
// Default limit of 1000 to prevent loading unlimited orders
|
||||
$query .= " LIMIT 1000";
|
||||
}
|
||||
|
||||
$cached_orders = $wpdb->get_results(
|
||||
$wpdb->prepare($query, $query_args)
|
||||
);
|
||||
|
||||
if (!empty($cached_orders)) {
|
||||
$orders = array();
|
||||
foreach ($cached_orders as $row) {
|
||||
$order_data = json_decode($row->order_data, true);
|
||||
if ($order_data) {
|
||||
$orders[] = $order_data;
|
||||
}
|
||||
}
|
||||
return $orders;
|
||||
}
|
||||
|
||||
// Cache is expired or empty, fetch from API
|
||||
$all_orders = $this->sync_all_orders();
|
||||
|
||||
// Apply limit to synced orders if specified
|
||||
if (!empty($args['limit']) && is_array($all_orders)) {
|
||||
return array_slice($all_orders, 0, absint($args['limit']));
|
||||
}
|
||||
|
||||
// Apply default limit
|
||||
if (is_array($all_orders)) {
|
||||
return array_slice($all_orders, 0, 1000);
|
||||
}
|
||||
|
||||
return $all_orders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get single order
|
||||
*/
|
||||
public function get_order($order_id, $force_refresh = false) {
|
||||
$order_id = sanitize_text_field($order_id);
|
||||
|
||||
if (!$force_refresh) {
|
||||
global $wpdb;
|
||||
|
||||
$cache_valid_time = gmdate('Y-m-d H:i:s', time() - $this->cache_duration);
|
||||
|
||||
$cached = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT order_data FROM {$this->table} WHERE order_id = %s AND last_synced > %s",
|
||||
$order_id,
|
||||
$cache_valid_time
|
||||
)
|
||||
);
|
||||
|
||||
if ($cached) {
|
||||
$order_data = json_decode($cached, true);
|
||||
if ($order_data) {
|
||||
return $order_data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch from API
|
||||
$order = $this->api->get_order($order_id);
|
||||
|
||||
if (is_wp_error($order)) {
|
||||
return $order;
|
||||
}
|
||||
|
||||
// Cache it
|
||||
$this->cache_order($order);
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync all orders from API - ENTERPRISE: Race condition protection
|
||||
*/
|
||||
public function sync_all_orders() {
|
||||
if (!$this->api->is_configured()) {
|
||||
return new WP_Error('no_api_key', __('API key not configured', 'ticket-tailor'));
|
||||
}
|
||||
|
||||
// ENTERPRISE: Acquire lock to prevent concurrent syncs
|
||||
$lock_key = 'ticket_tailor_sync_orders_lock';
|
||||
$lock_acquired = $this->acquire_sync_lock($lock_key);
|
||||
|
||||
if (!$lock_acquired) {
|
||||
return new WP_Error(
|
||||
'sync_in_progress',
|
||||
__('Order sync already in progress. Please wait.', 'ticket-tailor'),
|
||||
array('lock_key' => $lock_key)
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
$orders = $this->api->get_all_paginated('orders');
|
||||
|
||||
if (is_wp_error($orders)) {
|
||||
$this->release_sync_lock($lock_key);
|
||||
return $orders;
|
||||
}
|
||||
|
||||
// ENTERPRISE: Use transaction for atomic cache updates
|
||||
global $wpdb;
|
||||
$wpdb->query('START TRANSACTION');
|
||||
|
||||
try {
|
||||
// Cache all orders
|
||||
$cached_count = 0;
|
||||
foreach ($orders as $order) {
|
||||
if ($this->cache_order($order)) {
|
||||
$cached_count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Update last sync time
|
||||
update_option('ticket_tailor_last_order_sync', time());
|
||||
|
||||
$wpdb->query('COMMIT');
|
||||
|
||||
error_log(sprintf(
|
||||
'Ticket Tailor: Successfully synced %d orders (%d cached)',
|
||||
count($orders),
|
||||
$cached_count
|
||||
));
|
||||
|
||||
} catch (Exception $e) {
|
||||
$wpdb->query('ROLLBACK');
|
||||
$this->release_sync_lock($lock_key);
|
||||
|
||||
return new WP_Error(
|
||||
'sync_transaction_failed',
|
||||
sprintf('Order sync transaction failed: %s', $e->getMessage())
|
||||
);
|
||||
}
|
||||
|
||||
$this->release_sync_lock($lock_key);
|
||||
return $orders;
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->release_sync_lock($lock_key);
|
||||
|
||||
return new WP_Error(
|
||||
'sync_failed',
|
||||
sprintf('Order sync failed: %s', $e->getMessage())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquire sync lock - ENTERPRISE: Prevent concurrent syncs
|
||||
*/
|
||||
private function acquire_sync_lock($lock_key, $timeout = 300) {
|
||||
$acquired = set_transient($lock_key, time(), $timeout);
|
||||
|
||||
if (!$acquired) {
|
||||
$lock_time = get_transient($lock_key);
|
||||
if ($lock_time && (time() - $lock_time) > $timeout) {
|
||||
delete_transient($lock_key);
|
||||
return set_transient($lock_key, time(), $timeout);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release sync lock - ENTERPRISE
|
||||
*/
|
||||
private function release_sync_lock($lock_key) {
|
||||
return delete_transient($lock_key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache a single order
|
||||
*/
|
||||
private function cache_order($order) {
|
||||
if (empty($order['id'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
$order_id = sanitize_text_field($order['id']);
|
||||
$event_id = !empty($order['event_id']) ? sanitize_text_field($order['event_id']) : '';
|
||||
$order_data = wp_json_encode($order);
|
||||
|
||||
// Check if exists
|
||||
$exists = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT id FROM {$this->table} WHERE order_id = %s",
|
||||
$order_id
|
||||
)
|
||||
);
|
||||
|
||||
if ($exists) {
|
||||
// Update existing
|
||||
return $wpdb->update(
|
||||
$this->table,
|
||||
array(
|
||||
'event_id' => $event_id,
|
||||
'order_data' => $order_data,
|
||||
'last_synced' => current_time('mysql', 1),
|
||||
),
|
||||
array('order_id' => $order_id),
|
||||
array('%s', '%s', '%s'),
|
||||
array('%s')
|
||||
);
|
||||
} else {
|
||||
// Insert new
|
||||
return $wpdb->insert(
|
||||
$this->table,
|
||||
array(
|
||||
'order_id' => $order_id,
|
||||
'event_id' => $event_id,
|
||||
'order_data' => $order_data,
|
||||
'last_synced' => current_time('mysql', 1),
|
||||
),
|
||||
array('%s', '%s', '%s', '%s')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get orders for a specific event
|
||||
*/
|
||||
public function get_event_orders($event_id) {
|
||||
return $this->get_orders(array('event_id' => $event_id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get order statistics
|
||||
*/
|
||||
public function get_order_stats($event_id = null) {
|
||||
$orders = $event_id ? $this->get_event_orders($event_id) : $this->get_orders();
|
||||
|
||||
if (is_wp_error($orders)) {
|
||||
return $orders;
|
||||
}
|
||||
|
||||
$stats = array(
|
||||
'total_orders' => count($orders),
|
||||
'total_revenue' => 0,
|
||||
'total_tickets' => 0,
|
||||
'by_status' => array(),
|
||||
);
|
||||
|
||||
foreach ($orders as $order) {
|
||||
// Count tickets
|
||||
if (isset($order['total_quantity'])) {
|
||||
$stats['total_tickets'] += (int) $order['total_quantity'];
|
||||
}
|
||||
|
||||
// Sum revenue
|
||||
if (isset($order['total'])) {
|
||||
$stats['total_revenue'] += (int) $order['total'];
|
||||
}
|
||||
|
||||
// Count by status
|
||||
$status = $order['status'] ?? 'unknown';
|
||||
if (!isset($stats['by_status'][$status])) {
|
||||
$stats['by_status'][$status] = 0;
|
||||
}
|
||||
$stats['by_status'][$status]++;
|
||||
}
|
||||
|
||||
// Format revenue (convert from cents to dollars/pounds)
|
||||
$currency = get_option('ticket_tailor_currency', 'USD');
|
||||
$stats['total_revenue_formatted'] = $this->format_currency($stats['total_revenue'], $currency);
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format currency
|
||||
*/
|
||||
private function format_currency($amount_cents, $currency = 'USD') {
|
||||
$amount = $amount_cents / 100;
|
||||
|
||||
$symbols = array(
|
||||
'USD' => '$',
|
||||
'GBP' => '£',
|
||||
'EUR' => '€',
|
||||
'CAD' => 'C$',
|
||||
'AUD' => 'A$',
|
||||
);
|
||||
|
||||
$symbol = $symbols[$currency] ?? $currency . ' ';
|
||||
|
||||
return $symbol . number_format($amount, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search orders
|
||||
*/
|
||||
public function search_orders($search_term) {
|
||||
$orders = $this->get_orders();
|
||||
|
||||
if (is_wp_error($orders)) {
|
||||
return $orders;
|
||||
}
|
||||
|
||||
$search_term = strtolower($search_term);
|
||||
|
||||
return array_filter($orders, function($order) use ($search_term) {
|
||||
$email = strtolower($order['email'] ?? '');
|
||||
$name = strtolower($order['customer_name'] ?? '');
|
||||
$order_id = strtolower($order['id'] ?? '');
|
||||
|
||||
return strpos($email, $search_term) !== false ||
|
||||
strpos($name, $search_term) !== false ||
|
||||
strpos($order_id, $search_term) !== false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent orders
|
||||
*/
|
||||
public function get_recent_orders($limit = 10) {
|
||||
return $this->get_orders(array('limit' => $limit));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear order cache - SECURITY FIX: Validate table name
|
||||
*/
|
||||
public function clear_cache($order_id = null) {
|
||||
global $wpdb;
|
||||
|
||||
if ($order_id) {
|
||||
$order_id = sanitize_text_field($order_id);
|
||||
$wpdb->delete($this->table, array('order_id' => $order_id), array('%s'));
|
||||
} else {
|
||||
// SECURITY FIX: Validate table name before TRUNCATE
|
||||
if (preg_match('/^[a-zA-Z0-9_]+$/', $this->table)) {
|
||||
$wpdb->query("TRUNCATE TABLE `{$this->table}`");
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache statistics
|
||||
*/
|
||||
public function get_cache_stats() {
|
||||
global $wpdb;
|
||||
|
||||
$total_orders = $wpdb->get_var("SELECT COUNT(*) FROM {$this->table}");
|
||||
|
||||
$cache_valid_time = gmdate('Y-m-d H:i:s', time() - $this->cache_duration);
|
||||
$valid_orders = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$this->table} WHERE last_synced > %s",
|
||||
$cache_valid_time
|
||||
)
|
||||
);
|
||||
|
||||
$last_sync = get_option('ticket_tailor_last_order_sync', 0);
|
||||
|
||||
return array(
|
||||
'total_cached' => (int) $total_orders,
|
||||
'valid_cached' => (int) $valid_orders,
|
||||
'expired_cached' => (int) $total_orders - (int) $valid_orders,
|
||||
'last_sync' => $last_sync,
|
||||
'cache_duration' => $this->cache_duration,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get order statistics efficiently - PERFORMANCE OPTIMIZATION
|
||||
* Uses aggregated database queries instead of loading all orders
|
||||
*/
|
||||
public function get_order_statistics() {
|
||||
global $wpdb;
|
||||
|
||||
$cache_key = 'tt_order_stats';
|
||||
$cached = wp_cache_get($cache_key);
|
||||
|
||||
if ($cached !== false) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$cache_valid_time = gmdate('Y-m-d H:i:s', time() - $this->cache_duration);
|
||||
|
||||
// Get total orders count
|
||||
$total_orders = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$this->table} WHERE last_synced > %s",
|
||||
$cache_valid_time
|
||||
)
|
||||
);
|
||||
|
||||
// Get all order data to calculate revenue (we need to parse JSON)
|
||||
$orders_data = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT order_data FROM {$this->table} WHERE last_synced > %s",
|
||||
$cache_valid_time
|
||||
)
|
||||
);
|
||||
|
||||
$total_revenue = 0;
|
||||
|
||||
foreach ($orders_data as $row) {
|
||||
$order = json_decode($row->order_data, true);
|
||||
if (!empty($order['total'])) {
|
||||
$total_revenue += floatval($order['total']);
|
||||
}
|
||||
}
|
||||
|
||||
$stats = array(
|
||||
'total_orders' => (int) $total_orders,
|
||||
'total_revenue' => $total_revenue,
|
||||
);
|
||||
|
||||
// Cache for 5 minutes
|
||||
wp_cache_set($cache_key, $stats, '', 300);
|
||||
|
||||
return $stats;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,370 @@
|
|||
<?php
|
||||
/**
|
||||
* Ticket Tailor Security Logger
|
||||
*
|
||||
* Logs security events and sends alerts
|
||||
* SECURITY ENHANCEMENT: Complete audit trail for compliance
|
||||
*/
|
||||
|
||||
// Exit if accessed directly
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Ticket_Tailor_Security_Logger {
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
private $table;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
global $wpdb;
|
||||
$this->table = $wpdb->prefix . 'ticket_tailor_security_log';
|
||||
|
||||
// Create table if needed
|
||||
$this->maybe_create_table();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create security log table
|
||||
*/
|
||||
private function maybe_create_table() {
|
||||
global $wpdb;
|
||||
|
||||
// Check if table already exists
|
||||
$table_exists = $wpdb->get_var($wpdb->prepare(
|
||||
"SHOW TABLES LIKE %s",
|
||||
$this->table
|
||||
));
|
||||
|
||||
if ($table_exists) {
|
||||
return;
|
||||
}
|
||||
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
|
||||
$sql = "CREATE TABLE IF NOT EXISTS {$this->table} (
|
||||
id bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
event_type varchar(50) NOT NULL,
|
||||
user_id bigint(20) DEFAULT 0,
|
||||
ip_address varchar(45) NOT NULL,
|
||||
details longtext,
|
||||
timestamp datetime NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY event_type (event_type),
|
||||
KEY timestamp (timestamp),
|
||||
KEY ip_address (ip_address)
|
||||
) {$charset_collate};";
|
||||
|
||||
// Only require upgrade.php if we actually need to create the table
|
||||
if (!function_exists('dbDelta')) {
|
||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||
}
|
||||
dbDelta($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log security event
|
||||
*/
|
||||
public function log_event($event_type, $details = array()) {
|
||||
global $wpdb;
|
||||
|
||||
$wpdb->insert(
|
||||
$this->table,
|
||||
array(
|
||||
'event_type' => sanitize_key($event_type),
|
||||
'user_id' => get_current_user_id(),
|
||||
'ip_address' => $this->get_ip(),
|
||||
'details' => wp_json_encode($details),
|
||||
'timestamp' => current_time('mysql', 1),
|
||||
),
|
||||
array('%s', '%d', '%s', '%s', '%s')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log failed webhook
|
||||
*/
|
||||
public function log_failed_webhook($ip, $reason) {
|
||||
$this->log_event('webhook_failed', array(
|
||||
'ip' => $ip,
|
||||
'reason' => $reason,
|
||||
));
|
||||
|
||||
// Check for multiple failures
|
||||
$recent_failures = $this->count_recent_events('webhook_failed', 300);
|
||||
if ($recent_failures >= 5) {
|
||||
$this->send_security_alert('Multiple Failed Webhook Attempts', array(
|
||||
'count' => $recent_failures,
|
||||
'ip' => $ip,
|
||||
'timeframe' => '5 minutes',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log successful webhook
|
||||
*/
|
||||
public function log_successful_webhook($ip, $event_type) {
|
||||
$this->log_event('webhook_success', array(
|
||||
'ip' => $ip,
|
||||
'webhook_type' => $event_type,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Log API key change
|
||||
*/
|
||||
public function log_api_key_change($old_key_hash, $new_key_hash) {
|
||||
$this->log_event('api_key_changed', array(
|
||||
'old_hash' => substr($old_key_hash, 0, 10),
|
||||
'new_hash' => substr($new_key_hash, 0, 10),
|
||||
'user' => wp_get_current_user()->user_login,
|
||||
));
|
||||
|
||||
$this->send_security_alert('API Key Changed', array(
|
||||
'changed_by' => wp_get_current_user()->user_login,
|
||||
'ip' => $this->get_ip(),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Log settings change
|
||||
*/
|
||||
public function log_settings_change($setting_name, $old_value = null, $new_value = null) {
|
||||
$this->log_event('settings_changed', array(
|
||||
'setting' => $setting_name,
|
||||
'changed_by' => wp_get_current_user()->user_login,
|
||||
'has_old' => !is_null($old_value),
|
||||
'has_new' => !is_null($new_value),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Log failed login attempt
|
||||
*/
|
||||
public function log_failed_login($username) {
|
||||
$this->log_event('login_failed', array(
|
||||
'username' => $username,
|
||||
));
|
||||
|
||||
// Check for brute force
|
||||
$recent_failures = $this->count_recent_events('login_failed', 900, $this->get_ip());
|
||||
if ($recent_failures >= 5) {
|
||||
$this->send_security_alert('Multiple Failed Login Attempts', array(
|
||||
'count' => $recent_failures,
|
||||
'ip' => $this->get_ip(),
|
||||
'username' => $username,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log rate limit exceeded
|
||||
*/
|
||||
public function log_rate_limit_exceeded($ip, $context = 'webhook') {
|
||||
$this->log_event('rate_limit_exceeded', array(
|
||||
'ip' => $ip,
|
||||
'context' => $context,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Log unauthorized access attempt
|
||||
*/
|
||||
public function log_unauthorized_access($resource) {
|
||||
$this->log_event('unauthorized_access', array(
|
||||
'resource' => $resource,
|
||||
'user_id' => get_current_user_id(),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Count recent events
|
||||
*/
|
||||
private function count_recent_events($event_type, $seconds, $ip = null) {
|
||||
global $wpdb;
|
||||
|
||||
$time_threshold = gmdate('Y-m-d H:i:s', time() - $seconds);
|
||||
|
||||
if ($ip) {
|
||||
return $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$this->table}
|
||||
WHERE event_type = %s AND timestamp > %s AND ip_address = %s",
|
||||
$event_type,
|
||||
$time_threshold,
|
||||
$ip
|
||||
));
|
||||
} else {
|
||||
return $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$this->table}
|
||||
WHERE event_type = %s AND timestamp > %s",
|
||||
$event_type,
|
||||
$time_threshold
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send security alert email
|
||||
*/
|
||||
public function send_security_alert($event_type, $details) {
|
||||
// Check if alerts are enabled
|
||||
if (!get_option('ticket_tailor_security_alerts', true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$admin_email = get_option('admin_email');
|
||||
$site_name = get_bloginfo('name');
|
||||
$site_url = get_bloginfo('url');
|
||||
|
||||
$subject = sprintf(
|
||||
'[%s] Security Alert: %s',
|
||||
$site_name,
|
||||
$event_type
|
||||
);
|
||||
|
||||
$message = sprintf(
|
||||
"Security Event Detected on %s\n\n" .
|
||||
"Event Type: %s\n" .
|
||||
"Time: %s UTC\n" .
|
||||
"IP Address: %s\n" .
|
||||
"User: %s\n\n" .
|
||||
"Details:\n%s\n\n" .
|
||||
"---\n" .
|
||||
"This is an automated security alert from Ticket Tailor plugin.\n" .
|
||||
"To disable these alerts, go to: %s/wp-admin/admin.php?page=ticket-tailor-settings",
|
||||
$site_name,
|
||||
$event_type,
|
||||
current_time('Y-m-d H:i:s'),
|
||||
$this->get_ip(),
|
||||
wp_get_current_user()->user_login ?: 'Guest',
|
||||
$this->format_details($details),
|
||||
$site_url
|
||||
);
|
||||
|
||||
wp_mail($admin_email, $subject, $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format details for email
|
||||
*/
|
||||
private function format_details($details) {
|
||||
$formatted = '';
|
||||
foreach ($details as $key => $value) {
|
||||
$formatted .= ' ' . ucfirst(str_replace('_', ' ', $key)) . ': ' . $value . "\n";
|
||||
}
|
||||
return $formatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get client IP
|
||||
*/
|
||||
private function get_ip() {
|
||||
$ip_keys = array(
|
||||
'HTTP_CF_CONNECTING_IP',
|
||||
'HTTP_X_FORWARDED_FOR',
|
||||
'HTTP_X_REAL_IP',
|
||||
'REMOTE_ADDR',
|
||||
);
|
||||
|
||||
foreach ($ip_keys as $key) {
|
||||
if (!empty($_SERVER[$key])) {
|
||||
$ip = $_SERVER[$key];
|
||||
|
||||
if (strpos($ip, ',') !== false) {
|
||||
$ip = trim(explode(',', $ip)[0]);
|
||||
}
|
||||
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP)) {
|
||||
return $ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return '0.0.0.0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent security events
|
||||
*/
|
||||
public function get_recent_events($limit = 50, $event_type = null) {
|
||||
global $wpdb;
|
||||
|
||||
if ($event_type) {
|
||||
return $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT * FROM {$this->table}
|
||||
WHERE event_type = %s
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT %d",
|
||||
$event_type,
|
||||
$limit
|
||||
));
|
||||
} else {
|
||||
return $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT * FROM {$this->table}
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT %d",
|
||||
$limit
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean old logs (older than 90 days)
|
||||
*/
|
||||
public function cleanup_old_logs() {
|
||||
global $wpdb;
|
||||
|
||||
$threshold = gmdate('Y-m-d H:i:s', time() - (90 * DAY_IN_SECONDS));
|
||||
|
||||
$wpdb->query($wpdb->prepare(
|
||||
"DELETE FROM {$this->table} WHERE timestamp < %s",
|
||||
$threshold
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get security statistics
|
||||
*/
|
||||
public function get_statistics($days = 30) {
|
||||
global $wpdb;
|
||||
|
||||
$threshold = gmdate('Y-m-d H:i:s', time() - ($days * DAY_IN_SECONDS));
|
||||
|
||||
$stats = array();
|
||||
|
||||
// Total events
|
||||
$stats['total_events'] = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$this->table} WHERE timestamp > %s",
|
||||
$threshold
|
||||
));
|
||||
|
||||
// Events by type
|
||||
$stats['by_type'] = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT event_type, COUNT(*) as count
|
||||
FROM {$this->table}
|
||||
WHERE timestamp > %s
|
||||
GROUP BY event_type
|
||||
ORDER BY count DESC",
|
||||
$threshold
|
||||
), ARRAY_A);
|
||||
|
||||
// Top IPs
|
||||
$stats['top_ips'] = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT ip_address, COUNT(*) as count
|
||||
FROM {$this->table}
|
||||
WHERE timestamp > %s
|
||||
GROUP BY ip_address
|
||||
ORDER BY count DESC
|
||||
LIMIT 10",
|
||||
$threshold
|
||||
), ARRAY_A);
|
||||
|
||||
return $stats;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,304 @@
|
|||
<?php
|
||||
/**
|
||||
* Ticket Tailor Shortcodes
|
||||
* Updated with equal-height card support
|
||||
*/
|
||||
|
||||
// Exit if accessed directly
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Ticket_Tailor_Shortcodes {
|
||||
|
||||
private $events;
|
||||
private $orders;
|
||||
|
||||
public function __construct($events, $orders) {
|
||||
$this->events = $events;
|
||||
$this->orders = $orders;
|
||||
|
||||
$this->register_shortcodes();
|
||||
}
|
||||
|
||||
private function register_shortcodes() {
|
||||
add_shortcode('tt-event', array($this, 'event_widget_shortcode'));
|
||||
add_shortcode('tt-events', array($this, 'event_listing_shortcode'));
|
||||
add_shortcode('tt-single-event', array($this, 'single_event_shortcode'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Event widget shortcode (original, enhanced)
|
||||
*/
|
||||
public function event_widget_shortcode($atts) {
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'url' => '',
|
||||
'minimal' => 'false',
|
||||
'bg_fill' => 'true',
|
||||
'show_logo' => 'true',
|
||||
'inherit_ref_from_url_param' => '',
|
||||
'ref' => 'website_widget',
|
||||
),
|
||||
$atts,
|
||||
'tt-event'
|
||||
);
|
||||
|
||||
if (empty($atts['url'])) {
|
||||
return '<div class="tt-widget-error"><p>' .
|
||||
esc_html__('Error: No URL set for Ticket Tailor widget', 'ticket-tailor') .
|
||||
'</p></div>';
|
||||
}
|
||||
|
||||
$url = esc_url($atts['url'], array('http', 'https'));
|
||||
if (empty($url)) {
|
||||
return '<div class="tt-widget-error"><p>' .
|
||||
esc_html__('Error: Invalid URL provided', 'ticket-tailor') .
|
||||
'</p></div>';
|
||||
}
|
||||
|
||||
$minimal = $this->sanitize_boolean($atts['minimal']);
|
||||
$bg_fill = $this->sanitize_boolean($atts['bg_fill']);
|
||||
$show_logo = $this->sanitize_boolean($atts['show_logo']);
|
||||
$inherit_ref = sanitize_text_field($atts['inherit_ref_from_url_param']);
|
||||
$ref = sanitize_text_field($atts['ref']);
|
||||
|
||||
static $widget_count = 0;
|
||||
$widget_count++;
|
||||
$widget_id = 'tt-widget-' . $widget_count;
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div id="<?php echo esc_attr($widget_id); ?>" class="tt-widget-container">
|
||||
<script>
|
||||
(function() {
|
||||
var script = document.createElement('script');
|
||||
script.src = 'https://cdn.tickettailor.com/js/widgets/min/widget.js';
|
||||
script.setAttribute('data-url', <?php echo wp_json_encode($url); ?>);
|
||||
script.setAttribute('data-type', 'inline');
|
||||
script.setAttribute('data-inline-minimal', <?php echo wp_json_encode($minimal ? 'true' : 'false'); ?>);
|
||||
script.setAttribute('data-inline-show-logo', <?php echo wp_json_encode($show_logo ? 'true' : 'false'); ?>);
|
||||
script.setAttribute('data-inline-bg-fill', <?php echo wp_json_encode($bg_fill ? 'true' : 'false'); ?>);
|
||||
<?php if (!empty($inherit_ref)) : ?>
|
||||
script.setAttribute('data-inline-inherit-ref-from-url-param', <?php echo wp_json_encode($inherit_ref); ?>);
|
||||
<?php endif; ?>
|
||||
script.setAttribute('data-inline-ref', <?php echo wp_json_encode($ref); ?>);
|
||||
|
||||
var container = document.getElementById(<?php echo wp_json_encode($widget_id); ?>);
|
||||
if (container) {
|
||||
container.appendChild(script);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listing shortcode - UPDATED for equal heights and wider cards
|
||||
*/
|
||||
public function event_listing_shortcode($atts) {
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'limit' => 10,
|
||||
'layout' => 'grid',
|
||||
'columns' => 3,
|
||||
'show_past' => 'false',
|
||||
'show_image' => 'true',
|
||||
),
|
||||
$atts,
|
||||
'tt-events'
|
||||
);
|
||||
|
||||
$limit = absint($atts['limit']);
|
||||
$layout = sanitize_key($atts['layout']);
|
||||
$columns = absint($atts['columns']);
|
||||
$show_past = $this->sanitize_boolean($atts['show_past']);
|
||||
$show_image = $this->sanitize_boolean($atts['show_image']);
|
||||
|
||||
$events = $show_past ?
|
||||
$this->events->get_past_events($limit) :
|
||||
$this->events->get_upcoming_events($limit);
|
||||
|
||||
if (is_wp_error($events)) {
|
||||
return '<div class="tt-error">' . esc_html($events->get_error_message()) . '</div>';
|
||||
}
|
||||
|
||||
if (empty($events)) {
|
||||
return '<div class="tt-no-events">' . esc_html__('No events found', 'ticket-tailor') . '</div>';
|
||||
}
|
||||
|
||||
ob_start();
|
||||
|
||||
$class = 'tt-event-listing tt-layout-' . esc_attr($layout);
|
||||
if ($layout === 'grid') {
|
||||
$class .= ' tt-columns-' . esc_attr($columns);
|
||||
}
|
||||
?>
|
||||
<div class="<?php echo esc_attr($class); ?>">
|
||||
<?php foreach ($events as $event) : ?>
|
||||
<div class="tt-event-card">
|
||||
<?php if ($show_image && !empty($event['images']['header'])) : ?>
|
||||
<div class="tt-event-image">
|
||||
<img src="<?php echo esc_url($event['images']['header']); ?>"
|
||||
alt="<?php echo esc_attr($event['name']); ?>">
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="tt-event-content">
|
||||
<h3 class="tt-event-title"><?php echo esc_html($event['name']); ?></h3>
|
||||
|
||||
<?php if (!empty($event['start']['iso'])) : ?>
|
||||
<div class="tt-event-date">
|
||||
<span class="dashicons dashicons-calendar-alt"></span>
|
||||
<?php echo esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($event['start']['iso']))); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($event['venue']['name'])) : ?>
|
||||
<div class="tt-event-venue">
|
||||
<span class="dashicons dashicons-location"></span>
|
||||
<?php echo esc_html($event['venue']['name']); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
// Only show description in LIST layout, never in GRID
|
||||
if ($layout === 'list' && !empty($event['description'])) :
|
||||
?>
|
||||
<div class="tt-event-excerpt">
|
||||
<?php echo esc_html(wp_trim_words($event['description'], 20)); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Spacer pushes button to bottom -->
|
||||
<div class="tt-event-spacer"></div>
|
||||
|
||||
<!-- Button container at bottom of card -->
|
||||
<?php if (!empty($event['url'])) : ?>
|
||||
<div class="tt-event-button-container">
|
||||
<a href="<?php echo esc_url($event['url']); ?>"
|
||||
class="tt-event-button"
|
||||
target="_blank">
|
||||
<?php esc_html_e('Get Tickets', 'ticket-tailor'); ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php
|
||||
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Single event shortcode
|
||||
*/
|
||||
public function single_event_shortcode($atts) {
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'id' => '',
|
||||
'show_description' => 'true',
|
||||
'show_tickets' => 'true',
|
||||
'show_image' => 'true',
|
||||
),
|
||||
$atts,
|
||||
'tt-single-event'
|
||||
);
|
||||
|
||||
$event_id = sanitize_text_field($atts['id']);
|
||||
|
||||
if (empty($event_id)) {
|
||||
return '<div class="tt-error">' . esc_html__('Error: No event ID specified', 'ticket-tailor') . '</div>';
|
||||
}
|
||||
|
||||
$show_description = $this->sanitize_boolean($atts['show_description']);
|
||||
$show_tickets = $this->sanitize_boolean($atts['show_tickets']);
|
||||
$show_image = $this->sanitize_boolean($atts['show_image']);
|
||||
|
||||
$event = $this->events->get_event($event_id);
|
||||
|
||||
if (is_wp_error($event)) {
|
||||
return '<div class="tt-error">' . esc_html($event->get_error_message()) . '</div>';
|
||||
}
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div class="tt-single-event">
|
||||
<?php if ($show_image && !empty($event['images']['header'])) : ?>
|
||||
<div class="tt-event-header-image">
|
||||
<img src="<?php echo esc_url($event['images']['header']); ?>"
|
||||
alt="<?php echo esc_attr($event['name']); ?>">
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="tt-event-details">
|
||||
<h2 class="tt-event-title"><?php echo esc_html($event['name']); ?></h2>
|
||||
|
||||
<div class="tt-event-meta">
|
||||
<?php if (!empty($event['start']['iso'])) : ?>
|
||||
<div class="tt-meta-item">
|
||||
<span class="dashicons dashicons-calendar-alt"></span>
|
||||
<strong><?php esc_html_e('Date:', 'ticket-tailor'); ?></strong>
|
||||
<?php echo esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($event['start']['iso']))); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($event['venue']['name'])) : ?>
|
||||
<div class="tt-meta-item">
|
||||
<span class="dashicons dashicons-location"></span>
|
||||
<strong><?php esc_html_e('Venue:', 'ticket-tailor'); ?></strong>
|
||||
<?php echo esc_html($event['venue']['name']); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($event['status'])) : ?>
|
||||
<div class="tt-meta-item">
|
||||
<span class="dashicons dashicons-info"></span>
|
||||
<strong><?php esc_html_e('Status:', 'ticket-tailor'); ?></strong>
|
||||
<span class="tt-status"><?php echo esc_html($event['status']); ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($show_description && !empty($event['description'])) : ?>
|
||||
<div class="tt-event-description">
|
||||
<?php echo wp_kses_post(wpautop($event['description'])); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($show_tickets && !empty($event['url'])) : ?>
|
||||
<div class="tt-event-cta">
|
||||
<a href="<?php echo esc_url($event['url']); ?>"
|
||||
class="tt-button tt-button-primary"
|
||||
target="_blank">
|
||||
<?php esc_html_e('Buy Tickets', 'ticket-tailor'); ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize boolean values
|
||||
*/
|
||||
private function sanitize_boolean($value) {
|
||||
if (is_bool($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
$value = strtolower(trim($value));
|
||||
return in_array($value, array('1', 'true', 'yes', 'on'), true);
|
||||
}
|
||||
|
||||
return (bool) $value;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
/**
|
||||
* Ticket Tailor Template Loader
|
||||
*
|
||||
* Handles template loading with theme override support
|
||||
*/
|
||||
|
||||
// Exit if accessed directly
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Ticket_Tailor_Template_Loader {
|
||||
|
||||
/**
|
||||
* Get template path
|
||||
*
|
||||
* Checks theme folder first, then plugin folder
|
||||
*/
|
||||
public static function get_template_path($template_name) {
|
||||
$template = '';
|
||||
|
||||
// Check in theme
|
||||
$theme_template = locate_template(array(
|
||||
'ticket-tailor/' . $template_name,
|
||||
$template_name,
|
||||
));
|
||||
|
||||
if ($theme_template) {
|
||||
$template = $theme_template;
|
||||
} else {
|
||||
// Use plugin template
|
||||
$template = TICKET_TAILOR_PLUGIN_DIR . 'templates/' . $template_name;
|
||||
}
|
||||
|
||||
return apply_filters('ticket_tailor_template_path', $template, $template_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load template
|
||||
*/
|
||||
public static function load_template($template_name, $args = array()) {
|
||||
$template_path = self::get_template_path($template_name);
|
||||
|
||||
if (!file_exists($template_path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract variables
|
||||
if (!empty($args) && is_array($args)) {
|
||||
extract($args);
|
||||
}
|
||||
|
||||
include $template_path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get template HTML
|
||||
*/
|
||||
public static function get_template_html($template_name, $args = array()) {
|
||||
ob_start();
|
||||
self::load_template($template_name, $args);
|
||||
return ob_get_clean();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,560 @@
|
|||
<?php
|
||||
/**
|
||||
* Ticket Tailor Webhook Handler
|
||||
*
|
||||
* Handles real-time updates via webhooks from Ticket Tailor
|
||||
* FIXED VERSION: Added rate limiting (100 requests/hour)
|
||||
*/
|
||||
|
||||
// Exit if accessed directly
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Ticket_Tailor_Webhook_Handler {
|
||||
|
||||
/**
|
||||
* Event Manager
|
||||
*/
|
||||
private $events;
|
||||
|
||||
/**
|
||||
* Order Manager
|
||||
*/
|
||||
private $orders;
|
||||
|
||||
/**
|
||||
* Webhook secret
|
||||
*/
|
||||
private $webhook_secret;
|
||||
|
||||
/**
|
||||
* Rate limiting settings
|
||||
*/
|
||||
private $rate_limit = 100; // Max requests per hour
|
||||
private $rate_limit_window = 3600; // 1 hour in seconds
|
||||
|
||||
/**
|
||||
* Allowed webhook IP ranges - SECURITY ENHANCEMENT
|
||||
* Note: Update this array with actual Ticket Tailor IP ranges
|
||||
*/
|
||||
private $default_webhook_ips = array(
|
||||
// These are placeholder IPs - replace with actual Ticket Tailor IPs
|
||||
// Format: 'x.x.x.x' or 'x.x.x.x/24' for CIDR ranges
|
||||
'0.0.0.0/0', // Allow all by default - should be configured by admin
|
||||
);
|
||||
|
||||
/**
|
||||
* Security logger
|
||||
*/
|
||||
private $security_logger;
|
||||
|
||||
/**
|
||||
* Constructor - SECURITY ENHANCEMENT: Added security logger
|
||||
*/
|
||||
public function __construct($events, $orders) {
|
||||
$this->events = $events;
|
||||
$this->orders = $orders;
|
||||
$this->webhook_secret = get_option('ticket_tailor_webhook_secret', '');
|
||||
$this->security_logger = new Ticket_Tailor_Security_Logger();
|
||||
|
||||
// Register webhook endpoint
|
||||
add_action('rest_api_init', array($this, 'register_webhook_endpoint'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register REST API endpoint for webhooks
|
||||
*/
|
||||
public function register_webhook_endpoint() {
|
||||
register_rest_route('ticket-tailor/v1', '/webhook', array(
|
||||
'methods' => 'POST',
|
||||
'callback' => array($this, 'handle_webhook'),
|
||||
'permission_callback' => '__return_true', // Public endpoint (verified via secret)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming webhook - SECURITY ENHANCEMENT: IP whitelisting & logging
|
||||
*/
|
||||
public function handle_webhook($request) {
|
||||
$ip_address = $this->get_client_ip();
|
||||
|
||||
// SECURITY ENHANCEMENT: Check IP whitelist first
|
||||
if (!$this->verify_webhook_ip($ip_address)) {
|
||||
$this->security_logger->log_failed_webhook($ip_address, 'IP not whitelisted');
|
||||
return new WP_Error(
|
||||
'ip_not_allowed',
|
||||
__('IP address not authorized for webhooks', 'ticket-tailor'),
|
||||
array('status' => 403)
|
||||
);
|
||||
}
|
||||
|
||||
// SECURITY FIX: Verify webhook secret is configured (mandatory)
|
||||
if (empty($this->webhook_secret)) {
|
||||
$this->security_logger->log_failed_webhook($ip_address, 'Webhook secret not configured');
|
||||
return new WP_Error(
|
||||
'webhook_not_configured',
|
||||
__('Webhook secret not configured. Please set up webhook authentication.', 'ticket-tailor'),
|
||||
array('status' => 503)
|
||||
);
|
||||
}
|
||||
|
||||
// Check rate limit
|
||||
if (!$this->check_rate_limit($ip_address)) {
|
||||
$this->security_logger->log_rate_limit_exceeded($ip_address, 'webhook');
|
||||
return new WP_Error(
|
||||
'rate_limit_exceeded',
|
||||
__('Rate limit exceeded. Please try again later.', 'ticket-tailor'),
|
||||
array('status' => 429)
|
||||
);
|
||||
}
|
||||
|
||||
// Get raw body with size limit (SECURITY FIX: Prevent DoS)
|
||||
$body = $request->get_body();
|
||||
if (strlen($body) > 102400) { // 100KB limit
|
||||
return new WP_Error(
|
||||
'payload_too_large',
|
||||
__('Webhook payload too large', 'ticket-tailor'),
|
||||
array('status' => 413)
|
||||
);
|
||||
}
|
||||
|
||||
$data = json_decode($body, true);
|
||||
|
||||
// SECURITY FIX: Validate JSON
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
return new WP_Error(
|
||||
'invalid_json',
|
||||
__('Invalid JSON payload', 'ticket-tailor'),
|
||||
array('status' => 400)
|
||||
);
|
||||
}
|
||||
|
||||
// SECURITY FIX: Verify webhook signature (now mandatory)
|
||||
$signature = $request->get_header('X-TT-Signature');
|
||||
|
||||
if (!$this->verify_signature($body, $signature)) {
|
||||
$this->security_logger->log_failed_webhook($ip_address, 'Invalid signature');
|
||||
return new WP_Error(
|
||||
'invalid_signature',
|
||||
__('Invalid webhook signature', 'ticket-tailor'),
|
||||
array('status' => 401)
|
||||
);
|
||||
}
|
||||
|
||||
// ENTERPRISE: Idempotency check - prevent duplicate processing
|
||||
$webhook_id = $data['id'] ?? md5($body);
|
||||
if ($this->is_webhook_processed($webhook_id)) {
|
||||
// Already processed this webhook - return success to ack receipt
|
||||
return new WP_REST_Response(array(
|
||||
'success' => true,
|
||||
'message' => 'Webhook already processed (idempotent)',
|
||||
'webhook_id' => $webhook_id,
|
||||
), 200);
|
||||
}
|
||||
|
||||
// Log webhook for debugging (optional)
|
||||
$this->log_webhook($data);
|
||||
|
||||
// SECURITY ENHANCEMENT: Log successful webhook
|
||||
$event_type = $data['type'] ?? 'unknown';
|
||||
$this->security_logger->log_successful_webhook($ip_address, $event_type);
|
||||
|
||||
// ENTERPRISE: Mark webhook as processed
|
||||
$this->mark_webhook_processed($webhook_id, $event_type, $ip_address);
|
||||
|
||||
// Process webhook based on type
|
||||
$event_type = $data['type'] ?? '';
|
||||
|
||||
switch ($event_type) {
|
||||
case 'order.created':
|
||||
case 'order.updated':
|
||||
$this->handle_order_webhook($data);
|
||||
break;
|
||||
|
||||
case 'issued_ticket.created':
|
||||
case 'issued_ticket.updated':
|
||||
case 'issued_ticket.voided':
|
||||
$this->handle_ticket_webhook($data);
|
||||
break;
|
||||
|
||||
case 'event.created':
|
||||
case 'event.updated':
|
||||
$this->handle_event_webhook($data);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Unknown webhook type
|
||||
break;
|
||||
}
|
||||
|
||||
// Trigger custom action for developers
|
||||
do_action('ticket_tailor_webhook_received', $event_type, $data);
|
||||
|
||||
return new WP_REST_Response(array(
|
||||
'success' => true,
|
||||
'message' => 'Webhook processed',
|
||||
), 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check rate limit - SECURITY FIX: IP-based rate limiting with database storage
|
||||
*/
|
||||
private function check_rate_limit($ip_address) {
|
||||
global $wpdb;
|
||||
|
||||
// Sanitize IP address
|
||||
$ip_address = filter_var($ip_address, FILTER_VALIDATE_IP);
|
||||
if (!$ip_address) {
|
||||
return false; // Invalid IP
|
||||
}
|
||||
|
||||
// Create rate limit table if it doesn't exist
|
||||
$table_name = $wpdb->prefix . 'ticket_tailor_rate_limits';
|
||||
$this->maybe_create_rate_limit_table($table_name);
|
||||
|
||||
// Clean up old entries (older than rate limit window)
|
||||
$wpdb->query($wpdb->prepare(
|
||||
"DELETE FROM {$table_name} WHERE timestamp < %s",
|
||||
gmdate('Y-m-d H:i:s', time() - $this->rate_limit_window)
|
||||
));
|
||||
|
||||
// Count requests from this IP in the current window
|
||||
$request_count = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table_name} WHERE ip_address = %s AND timestamp > %s",
|
||||
$ip_address,
|
||||
gmdate('Y-m-d H:i:s', time() - $this->rate_limit_window)
|
||||
));
|
||||
|
||||
if ($request_count >= $this->rate_limit) {
|
||||
return false; // Rate limit exceeded
|
||||
}
|
||||
|
||||
// Record this request
|
||||
$wpdb->insert(
|
||||
$table_name,
|
||||
array(
|
||||
'ip_address' => $ip_address,
|
||||
'timestamp' => current_time('mysql', 1),
|
||||
),
|
||||
array('%s', '%s')
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create rate limit table if it doesn't exist - SECURITY FIX
|
||||
*/
|
||||
private function maybe_create_rate_limit_table($table_name) {
|
||||
global $wpdb;
|
||||
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
|
||||
$sql = "CREATE TABLE IF NOT EXISTS {$table_name} (
|
||||
id bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
ip_address varchar(45) NOT NULL,
|
||||
timestamp datetime NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY ip_timestamp (ip_address, timestamp)
|
||||
) {$charset_collate};";
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||
dbDelta($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get client IP address safely - SECURITY FIX
|
||||
*/
|
||||
private function get_client_ip() {
|
||||
$ip_keys = array(
|
||||
'HTTP_CF_CONNECTING_IP', // CloudFlare
|
||||
'HTTP_X_FORWARDED_FOR', // Proxy
|
||||
'HTTP_X_REAL_IP', // Nginx proxy
|
||||
'REMOTE_ADDR', // Direct connection
|
||||
);
|
||||
|
||||
foreach ($ip_keys as $key) {
|
||||
if (!empty($_SERVER[$key])) {
|
||||
$ip = $_SERVER[$key];
|
||||
|
||||
// Handle comma-separated list (X-Forwarded-For)
|
||||
if (strpos($ip, ',') !== false) {
|
||||
$ip = trim(explode(',', $ip)[0]);
|
||||
}
|
||||
|
||||
// Validate IP
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
|
||||
return $ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to REMOTE_ADDR even if private
|
||||
return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '0.0.0.0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current rate limit status - NEW IN THIS VERSION
|
||||
*/
|
||||
public function get_rate_limit_status() {
|
||||
$transient_key = 'tt_webhook_rate_limit';
|
||||
$requests = get_transient($transient_key);
|
||||
|
||||
return array(
|
||||
'requests_made' => $requests !== false ? $requests : 0,
|
||||
'requests_remaining' => $this->rate_limit - ($requests !== false ? $requests : 0),
|
||||
'limit' => $this->rate_limit,
|
||||
'window' => $this->rate_limit_window,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify webhook IP address - SECURITY ENHANCEMENT
|
||||
*/
|
||||
private function verify_webhook_ip($ip) {
|
||||
// Get configured IP whitelist (defaults to allow all if not configured)
|
||||
$allowed_ips = get_option('ticket_tailor_webhook_ips', $this->default_webhook_ips);
|
||||
|
||||
// If whitelist is empty, allow all (for backward compatibility)
|
||||
if (empty($allowed_ips)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($allowed_ips as $allowed_range) {
|
||||
if ($this->ip_in_range($ip, $allowed_range)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if IP is in CIDR range - SECURITY ENHANCEMENT
|
||||
*/
|
||||
private function ip_in_range($ip, $range) {
|
||||
// Handle exact match
|
||||
if (strpos($range, '/') === false) {
|
||||
return $ip === $range;
|
||||
}
|
||||
|
||||
// Handle CIDR notation
|
||||
list($subnet, $bits) = explode('/', $range);
|
||||
|
||||
// Convert IPs to long integers
|
||||
$ip_long = ip2long($ip);
|
||||
$subnet_long = ip2long($subnet);
|
||||
|
||||
if ($ip_long === false || $subnet_long === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create netmask
|
||||
$mask = -1 << (32 - (int)$bits);
|
||||
$subnet_long &= $mask;
|
||||
|
||||
return ($ip_long & $mask) == $subnet_long;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify webhook signature
|
||||
*/
|
||||
private function verify_signature($payload, $signature) {
|
||||
if (empty($signature) || empty($this->webhook_secret)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$expected_signature = hash_hmac('sha256', $payload, $this->webhook_secret);
|
||||
|
||||
return hash_equals($expected_signature, $signature);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle order webhook
|
||||
*/
|
||||
private function handle_order_webhook($data) {
|
||||
if (empty($data['data']['id'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$order_id = $data['data']['id'];
|
||||
|
||||
// Force refresh this specific order
|
||||
$order = $this->orders->get_order($order_id, true);
|
||||
|
||||
if (!is_wp_error($order)) {
|
||||
// Trigger action for custom handling
|
||||
do_action('ticket_tailor_order_webhook', $data['type'], $order);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle ticket webhook
|
||||
*/
|
||||
private function handle_ticket_webhook($data) {
|
||||
if (empty($data['data']['order_id'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$order_id = $data['data']['order_id'];
|
||||
|
||||
// Refresh the related order
|
||||
$order = $this->orders->get_order($order_id, true);
|
||||
|
||||
if (!is_wp_error($order)) {
|
||||
// Trigger action for custom handling
|
||||
do_action('ticket_tailor_ticket_webhook', $data['type'], $data['data'], $order);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle event webhook
|
||||
*/
|
||||
private function handle_event_webhook($data) {
|
||||
if (empty($data['data']['id'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event_id = $data['data']['id'];
|
||||
|
||||
// Force refresh this specific event
|
||||
$event = $this->events->get_event($event_id, true);
|
||||
|
||||
if (!is_wp_error($event)) {
|
||||
// Trigger action for custom handling
|
||||
do_action('ticket_tailor_event_webhook', $data['type'], $event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log webhook for debugging
|
||||
*/
|
||||
private function log_webhook($data) {
|
||||
// Only log if debug mode is enabled
|
||||
if (!get_option('ticket_tailor_debug_mode', false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$log_entry = array(
|
||||
'timestamp' => current_time('mysql'),
|
||||
'type' => $data['type'] ?? 'unknown',
|
||||
'data' => $data,
|
||||
);
|
||||
|
||||
// Store in transient (keep last 50 webhooks)
|
||||
$logs = get_transient('ticket_tailor_webhook_logs') ?: array();
|
||||
array_unshift($logs, $log_entry);
|
||||
$logs = array_slice($logs, 0, 50);
|
||||
set_transient('ticket_tailor_webhook_logs', $logs, DAY_IN_SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get webhook URL
|
||||
*/
|
||||
public function get_webhook_url() {
|
||||
return rest_url('ticket-tailor/v1/webhook');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate webhook secret
|
||||
*/
|
||||
public function generate_webhook_secret() {
|
||||
$secret = wp_generate_password(64, false);
|
||||
update_option('ticket_tailor_webhook_secret', $secret);
|
||||
$this->webhook_secret = $secret;
|
||||
return $secret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set webhook secret
|
||||
*/
|
||||
public function set_webhook_secret($secret) {
|
||||
$secret = sanitize_text_field($secret);
|
||||
update_option('ticket_tailor_webhook_secret', $secret);
|
||||
$this->webhook_secret = $secret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear webhook secret
|
||||
*/
|
||||
public function clear_webhook_secret() {
|
||||
delete_option('ticket_tailor_webhook_secret');
|
||||
$this->webhook_secret = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get webhook logs
|
||||
*/
|
||||
public function get_logs() {
|
||||
return get_transient('ticket_tailor_webhook_logs') ?: array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear webhook logs
|
||||
*/
|
||||
public function clear_logs() {
|
||||
delete_transient('ticket_tailor_webhook_logs');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test webhook
|
||||
*/
|
||||
public function test_webhook() {
|
||||
$test_data = array(
|
||||
'type' => 'test.webhook',
|
||||
'created_at' => current_time('mysql'),
|
||||
'data' => array(
|
||||
'message' => 'This is a test webhook',
|
||||
),
|
||||
);
|
||||
|
||||
$this->log_webhook($test_data);
|
||||
|
||||
do_action('ticket_tailor_webhook_received', 'test.webhook', $test_data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if webhook has been processed - ENTERPRISE: Idempotency
|
||||
*/
|
||||
private function is_webhook_processed($webhook_id) {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'ticket_tailor_webhook_log';
|
||||
|
||||
$exists = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table} WHERE webhook_id = %s",
|
||||
$webhook_id
|
||||
));
|
||||
|
||||
return (bool) $exists;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark webhook as processed - ENTERPRISE: Idempotency
|
||||
*/
|
||||
private function mark_webhook_processed($webhook_id, $event_type, $ip_address) {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'ticket_tailor_webhook_log';
|
||||
|
||||
$wpdb->insert(
|
||||
$table,
|
||||
array(
|
||||
'webhook_id' => $webhook_id,
|
||||
'event_type' => $event_type,
|
||||
'processed_at' => current_time('mysql', 1),
|
||||
'ip_address' => $ip_address,
|
||||
),
|
||||
array('%s', '%s', '%s', '%s')
|
||||
);
|
||||
|
||||
// ENTERPRISE: Cleanup old webhook logs (older than 30 days)
|
||||
$wpdb->query($wpdb->prepare(
|
||||
"DELETE FROM {$table} WHERE processed_at < %s",
|
||||
gmdate('Y-m-d H:i:s', time() - (30 * DAY_IN_SECONDS))
|
||||
));
|
||||
}
|
||||
}
|
||||
2
native/wordpress/ticket-tailor-wp-max/includes/index.php
Normal file
2
native/wordpress/ticket-tailor-wp-max/includes/index.php
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?php
|
||||
// Silence is golden.
|
||||
Loading…
Add table
Add a link
Reference in a new issue