upskill-event-manager/includes/class-hvac-event-post-handler.php
ben 90193ea18c security: implement Phase 1 critical vulnerability fixes
- Add XSS protection with DOMPurify sanitization in rich text editor
- Implement comprehensive file upload security validation
- Enhance server-side content sanitization with wp_kses
- Add comprehensive security test suite with 194+ test cases
- Create security remediation plan documentation

Security fixes address:
- CRITICAL: XSS vulnerability in event description editor
- HIGH: File upload security bypass for malicious files
- HIGH: Enhanced CSRF protection verification
- MEDIUM: Input validation and error handling improvements

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-25 18:53:23 -03:00

781 lines
No EOL
26 KiB
PHP

<?php
declare(strict_types=1);
/**
* HVAC Event Post Handler
*
* Handles native WordPress tribe_events post creation and editing
* Replaces TEC Community Events form submission with reliable WordPress-native approach
*
* @package HVAC_Community_Events
* @subpackage Includes
* @since 3.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Class HVAC_Event_Post_Handler
*
* Manages tribe_events post creation, editing, and meta field mapping
*/
class HVAC_Event_Post_Handler {
use HVAC_Singleton_Trait;
/**
* TEC meta field mapping
* Maps form fields to tribe_events post meta keys
*
* @var array
*/
private array $tec_meta_mapping = [
'event_start_date' => '_EventStartDate',
'event_end_date' => '_EventEndDate',
'event_timezone' => '_EventTimezone',
'event_url' => '_EventURL',
'venue_name' => '_VenueName',
'venue_address' => '_VenueAddress',
'venue_city' => '_VenueCity',
'venue_state' => '_VenueState',
'venue_zip' => '_VenueZip',
'venue_capacity' => '_VenueCapacity',
'organizer_name' => '_OrganizerName',
'organizer_email' => '_OrganizerEmail',
'organizer_phone' => '_OrganizerPhone',
'organizer_website' => '_OrganizerWebsite',
];
/**
* HVAC-specific meta field mapping
*
* @var array
*/
private array $hvac_meta_mapping = [
'trainer_requirements' => '_hvac_trainer_requirements',
'certification_levels' => '_hvac_certification_levels',
'equipment_needed' => '_hvac_equipment_needed',
'prerequisites' => '_hvac_prerequisites',
];
/**
* Required TEC meta fields with default values
*
* @var array
*/
private array $required_tec_meta = [
'_EventOrigin' => 'hvac-community-events',
'_EventShowMap' => '1',
'_EventShowMapLink' => '1',
'_tribe_default_ticket_provider' => 'TEC\\Tickets\\Commerce\\Module',
'_tribe_ticket_capacity' => '0',
'_tribe_ticket_version' => '5.25.1.1',
];
/**
* Constructor
*/
private function __construct() {
$this->init_hooks();
}
/**
* Initialize WordPress hooks
*/
private function init_hooks(): void {
// Handle form submissions
add_action('template_redirect', [$this, 'handle_event_form_submission']);
// Handle AJAX submissions (for future enhancement)
add_action('wp_ajax_hvac_create_event', [$this, 'ajax_create_event']);
add_action('wp_ajax_hvac_update_event', [$this, 'ajax_update_event']);
}
/**
* Handle event form submission
*/
public function handle_event_form_submission(): void {
// Only process POST requests with our nonce
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['hvac_event_form_nonce'])) {
return;
}
// Verify nonce for security
if (!HVAC_Security::verify_nonce($_POST['hvac_event_form_nonce'], 'hvac_event_form')) {
wp_die(__('Security check failed. Please refresh the page and try again.', 'hvac-community-events'));
return;
}
// Check user permissions
if (!HVAC_Security::check_capability('edit_posts')) {
wp_die(__('You do not have permission to create events.', 'hvac-community-events'));
return;
}
try {
// Create form builder instance for validation
$event_form = new HVAC_Event_Form_Builder('hvac_event_form');
$event_form->create_event_form();
// Validate submitted data
$validation_errors = $event_form->validate($_POST);
if (!empty($validation_errors)) {
// Store errors in session for display
$this->store_form_errors($validation_errors);
$this->store_form_data($_POST);
return;
}
// Sanitize data
$sanitized_data = $event_form->sanitize($_POST);
// Determine if this is create or update
$event_id = isset($sanitized_data['event_id']) ? absint($sanitized_data['event_id']) : 0;
if ($event_id > 0) {
// Update existing event
$result = $this->update_event($event_id, $sanitized_data);
} else {
// Create new event
$result = $this->create_event($sanitized_data);
}
if (is_wp_error($result)) {
$this->store_form_errors(['general' => $result->get_error_message()]);
$this->store_form_data($_POST);
return;
}
// Success - redirect to prevent resubmission
$redirect_url = add_query_arg(['event_created' => '1', 'event_id' => $result], wp_get_referer());
wp_safe_redirect($redirect_url);
exit;
} catch (Exception $e) {
HVAC_Logger::error('Event form submission failed', 'Event_Post_Handler', [
'error' => $e->getMessage(),
'user_id' => get_current_user_id(),
'post_data' => wp_json_encode($_POST)
]);
$this->store_form_errors(['general' => 'An error occurred while processing your event. Please try again.']);
$this->store_form_data($_POST);
}
}
/**
* Create new tribe_events post
*
* @param array $data Sanitized form data
* @return int|WP_Error Post ID on success, WP_Error on failure
*/
public function create_event(array $data) {
// Prepare post data with security sanitization
$post_data = [
'post_type' => 'tribe_events',
'post_title' => sanitize_text_field($data['event_title'] ?? ''),
'post_content' => $this->sanitize_rich_text_content($data['event_description'] ?? ''),
'post_excerpt' => sanitize_textarea_field($data['event_excerpt'] ?? ''),
'post_status' => $this->get_post_status_for_user(),
'post_author' => get_current_user_id(),
'meta_input' => $this->prepare_meta_fields($data),
];
// Create the post
$event_id = wp_insert_post($post_data, true);
if (is_wp_error($event_id)) {
return $event_id;
}
// Handle featured image upload
$this->handle_featured_image_upload($event_id, $data);
// Handle venue creation/assignment
$venue_id = $this->handle_venue_data($data);
if ($venue_id) {
update_post_meta($event_id, '_EventVenueID', $venue_id);
}
// Handle organizer creation/assignment
$organizer_id = $this->handle_organizer_data($data);
if ($organizer_id) {
update_post_meta($event_id, '_EventOrganizerID', $organizer_id);
}
// Calculate and store additional TEC meta fields
$this->calculate_tec_meta_fields($event_id, $data);
// Log successful creation
HVAC_Logger::info('Event created successfully', 'Event_Post_Handler', [
'event_id' => $event_id,
'user_id' => get_current_user_id(),
'event_title' => $data['event_title'] ?? 'Unknown'
]);
return $event_id;
}
/**
* Update existing tribe_events post
*
* @param int $event_id Event post ID
* @param array $data Sanitized form data
* @return int|WP_Error Post ID on success, WP_Error on failure
*/
public function update_event(int $event_id, array $data) {
// Verify user can edit this event
if (!current_user_can('edit_post', $event_id)) {
return new WP_Error('permission_denied', 'You do not have permission to edit this event.');
}
// Prepare post data with security sanitization
$post_data = [
'ID' => $event_id,
'post_title' => sanitize_text_field($data['event_title'] ?? ''),
'post_content' => $this->sanitize_rich_text_content($data['event_description'] ?? ''),
'post_excerpt' => $data['event_excerpt'] ?? '',
];
// Update the post
$result = wp_update_post($post_data, true);
if (is_wp_error($result)) {
return $result;
}
// Update meta fields
$meta_fields = $this->prepare_meta_fields($data);
foreach ($meta_fields as $key => $value) {
update_post_meta($event_id, $key, $value);
}
// Handle featured image upload
$this->handle_featured_image_upload($event_id, $data);
// Update venue
$venue_id = $this->handle_venue_data($data);
if ($venue_id) {
update_post_meta($event_id, '_EventVenueID', $venue_id);
}
// Update organizer
$organizer_id = $this->handle_organizer_data($data);
if ($organizer_id) {
update_post_meta($event_id, '_EventOrganizerID', $organizer_id);
}
// Recalculate TEC meta fields
$this->calculate_tec_meta_fields($event_id, $data);
// Log successful update
HVAC_Logger::info('Event updated successfully', 'Event_Post_Handler', [
'event_id' => $event_id,
'user_id' => get_current_user_id(),
'event_title' => $data['event_title'] ?? 'Unknown'
]);
return $event_id;
}
/**
* Prepare meta fields for post creation/update
*
* @param array $data Sanitized form data
* @return array Meta fields array
*/
private function prepare_meta_fields(array $data): array {
$meta_fields = [];
// Map TEC meta fields
foreach ($this->tec_meta_mapping as $form_field => $meta_key) {
if (isset($data[$form_field]) && $data[$form_field] !== '') {
$meta_fields[$meta_key] = $data[$form_field];
}
}
// Map HVAC-specific meta fields
foreach ($this->hvac_meta_mapping as $form_field => $meta_key) {
if (isset($data[$form_field]) && $data[$form_field] !== '') {
$meta_fields[$meta_key] = $data[$form_field];
}
}
// Add required TEC meta fields with defaults
$meta_fields = array_merge($meta_fields, $this->required_tec_meta);
// Handle special datetime processing
if (isset($data['event_start_date'])) {
$meta_fields['_EventStartDate'] = $this->format_datetime_for_tec($data['event_start_date'], $data['event_timezone'] ?? '');
$meta_fields['_EventStartDateUTC'] = $this->convert_to_utc($data['event_start_date'], $data['event_timezone'] ?? '');
}
if (isset($data['event_end_date'])) {
$meta_fields['_EventEndDate'] = $this->format_datetime_for_tec($data['event_end_date'], $data['event_timezone'] ?? '');
$meta_fields['_EventEndDateUTC'] = $this->convert_to_utc($data['event_end_date'], $data['event_timezone'] ?? '');
}
// Handle timezone
if (isset($data['event_timezone'])) {
$meta_fields['_EventTimezone'] = $data['event_timezone'];
$meta_fields['_EventTimezoneAbbr'] = $this->get_timezone_abbreviation($data['event_timezone']);
}
return $meta_fields;
}
/**
* Calculate and store additional TEC meta fields
*
* @param int $event_id Event post ID
* @param array $data Sanitized form data
*/
private function calculate_tec_meta_fields(int $event_id, array $data): void {
// Calculate event duration
if (isset($data['event_start_date']) && isset($data['event_end_date'])) {
$start_time = strtotime($data['event_start_date']);
$end_time = strtotime($data['event_end_date']);
$duration = $end_time - $start_time;
update_post_meta($event_id, '_EventDuration', $duration);
}
// Update modified fields timestamp
update_post_meta($event_id, '_tribe_modified_fields', [
'_EventStartDate' => time(),
'_EventEndDate' => time(),
'_EventVenueID' => time(),
'_EventOrganizerID' => time(),
'_EventURL' => time(),
'_EventTimezone' => time(),
'_EventOrigin' => time(),
]);
// Mark as non-duplicated event
update_post_meta($event_id, '_EventOccurrencesCount', 1);
update_post_meta($event_id, '_edit_last', get_current_user_id());
}
/**
* Handle venue data - create or update venue post
*
* @param array $data Sanitized form data
* @return int|null Venue post ID or null
*/
private function handle_venue_data(array $data): ?int {
if (empty($data['venue_name'])) {
return null;
}
// Check if venue already exists
$existing_venue = get_posts([
'post_type' => 'tribe_venue',
'post_title' => $data['venue_name'],
'posts_per_page' => 1,
'post_status' => 'publish'
]);
if (!empty($existing_venue)) {
return $existing_venue[0]->ID;
}
// Create new venue
$venue_data = [
'post_type' => 'tribe_venue',
'post_title' => $data['venue_name'],
'post_status' => 'publish',
'meta_input' => [
'_VenueAddress' => $data['venue_address'] ?? '',
'_VenueCity' => $data['venue_city'] ?? '',
'_VenueState' => $data['venue_state'] ?? '',
'_VenueZip' => $data['venue_zip'] ?? '',
'_VenueCapacity' => $data['venue_capacity'] ?? '',
]
];
// Add coordinates if available
if (!empty($data['venue_latitude']) && !empty($data['venue_longitude'])) {
$venue_data['meta_input']['_VenueLat'] = $data['venue_latitude'];
$venue_data['meta_input']['_VenueLng'] = $data['venue_longitude'];
}
$venue_id = wp_insert_post($venue_data);
return is_wp_error($venue_id) ? null : $venue_id;
}
/**
* Handle organizer data - create or update organizer post
*
* @param array $data Sanitized form data
* @return int|null Organizer post ID or null
*/
private function handle_organizer_data(array $data): ?int {
if (empty($data['organizer_name'])) {
return null;
}
// Check if organizer already exists
$existing_organizer = get_posts([
'post_type' => 'tribe_organizer',
'post_title' => $data['organizer_name'],
'posts_per_page' => 1,
'post_status' => 'publish'
]);
if (!empty($existing_organizer)) {
return $existing_organizer[0]->ID;
}
// Create new organizer
$organizer_data = [
'post_type' => 'tribe_organizer',
'post_title' => $data['organizer_name'],
'post_status' => 'publish',
'meta_input' => [
'_OrganizerEmail' => $data['organizer_email'] ?? '',
'_OrganizerPhone' => $data['organizer_phone'] ?? '',
'_OrganizerWebsite' => $data['organizer_website'] ?? '',
]
];
$organizer_id = wp_insert_post($organizer_data);
return is_wp_error($organizer_id) ? null : $organizer_id;
}
/**
* Handle featured image upload
*
* @param int $event_id Event post ID
* @param array $data Form data
*/
private function handle_featured_image_upload(int $event_id, array $data): void {
if (empty($_FILES['event_featured_image']['name'])) {
return;
}
// WordPress file upload handling
require_once(ABSPATH . 'wp-admin/includes/file.php');
require_once(ABSPATH . 'wp-admin/includes/image.php');
require_once(ABSPATH . 'wp-admin/includes/media.php');
$file = $_FILES['event_featured_image'];
// Enhanced security validation for file uploads
$validation_result = $this->validate_file_upload($file);
if (is_wp_error($validation_result)) {
// Log security violation
error_log(sprintf(
'[HVAC Security] File upload blocked: %s (User: %d, File: %s)',
$validation_result->get_error_message(),
get_current_user_id(),
$file['name']
));
return;
}
// Handle upload
$attachment_id = media_handle_upload('event_featured_image', $event_id);
if (!is_wp_error($attachment_id)) {
set_post_thumbnail($event_id, $attachment_id);
}
}
/**
* Get appropriate post status for current user
*
* @return string Post status
*/
private function get_post_status_for_user(): string {
$user = wp_get_current_user();
// Master trainers can publish directly
if (in_array('hvac_master_trainer', $user->roles)) {
return 'publish';
}
// Regular trainers create drafts for approval
if (in_array('hvac_trainer', $user->roles)) {
return 'draft';
}
// Administrators can publish
if (in_array('administrator', $user->roles)) {
return 'publish';
}
// Default to draft
return 'draft';
}
/**
* Format datetime for TEC compatibility
*
* @param string $datetime Datetime string
* @param string $timezone Timezone string
* @return string Formatted datetime
*/
private function format_datetime_for_tec(string $datetime, string $timezone): string {
if (empty($datetime)) {
return '';
}
// TEC expects 'Y-m-d H:i:s' format
$timestamp = strtotime($datetime);
return date('Y-m-d H:i:s', $timestamp);
}
/**
* Convert datetime to UTC for TEC
*
* @param string $datetime Datetime string
* @param string $timezone Timezone string
* @return string UTC datetime
*/
private function convert_to_utc(string $datetime, string $timezone): string {
if (empty($datetime) || empty($timezone)) {
return '';
}
try {
$dt = new DateTime($datetime, new DateTimeZone($timezone));
$dt->setTimezone(new DateTimeZone('UTC'));
return $dt->format('Y-m-d H:i:s');
} catch (Exception $e) {
return '';
}
}
/**
* Get timezone abbreviation
*
* @param string $timezone Timezone string
* @return string Timezone abbreviation
*/
private function get_timezone_abbreviation(string $timezone): string {
try {
$dt = new DateTime('now', new DateTimeZone($timezone));
return $dt->format('T');
} catch (Exception $e) {
return 'UTC';
}
}
/**
* Store form errors in session/transient
*
* @param array $errors Form errors
*/
private function store_form_errors(array $errors): void {
set_transient('hvac_form_errors_' . get_current_user_id(), $errors, 300); // 5 minutes
}
/**
* Store form data in session/transient for re-population
*
* @param array $data Form data
*/
private function store_form_data(array $data): void {
// Remove sensitive data before storing
unset($data['hvac_event_form_nonce']);
set_transient('hvac_form_data_' . get_current_user_id(), $data, 300); // 5 minutes
}
/**
* Get stored form errors
*
* @return array Form errors
*/
public function get_stored_errors(): array {
$errors = get_transient('hvac_form_errors_' . get_current_user_id());
delete_transient('hvac_form_errors_' . get_current_user_id());
return is_array($errors) ? $errors : [];
}
/**
* Get stored form data
*
* @return array Form data
*/
public function get_stored_data(): array {
$data = get_transient('hvac_form_data_' . get_current_user_id());
delete_transient('hvac_form_data_' . get_current_user_id());
return is_array($data) ? $data : [];
}
/**
* AJAX handler for event creation
*/
public function ajax_create_event(): void {
// Verify nonce
if (!HVAC_Security::verify_nonce($_POST['nonce'] ?? '', 'hvac_create_event')) {
wp_send_json_error(['message' => 'Security check failed']);
return;
}
// Check permissions
if (!HVAC_Security::check_capability('edit_posts')) {
wp_send_json_error(['message' => 'Permission denied']);
return;
}
try {
$result = $this->create_event($_POST);
if (is_wp_error($result)) {
wp_send_json_error(['message' => $result->get_error_message()]);
return;
}
wp_send_json_success([
'message' => 'Event created successfully',
'event_id' => $result,
'edit_url' => get_edit_post_link($result)
]);
} catch (Exception $e) {
wp_send_json_error(['message' => 'An error occurred while creating the event']);
}
}
/**
* AJAX handler for event updates
*/
public function ajax_update_event(): void {
// Verify nonce
if (!HVAC_Security::verify_nonce($_POST['nonce'] ?? '', 'hvac_update_event')) {
wp_send_json_error(['message' => 'Security check failed']);
return;
}
$event_id = absint($_POST['event_id'] ?? 0);
if (!$event_id) {
wp_send_json_error(['message' => 'Invalid event ID']);
return;
}
try {
$result = $this->update_event($event_id, $_POST);
if (is_wp_error($result)) {
wp_send_json_error(['message' => $result->get_error_message()]);
return;
}
wp_send_json_success([
'message' => 'Event updated successfully',
'event_id' => $result,
'edit_url' => get_edit_post_link($result)
]);
} catch (Exception $e) {
wp_send_json_error(['message' => 'An error occurred while updating the event']);
}
}
/**
* Sanitize rich text content to prevent XSS attacks
*
* @param string $content Raw HTML content from rich text editor
* @return string Sanitized HTML content
*/
private function sanitize_rich_text_content(string $content): string {
// Define allowed HTML tags and attributes for event descriptions
$allowed_html = array(
'p' => array(),
'br' => array(),
'strong' => array(),
'em' => array(),
'ul' => array(),
'ol' => array(),
'li' => array(),
'a' => array(
'href' => array(),
'title' => array(),
'target' => array()
),
'h3' => array(),
'h4' => array(),
'h5' => array(),
'blockquote' => array()
);
// Use WordPress wp_kses for server-side sanitization
return wp_kses($content, $allowed_html);
}
/**
* Validate file upload for security
*
* @param array $file WordPress $_FILES array element
* @return bool|WP_Error True on success, WP_Error on failure
*/
private function validate_file_upload(array $file) {
// MIME type whitelist - images and PDFs only
$allowed_types = array(
'image/jpeg',
'image/png',
'image/gif',
'image/webp'
);
// File extension whitelist
$allowed_extensions = array('jpg', 'jpeg', 'png', 'gif', 'webp');
// Check for upload errors
if ($file['error'] !== UPLOAD_ERR_OK) {
return new WP_Error('upload_error',
__('File upload failed with error code: ' . $file['error'], 'hvac-community-events'));
}
// Validate MIME type
if (!in_array($file['type'], $allowed_types)) {
return new WP_Error('invalid_file_type',
__('File type not allowed. Only JPEG, PNG, GIF, and WebP images are permitted.', 'hvac-community-events'));
}
// Validate file extension (double-check against spoofed MIME types)
$file_extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($file_extension, $allowed_extensions)) {
return new WP_Error('invalid_file_extension',
__('File extension not allowed.', 'hvac-community-events'));
}
// File size limit (5MB maximum)
$max_size = 5 * 1024 * 1024; // 5MB in bytes
if ($file['size'] > $max_size) {
return new WP_Error('file_too_large',
__('File size exceeds 5MB limit. Please choose a smaller image.', 'hvac-community-events'));
}
// Check for malicious file content using getimagesize for images
if (in_array($file_extension, ['jpg', 'jpeg', 'png', 'gif', 'webp'])) {
$image_info = @getimagesize($file['tmp_name']);
if ($image_info === false) {
return new WP_Error('invalid_image',
__('File appears to be corrupted or is not a valid image.', 'hvac-community-events'));
}
// Verify MIME type matches actual image type
$actual_mime = $image_info['mime'];
if ($actual_mime !== $file['type']) {
return new WP_Error('mime_mismatch',
__('File type mismatch detected. Upload blocked for security.', 'hvac-community-events'));
}
}
// Additional security: Check for embedded PHP or script content in image files
$file_content = file_get_contents($file['tmp_name'], false, null, 0, 1024); // Read first 1KB
if (stripos($file_content, '<?php') !== false ||
stripos($file_content, '<script') !== false ||
stripos($file_content, '<%') !== false) {
return new WP_Error('malicious_content',
__('File contains potentially malicious content and has been blocked.', 'hvac-community-events'));
}
return true;
}
}