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

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