added additional plugins

This commit is contained in:
Rodolfo Martinez 2025-12-12 19:05:48 -05:00
parent c85895d306
commit 00e60ec1b7
132 changed files with 27514 additions and 0 deletions

File diff suppressed because it is too large Load diff

View file

@ -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');
}
}

View 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();
}
}

View file

@ -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;
}
}

View file

@ -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');
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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))
));
}
}

View file

@ -0,0 +1,2 @@
<?php
// Silence is golden.