- 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>
676 lines
No EOL
24 KiB
JavaScript
676 lines
No EOL
24 KiB
JavaScript
/**
|
|
* HVAC TEC Tickets Interactive JavaScript
|
|
* Handles all UI/UX interactions for the enhanced event form
|
|
*/
|
|
(function($) {
|
|
'use strict';
|
|
|
|
// Global state management
|
|
window.HVACEventForm = {
|
|
selectedOrganizers: [],
|
|
selectedCategories: [],
|
|
selectedVenue: null,
|
|
richTextEditors: {},
|
|
|
|
init: function() {
|
|
this.initRichTextEditor();
|
|
this.initToggleSwitches();
|
|
this.initFeaturedImageUploader();
|
|
this.initSearchableSelectors();
|
|
this.initModalForms();
|
|
this.bindEvents();
|
|
},
|
|
|
|
// Rich Text Editor Functionality
|
|
initRichTextEditor: function() {
|
|
const editorWrapper = document.getElementById('event-description-editor-wrapper');
|
|
if (!editorWrapper) return;
|
|
|
|
const editor = document.getElementById('event-description-editor');
|
|
const hiddenTextarea = document.getElementById('event_description');
|
|
const toolbar = document.getElementById('event-description-toolbar');
|
|
|
|
if (!editor || !hiddenTextarea || !toolbar) return;
|
|
|
|
// Initialize editor
|
|
this.richTextEditors.description = {
|
|
editor: editor,
|
|
textarea: hiddenTextarea,
|
|
toolbar: toolbar
|
|
};
|
|
|
|
// Bind toolbar events
|
|
$(toolbar).on('click', 'button', function(e) {
|
|
e.preventDefault();
|
|
const command = $(this).data('command');
|
|
const value = $(this).data('value') || null;
|
|
|
|
document.execCommand(command, false, value);
|
|
HVACEventForm.updateToolbarState();
|
|
HVACEventForm.syncEditorContent();
|
|
});
|
|
|
|
// Sync content changes
|
|
editor.addEventListener('input', () => {
|
|
this.syncEditorContent();
|
|
});
|
|
|
|
editor.addEventListener('keyup', () => {
|
|
this.updateToolbarState();
|
|
});
|
|
|
|
editor.addEventListener('mouseup', () => {
|
|
this.updateToolbarState();
|
|
});
|
|
|
|
// Load existing content with XSS protection
|
|
if (hiddenTextarea.value) {
|
|
const cleanContent = DOMPurify.sanitize(hiddenTextarea.value, {
|
|
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li', 'a'],
|
|
ALLOWED_ATTR: ['href', 'title'],
|
|
ALLOW_DATA_ATTR: false
|
|
});
|
|
editor.innerHTML = cleanContent;
|
|
}
|
|
},
|
|
|
|
syncEditorContent: function() {
|
|
const editor = this.richTextEditors.description?.editor;
|
|
const textarea = this.richTextEditors.description?.textarea;
|
|
|
|
if (editor && textarea) {
|
|
// Sanitize content before storing in textarea to prevent XSS
|
|
const cleanContent = DOMPurify.sanitize(editor.innerHTML, {
|
|
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li', 'a'],
|
|
ALLOWED_ATTR: ['href', 'title'],
|
|
ALLOW_DATA_ATTR: false
|
|
});
|
|
textarea.value = cleanContent;
|
|
}
|
|
},
|
|
|
|
updateToolbarState: function() {
|
|
const toolbar = this.richTextEditors.description?.toolbar;
|
|
if (!toolbar) return;
|
|
|
|
$(toolbar).find('button').each(function() {
|
|
const command = $(this).data('command');
|
|
if (document.queryCommandState(command)) {
|
|
$(this).addClass('active');
|
|
} else {
|
|
$(this).removeClass('active');
|
|
}
|
|
});
|
|
},
|
|
|
|
// Toggle Switch Functionality
|
|
initToggleSwitches: function() {
|
|
// Virtual Event Toggle
|
|
$(document).on('change', 'input[name="enable_virtual_event"]', function() {
|
|
const isChecked = $(this).is(':checked');
|
|
$('.virtual-event-config').toggle(isChecked);
|
|
});
|
|
|
|
// RSVP Toggle
|
|
$(document).on('change', 'input[name="enable_rsvp"]', function() {
|
|
const isChecked = $(this).is(':checked');
|
|
$('.rsvp-config').toggle(isChecked);
|
|
});
|
|
|
|
// Ticketing Toggle (existing functionality)
|
|
window.hvacToggleTicketFields = function(enabled) {
|
|
$('.ticket-config-field').toggle(enabled);
|
|
};
|
|
},
|
|
|
|
// Featured Image Uploader
|
|
initFeaturedImageUploader: function() {
|
|
const uploadArea = document.getElementById('upload-area');
|
|
const fileInput = document.getElementById('featured-image-file');
|
|
const preview = document.getElementById('featured-image-preview');
|
|
const uploader = document.getElementById('featured-image-uploader');
|
|
const img = document.getElementById('featured-image-img');
|
|
const changeBtn = document.getElementById('change-featured-image');
|
|
const removeBtn = document.getElementById('remove-featured-image');
|
|
|
|
if (!uploadArea || !fileInput) return;
|
|
|
|
// Click to upload
|
|
uploadArea.addEventListener('click', () => {
|
|
fileInput.click();
|
|
});
|
|
|
|
// Drag and drop
|
|
uploadArea.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
uploadArea.classList.add('dragover');
|
|
});
|
|
|
|
uploadArea.addEventListener('dragleave', () => {
|
|
uploadArea.classList.remove('dragover');
|
|
});
|
|
|
|
uploadArea.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
uploadArea.classList.remove('dragover');
|
|
const files = e.dataTransfer.files;
|
|
if (files.length > 0) {
|
|
this.handleImageUpload(files[0]);
|
|
}
|
|
});
|
|
|
|
// File input change
|
|
fileInput.addEventListener('change', (e) => {
|
|
if (e.target.files.length > 0) {
|
|
this.handleImageUpload(e.target.files[0]);
|
|
}
|
|
});
|
|
|
|
// Change image button
|
|
if (changeBtn) {
|
|
changeBtn.addEventListener('click', () => {
|
|
fileInput.click();
|
|
});
|
|
}
|
|
|
|
// Remove image button
|
|
if (removeBtn) {
|
|
removeBtn.addEventListener('click', () => {
|
|
this.removeFeaturedImage();
|
|
});
|
|
}
|
|
},
|
|
|
|
handleImageUpload: function(file) {
|
|
if (!file.type.startsWith('image/')) {
|
|
alert('Please select a valid image file.');
|
|
return;
|
|
}
|
|
|
|
if (file.size > 5 * 1024 * 1024) { // 5MB limit
|
|
alert('Image file size should be less than 5MB.');
|
|
return;
|
|
}
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
const img = document.getElementById('featured-image-img');
|
|
const preview = document.getElementById('featured-image-preview');
|
|
const uploader = document.getElementById('featured-image-uploader');
|
|
const hiddenInput = document.getElementById('featured-image-data');
|
|
|
|
img.src = e.target.result;
|
|
preview.style.display = 'block';
|
|
uploader.style.display = 'none';
|
|
|
|
// Store image data for form submission
|
|
if (hiddenInput) {
|
|
hiddenInput.value = e.target.result;
|
|
}
|
|
};
|
|
reader.readAsDataURL(file);
|
|
},
|
|
|
|
removeFeaturedImage: function() {
|
|
const preview = document.getElementById('featured-image-preview');
|
|
const uploader = document.getElementById('featured-image-uploader');
|
|
const hiddenInput = document.getElementById('featured-image-data');
|
|
const fileInput = document.getElementById('featured-image-file');
|
|
|
|
preview.style.display = 'none';
|
|
uploader.style.display = 'block';
|
|
|
|
if (hiddenInput) hiddenInput.value = '';
|
|
if (fileInput) fileInput.value = '';
|
|
},
|
|
|
|
// Searchable Selector Components
|
|
initSearchableSelectors: function() {
|
|
this.initOrganizerSelector();
|
|
this.initCategoriesSelector();
|
|
this.initVenueSelector();
|
|
},
|
|
|
|
initOrganizerSelector: function() {
|
|
const searchInput = document.getElementById('organizer-search-input');
|
|
const resultsContainer = document.getElementById('organizer-search-results');
|
|
const selectedContainer = document.getElementById('organizer-selected-items');
|
|
const dropdownToggle = searchInput?.parentElement?.querySelector('.dropdown-toggle');
|
|
|
|
if (!searchInput || !resultsContainer) return;
|
|
|
|
// Search input events
|
|
searchInput.addEventListener('focus', () => {
|
|
this.loadOrganizerOptions();
|
|
resultsContainer.style.display = 'block';
|
|
});
|
|
|
|
searchInput.addEventListener('input', (e) => {
|
|
this.filterOrganizerOptions(e.target.value);
|
|
});
|
|
|
|
// Dropdown toggle
|
|
if (dropdownToggle) {
|
|
dropdownToggle.addEventListener('click', () => {
|
|
if (resultsContainer.style.display === 'none') {
|
|
this.loadOrganizerOptions();
|
|
resultsContainer.style.display = 'block';
|
|
searchInput.focus();
|
|
} else {
|
|
resultsContainer.style.display = 'none';
|
|
}
|
|
});
|
|
}
|
|
|
|
// Click outside to close
|
|
document.addEventListener('click', (e) => {
|
|
if (!searchInput.parentElement.contains(e.target)) {
|
|
resultsContainer.style.display = 'none';
|
|
}
|
|
});
|
|
|
|
// Create new organizer button
|
|
$(document).on('click', '#create-new-organizer .create-new-btn', () => {
|
|
this.openModal('#new-organizer-modal');
|
|
});
|
|
},
|
|
|
|
initCategoriesSelector: function() {
|
|
const searchInput = document.getElementById('categories-search-input');
|
|
const resultsContainer = document.getElementById('categories-search-results');
|
|
const dropdownToggle = searchInput?.parentElement?.querySelector('.dropdown-toggle');
|
|
|
|
if (!searchInput || !resultsContainer) return;
|
|
|
|
searchInput.addEventListener('focus', () => {
|
|
this.loadCategoriesOptions();
|
|
resultsContainer.style.display = 'block';
|
|
});
|
|
|
|
searchInput.addEventListener('input', (e) => {
|
|
this.filterCategoriesOptions(e.target.value);
|
|
});
|
|
|
|
if (dropdownToggle) {
|
|
dropdownToggle.addEventListener('click', () => {
|
|
if (resultsContainer.style.display === 'none') {
|
|
this.loadCategoriesOptions();
|
|
resultsContainer.style.display = 'block';
|
|
searchInput.focus();
|
|
} else {
|
|
resultsContainer.style.display = 'none';
|
|
}
|
|
});
|
|
}
|
|
|
|
document.addEventListener('click', (e) => {
|
|
if (!searchInput.parentElement.contains(e.target)) {
|
|
resultsContainer.style.display = 'none';
|
|
}
|
|
});
|
|
|
|
$(document).on('click', '#create-new-category .create-new-btn', () => {
|
|
this.openModal('#new-category-modal');
|
|
});
|
|
},
|
|
|
|
initVenueSelector: function() {
|
|
const searchInput = document.getElementById('venue-search-input');
|
|
const resultsContainer = document.getElementById('venue-search-results');
|
|
const dropdownToggle = searchInput?.parentElement?.querySelector('.dropdown-toggle');
|
|
|
|
if (!searchInput || !resultsContainer) return;
|
|
|
|
searchInput.addEventListener('focus', () => {
|
|
this.loadVenueOptions();
|
|
resultsContainer.style.display = 'block';
|
|
});
|
|
|
|
searchInput.addEventListener('input', (e) => {
|
|
this.filterVenueOptions(e.target.value);
|
|
});
|
|
|
|
if (dropdownToggle) {
|
|
dropdownToggle.addEventListener('click', () => {
|
|
if (resultsContainer.style.display === 'none') {
|
|
this.loadVenueOptions();
|
|
resultsContainer.style.display = 'block';
|
|
searchInput.focus();
|
|
} else {
|
|
resultsContainer.style.display = 'none';
|
|
}
|
|
});
|
|
}
|
|
|
|
document.addEventListener('click', (e) => {
|
|
if (!searchInput.parentElement.contains(e.target)) {
|
|
resultsContainer.style.display = 'none';
|
|
}
|
|
});
|
|
|
|
$(document).on('click', '.create-new-btn', () => {
|
|
this.openModal('#new-venue-modal');
|
|
});
|
|
},
|
|
|
|
// Load options from server
|
|
loadOrganizerOptions: function() {
|
|
// Mock data for now - replace with AJAX call
|
|
const mockOrganizers = [
|
|
{ id: 1, name: 'John Smith', email: 'john@example.com' },
|
|
{ id: 2, name: 'Jane Doe', email: 'jane@example.com' },
|
|
{ id: 3, name: 'HVAC Training Institute', email: 'info@hvactraining.com' }
|
|
];
|
|
|
|
this.renderOrganizerOptions(mockOrganizers);
|
|
},
|
|
|
|
loadCategoriesOptions: function() {
|
|
const mockCategories = [
|
|
{ id: 1, name: 'HVAC Basics', description: 'Fundamental HVAC concepts' },
|
|
{ id: 2, name: 'Advanced Systems', description: 'Complex HVAC systems' },
|
|
{ id: 3, name: 'Safety Training', description: 'Safety protocols and procedures' }
|
|
];
|
|
|
|
this.renderCategoriesOptions(mockCategories);
|
|
},
|
|
|
|
loadVenueOptions: function() {
|
|
const mockVenues = [
|
|
{ id: 1, name: 'Training Center A', address: '123 Main St, City, ST' },
|
|
{ id: 2, name: 'Conference Hall B', address: '456 Oak Ave, City, ST' },
|
|
{ id: 3, name: 'Community Center', address: '789 Pine Rd, City, ST' }
|
|
];
|
|
|
|
this.renderVenueOptions(mockVenues);
|
|
},
|
|
|
|
renderOrganizerOptions: function(organizers) {
|
|
const resultsList = document.getElementById('organizer-results-list');
|
|
if (!resultsList) return;
|
|
|
|
resultsList.innerHTML = '';
|
|
organizers.forEach(organizer => {
|
|
const item = document.createElement('div');
|
|
item.className = 'search-result-item';
|
|
item.innerHTML = `<strong>${organizer.name}</strong><br><small>${organizer.email}</small>`;
|
|
item.addEventListener('click', () => {
|
|
this.selectOrganizer(organizer);
|
|
});
|
|
resultsList.appendChild(item);
|
|
});
|
|
},
|
|
|
|
renderCategoriesOptions: function(categories) {
|
|
const resultsList = document.getElementById('categories-results-list');
|
|
if (!resultsList) return;
|
|
|
|
resultsList.innerHTML = '';
|
|
categories.forEach(category => {
|
|
const item = document.createElement('div');
|
|
item.className = 'search-result-item';
|
|
item.innerHTML = `<strong>${category.name}</strong><br><small>${category.description}</small>`;
|
|
item.addEventListener('click', () => {
|
|
this.selectCategory(category);
|
|
});
|
|
resultsList.appendChild(item);
|
|
});
|
|
},
|
|
|
|
renderVenueOptions: function(venues) {
|
|
const resultsList = document.querySelector('#venue-search-results .results-list');
|
|
if (!resultsList) return;
|
|
|
|
resultsList.innerHTML = '';
|
|
venues.forEach(venue => {
|
|
const item = document.createElement('div');
|
|
item.className = 'search-result-item';
|
|
item.innerHTML = `<strong>${venue.name}</strong><br><small>${venue.address}</small>`;
|
|
item.addEventListener('click', () => {
|
|
this.selectVenue(venue);
|
|
});
|
|
resultsList.appendChild(item);
|
|
});
|
|
},
|
|
|
|
// Selection handlers
|
|
selectOrganizer: function(organizer) {
|
|
if (this.selectedOrganizers.length >= 3) {
|
|
alert('You can only select up to 3 organizers.');
|
|
return;
|
|
}
|
|
|
|
if (this.selectedOrganizers.find(o => o.id === organizer.id)) {
|
|
return; // Already selected
|
|
}
|
|
|
|
this.selectedOrganizers.push(organizer);
|
|
this.renderSelectedOrganizers();
|
|
document.getElementById('organizer-search-results').style.display = 'none';
|
|
document.getElementById('organizer-search-input').value = '';
|
|
},
|
|
|
|
selectCategory: function(category) {
|
|
if (this.selectedCategories.length >= 3) {
|
|
alert('You can only select up to 3 categories.');
|
|
return;
|
|
}
|
|
|
|
if (this.selectedCategories.find(c => c.id === category.id)) {
|
|
return;
|
|
}
|
|
|
|
this.selectedCategories.push(category);
|
|
this.renderSelectedCategories();
|
|
document.getElementById('categories-search-results').style.display = 'none';
|
|
document.getElementById('categories-search-input').value = '';
|
|
},
|
|
|
|
selectVenue: function(venue) {
|
|
this.selectedVenue = venue;
|
|
document.getElementById('venue-search-input').value = venue.name;
|
|
document.getElementById('venue-search-results').style.display = 'none';
|
|
},
|
|
|
|
renderSelectedOrganizers: function() {
|
|
const container = document.getElementById('organizer-selected-items');
|
|
if (!container) return;
|
|
|
|
container.innerHTML = '';
|
|
this.selectedOrganizers.forEach((organizer, index) => {
|
|
const tag = document.createElement('div');
|
|
tag.className = 'selected-item';
|
|
tag.innerHTML = `
|
|
${organizer.name}
|
|
<button type="button" class="remove-item" onclick="HVACEventForm.removeOrganizer(${index})">
|
|
<span class="dashicons dashicons-no-alt"></span>
|
|
</button>
|
|
`;
|
|
container.appendChild(tag);
|
|
});
|
|
|
|
// Update hidden input
|
|
const hiddenInput = document.getElementById('event_organizer');
|
|
if (hiddenInput) {
|
|
hiddenInput.value = this.selectedOrganizers.map(o => o.id).join(',');
|
|
}
|
|
},
|
|
|
|
renderSelectedCategories: function() {
|
|
const container = document.getElementById('categories-selected-items');
|
|
if (!container) return;
|
|
|
|
container.innerHTML = '';
|
|
this.selectedCategories.forEach((category, index) => {
|
|
const tag = document.createElement('div');
|
|
tag.className = 'selected-item';
|
|
tag.innerHTML = `
|
|
${category.name}
|
|
<button type="button" class="remove-item" onclick="HVACEventForm.removeCategory(${index})">
|
|
<span class="dashicons dashicons-no-alt"></span>
|
|
</button>
|
|
`;
|
|
container.appendChild(tag);
|
|
});
|
|
|
|
const hiddenInput = document.getElementById('event_categories');
|
|
if (hiddenInput) {
|
|
hiddenInput.value = this.selectedCategories.map(c => c.id).join(',');
|
|
}
|
|
},
|
|
|
|
removeOrganizer: function(index) {
|
|
this.selectedOrganizers.splice(index, 1);
|
|
this.renderSelectedOrganizers();
|
|
},
|
|
|
|
removeCategory: function(index) {
|
|
this.selectedCategories.splice(index, 1);
|
|
this.renderSelectedCategories();
|
|
},
|
|
|
|
// Filter functions
|
|
filterOrganizerOptions: function(query) {
|
|
// In real implementation, this would make an AJAX call
|
|
this.loadOrganizerOptions();
|
|
},
|
|
|
|
filterCategoriesOptions: function(query) {
|
|
this.loadCategoriesOptions();
|
|
},
|
|
|
|
filterVenueOptions: function(query) {
|
|
this.loadVenueOptions();
|
|
},
|
|
|
|
// Modal Management
|
|
initModalForms: function() {
|
|
// Modal close handlers
|
|
$(document).on('click', '.hvac-modal-close, .hvac-modal-cancel', (e) => {
|
|
this.closeModal($(e.target).closest('.hvac-modal'));
|
|
});
|
|
|
|
// Save handlers
|
|
$(document).on('click', '#save-new-organizer', () => {
|
|
this.saveNewOrganizer();
|
|
});
|
|
|
|
$(document).on('click', '#save-new-category', () => {
|
|
this.saveNewCategory();
|
|
});
|
|
|
|
$(document).on('click', '#save-new-venue', () => {
|
|
this.saveNewVenue();
|
|
});
|
|
|
|
// Close on backdrop click
|
|
$(document).on('click', '.hvac-modal', function(e) {
|
|
if (e.target === this) {
|
|
HVACEventForm.closeModal($(this));
|
|
}
|
|
});
|
|
},
|
|
|
|
openModal: function(modalSelector) {
|
|
$(modalSelector).fadeIn(300);
|
|
// Focus first input
|
|
setTimeout(() => {
|
|
$(modalSelector).find('input:first').focus();
|
|
}, 300);
|
|
},
|
|
|
|
closeModal: function(modal) {
|
|
if (modal instanceof jQuery) {
|
|
modal.fadeOut(300);
|
|
// Clear form
|
|
modal.find('form')[0]?.reset();
|
|
} else {
|
|
$(modal).fadeOut(300);
|
|
$(modal).find('form')[0]?.reset();
|
|
}
|
|
},
|
|
|
|
saveNewOrganizer: function() {
|
|
const form = document.getElementById('new-organizer-form');
|
|
const formData = new FormData(form);
|
|
const name = formData.get('organizer_name');
|
|
|
|
if (!name.trim()) {
|
|
alert('Organizer name is required.');
|
|
return;
|
|
}
|
|
|
|
// Create new organizer object
|
|
const newOrganizer = {
|
|
id: Date.now(), // Temporary ID
|
|
name: name,
|
|
email: formData.get('organizer_email') || '',
|
|
organization: formData.get('organizer_organization') || ''
|
|
};
|
|
|
|
// Add to selection
|
|
this.selectOrganizer(newOrganizer);
|
|
this.closeModal('#new-organizer-modal');
|
|
},
|
|
|
|
saveNewCategory: function() {
|
|
const form = document.getElementById('new-category-form');
|
|
const formData = new FormData(form);
|
|
const name = formData.get('category_name');
|
|
|
|
if (!name.trim()) {
|
|
alert('Category name is required.');
|
|
return;
|
|
}
|
|
|
|
const newCategory = {
|
|
id: Date.now(),
|
|
name: name,
|
|
description: formData.get('category_description') || ''
|
|
};
|
|
|
|
this.selectCategory(newCategory);
|
|
this.closeModal('#new-category-modal');
|
|
},
|
|
|
|
saveNewVenue: function() {
|
|
const form = document.getElementById('new-venue-form');
|
|
const formData = new FormData(form);
|
|
const name = formData.get('venue_name');
|
|
const address = formData.get('venue_address');
|
|
|
|
if (!name.trim() || !address.trim()) {
|
|
alert('Venue name and address are required.');
|
|
return;
|
|
}
|
|
|
|
const newVenue = {
|
|
id: Date.now(),
|
|
name: name,
|
|
address: address
|
|
};
|
|
|
|
this.selectVenue(newVenue);
|
|
this.closeModal('#new-venue-modal');
|
|
},
|
|
|
|
// Event binding
|
|
bindEvents: function() {
|
|
// Form submission
|
|
$(document).on('submit', 'form', () => {
|
|
this.syncEditorContent();
|
|
});
|
|
|
|
// Auto-save functionality (if needed)
|
|
setInterval(() => {
|
|
this.syncEditorContent();
|
|
}, 5000);
|
|
}
|
|
};
|
|
|
|
// Initialize when DOM is ready
|
|
$(document).ready(function() {
|
|
HVACEventForm.init();
|
|
});
|
|
|
|
})(jQuery); |