Harden CSP by removing unsafe-inline for scripts
Refactored all inline event handlers (onclick, onchange, onsubmit) to use
addEventListener with data-action attributes and event delegation pattern.
Changes:
- views/*.php: Replaced inline handlers with data-action attributes
- views/admin/*.php: Same refactoring for all admin views
- assets/js/dashboard.js: Added event delegation for bulk/quick action modals
- assets/js/ticket.js: Added event delegation for dynamic elements
- assets/js/markdown.js: Refactored toolbar button handlers
- assets/js/keyboard-shortcuts.js: Refactored modal close button
- SecurityHeadersMiddleware.php: Enabled strict CSP with nonces
The CSP now uses script-src 'self' 'nonce-{nonce}' instead of 'unsafe-inline',
significantly improving XSS protection.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -97,7 +97,7 @@ function initMobileSidebar() {
|
||||
<span class="nav-icon">🏠</span>
|
||||
<span>Home</span>
|
||||
</a>
|
||||
<button type="button" onclick="openMobileSidebar()">
|
||||
<button type="button" data-action="open-mobile-sidebar">
|
||||
<span class="nav-icon">🔍</span>
|
||||
<span>Filter</span>
|
||||
</button>
|
||||
@@ -105,7 +105,7 @@ function initMobileSidebar() {
|
||||
<span class="nav-icon">➕</span>
|
||||
<span>New</span>
|
||||
</a>
|
||||
<button type="button" onclick="if(typeof openSettingsModal==='function')openSettingsModal()">
|
||||
<button type="button" data-action="open-settings-modal">
|
||||
<span class="nav-icon">⚙️</span>
|
||||
<span>Settings</span>
|
||||
</button>
|
||||
@@ -136,20 +136,75 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
window.location.href.includes('ticket.php') ||
|
||||
document.querySelector('.ticket-details') !== null;
|
||||
const isDashboard = hasTable && !isTicketPage;
|
||||
|
||||
|
||||
if (isDashboard) {
|
||||
// Dashboard-specific initialization
|
||||
initStatusFilter();
|
||||
initTableSorting();
|
||||
initSidebarFilters();
|
||||
}
|
||||
|
||||
|
||||
// Initialize for all pages
|
||||
initSettingsModal();
|
||||
|
||||
// Force dark mode only (terminal aesthetic - no theme switching)
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
document.body.classList.add('dark-mode');
|
||||
|
||||
// Event delegation for dynamically created modals
|
||||
document.addEventListener('click', function(e) {
|
||||
const target = e.target.closest('[data-action]');
|
||||
if (!target) return;
|
||||
|
||||
const action = target.dataset.action;
|
||||
switch (action) {
|
||||
// Bulk operations
|
||||
case 'perform-bulk-assign':
|
||||
performBulkAssign();
|
||||
break;
|
||||
case 'close-bulk-assign-modal':
|
||||
closeBulkAssignModal();
|
||||
break;
|
||||
case 'perform-bulk-priority':
|
||||
performBulkPriority();
|
||||
break;
|
||||
case 'close-bulk-priority-modal':
|
||||
closeBulkPriorityModal();
|
||||
break;
|
||||
case 'perform-bulk-status':
|
||||
performBulkStatusChange();
|
||||
break;
|
||||
case 'close-bulk-status-modal':
|
||||
closeBulkStatusModal();
|
||||
break;
|
||||
case 'perform-bulk-delete':
|
||||
performBulkDelete();
|
||||
break;
|
||||
case 'close-bulk-delete-modal':
|
||||
closeBulkDeleteModal();
|
||||
break;
|
||||
// Quick actions
|
||||
case 'perform-quick-status':
|
||||
performQuickStatusChange(target.dataset.ticketId);
|
||||
break;
|
||||
case 'close-quick-status-modal':
|
||||
closeQuickStatusModal();
|
||||
break;
|
||||
case 'perform-quick-assign':
|
||||
performQuickAssign(target.dataset.ticketId);
|
||||
break;
|
||||
case 'close-quick-assign-modal':
|
||||
closeQuickAssignModal();
|
||||
break;
|
||||
// Mobile navigation
|
||||
case 'open-mobile-sidebar':
|
||||
if (typeof openMobileSidebar === 'function') openMobileSidebar();
|
||||
break;
|
||||
case 'open-settings-modal':
|
||||
if (typeof openSettingsModal === 'function') openSettingsModal();
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function initTableSorting() {
|
||||
@@ -716,8 +771,8 @@ function showBulkAssignModal() {
|
||||
|
||||
<div class="ascii-content">
|
||||
<div class="modal-footer">
|
||||
<button onclick="performBulkAssign()" class="btn btn-bulk">Assign</button>
|
||||
<button onclick="closeBulkAssignModal()" class="btn btn-secondary">Cancel</button>
|
||||
<button data-action="perform-bulk-assign" class="btn btn-bulk">Assign</button>
|
||||
<button data-action="close-bulk-assign-modal" class="btn btn-secondary">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -829,8 +884,8 @@ function showBulkPriorityModal() {
|
||||
|
||||
<div class="ascii-content">
|
||||
<div class="modal-footer">
|
||||
<button onclick="performBulkPriority()" class="btn btn-bulk">Update</button>
|
||||
<button onclick="closeBulkPriorityModal()" class="btn btn-secondary">Cancel</button>
|
||||
<button data-action="perform-bulk-priority" class="btn btn-bulk">Update</button>
|
||||
<button data-action="close-bulk-priority-modal" class="btn btn-secondary">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -963,14 +1018,14 @@ function showBulkStatusModal() {
|
||||
|
||||
<div class="ascii-content">
|
||||
<div class="modal-footer">
|
||||
<button onclick="performBulkStatusChange()" class="btn btn-bulk">Update</button>
|
||||
<button onclick="closeBulkStatusModal()" class="btn btn-secondary">Cancel</button>
|
||||
<button data-action="perform-bulk-status" class="btn btn-bulk">Update</button>
|
||||
<button data-action="close-bulk-status-modal" class="btn btn-secondary">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
||||
}
|
||||
|
||||
@@ -1057,14 +1112,14 @@ function showBulkDeleteModal() {
|
||||
|
||||
<div class="ascii-content">
|
||||
<div class="modal-footer">
|
||||
<button onclick="performBulkDelete()" class="btn btn-bulk" style="background: var(--status-closed); border-color: var(--status-closed);">Delete Permanently</button>
|
||||
<button onclick="closeBulkDeleteModal()" class="btn btn-secondary">Cancel</button>
|
||||
<button data-action="perform-bulk-delete" class="btn btn-bulk" style="background: var(--status-closed); border-color: var(--status-closed);">Delete Permanently</button>
|
||||
<button data-action="close-bulk-delete-modal" class="btn btn-secondary">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
||||
}
|
||||
|
||||
@@ -1332,8 +1387,8 @@ function quickStatusChange(ticketId, currentStatus) {
|
||||
|
||||
<div class="ascii-content">
|
||||
<div class="modal-footer">
|
||||
<button onclick="performQuickStatusChange('${ticketId}')" class="btn btn-primary">Update</button>
|
||||
<button onclick="closeQuickStatusModal()" class="btn btn-secondary">Cancel</button>
|
||||
<button data-action="perform-quick-status" data-ticket-id="${ticketId}" class="btn btn-primary">Update</button>
|
||||
<button data-action="close-quick-status-modal" class="btn btn-secondary">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1407,8 +1462,8 @@ function quickAssign(ticketId) {
|
||||
|
||||
<div class="ascii-content">
|
||||
<div class="modal-footer">
|
||||
<button onclick="performQuickAssign('${ticketId}')" class="btn btn-primary">Assign</button>
|
||||
<button onclick="closeQuickAssignModal()" class="btn btn-secondary">Cancel</button>
|
||||
<button data-action="perform-quick-assign" data-ticket-id="${ticketId}" class="btn btn-primary">Assign</button>
|
||||
<button data-action="close-quick-assign-modal" class="btn btn-secondary">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -123,11 +123,16 @@ function showKeyboardHelp() {
|
||||
</table>
|
||||
</div>
|
||||
<div class="modal-footer" style="margin-top: 1rem;">
|
||||
<button class="btn btn-secondary" onclick="this.closest('.modal-overlay').remove()">Close</button>
|
||||
<button class="btn btn-secondary" data-action="close-shortcuts-modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Add event listener for the close button
|
||||
modal.querySelector('[data-action="close-shortcuts-modal"]').addEventListener('click', function() {
|
||||
modal.remove();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -356,17 +356,36 @@ function createEditorToolbar(textareaId, containerId) {
|
||||
const toolbar = document.createElement('div');
|
||||
toolbar.className = 'editor-toolbar';
|
||||
toolbar.innerHTML = `
|
||||
<button type="button" onclick="toolbarBold('${textareaId}')" title="Bold (Ctrl+B)"><b>B</b></button>
|
||||
<button type="button" onclick="toolbarItalic('${textareaId}')" title="Italic (Ctrl+I)"><i>I</i></button>
|
||||
<button type="button" onclick="toolbarCode('${textareaId}')" title="Code"></></button>
|
||||
<button type="button" data-toolbar-action="bold" data-textarea="${textareaId}" title="Bold (Ctrl+B)"><b>B</b></button>
|
||||
<button type="button" data-toolbar-action="italic" data-textarea="${textareaId}" title="Italic (Ctrl+I)"><i>I</i></button>
|
||||
<button type="button" data-toolbar-action="code" data-textarea="${textareaId}" title="Code"></></button>
|
||||
<span class="toolbar-separator"></span>
|
||||
<button type="button" onclick="toolbarHeading('${textareaId}')" title="Heading">H</button>
|
||||
<button type="button" onclick="toolbarList('${textareaId}')" title="List">≡</button>
|
||||
<button type="button" onclick="toolbarQuote('${textareaId}')" title="Quote">"</button>
|
||||
<button type="button" data-toolbar-action="heading" data-textarea="${textareaId}" title="Heading">H</button>
|
||||
<button type="button" data-toolbar-action="list" data-textarea="${textareaId}" title="List">≡</button>
|
||||
<button type="button" data-toolbar-action="quote" data-textarea="${textareaId}" title="Quote">"</button>
|
||||
<span class="toolbar-separator"></span>
|
||||
<button type="button" onclick="toolbarLink('${textareaId}')" title="Link">🔗</button>
|
||||
<button type="button" data-toolbar-action="link" data-textarea="${textareaId}" title="Link">🔗</button>
|
||||
`;
|
||||
|
||||
// Add event delegation for toolbar buttons
|
||||
toolbar.addEventListener('click', function(e) {
|
||||
const btn = e.target.closest('[data-toolbar-action]');
|
||||
if (!btn) return;
|
||||
|
||||
const action = btn.dataset.toolbarAction;
|
||||
const targetId = btn.dataset.textarea;
|
||||
|
||||
switch (action) {
|
||||
case 'bold': toolbarBold(targetId); break;
|
||||
case 'italic': toolbarItalic(targetId); break;
|
||||
case 'code': toolbarCode(targetId); break;
|
||||
case 'heading': toolbarHeading(targetId); break;
|
||||
case 'list': toolbarList(targetId); break;
|
||||
case 'quote': toolbarQuote(targetId); break;
|
||||
case 'link': toolbarLink(targetId); break;
|
||||
}
|
||||
});
|
||||
|
||||
container.insertBefore(toolbar, container.firstChild);
|
||||
}
|
||||
|
||||
|
||||
@@ -569,7 +569,7 @@ function showTab(tabName) {
|
||||
|
||||
// Show selected tab and activate its button
|
||||
document.getElementById(`${tabName}-tab`).style.display = 'block';
|
||||
document.querySelector(`[onclick="showTab('${tabName}')"]`).classList.add('active');
|
||||
document.querySelector(`.tab-btn[data-tab="${tabName}"]`).classList.add('active');
|
||||
|
||||
// Load attachments when tab is shown
|
||||
if (tabName === 'attachments') {
|
||||
@@ -654,7 +654,7 @@ function renderDependencies(dependencies) {
|
||||
<span style="margin-left: 0.5rem;">${escapeHtml(dep.title)}</span>
|
||||
<span class="status-badge ${statusClass}" style="margin-left: 0.5rem; font-size: 0.8rem;">${escapeHtml(dep.status)}</span>
|
||||
</div>
|
||||
<button onclick="removeDependency('${dep.dependency_id}')" class="btn btn-small" style="padding: 0.25rem 0.5rem; font-size: 0.8rem;">Remove</button>
|
||||
<button data-action="remove-dependency" data-dependency-id="${dep.dependency_id}" class="btn btn-small" style="padding: 0.25rem 0.5rem; font-size: 0.8rem;">Remove</button>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
@@ -956,7 +956,7 @@ function renderAttachments(attachments) {
|
||||
</div>
|
||||
<div class="attachment-actions">
|
||||
<a href="/api/download_attachment.php?id=${att.attachment_id}" class="btn btn-small" title="Download">⬇</a>
|
||||
<button onclick="deleteAttachment(${att.attachment_id})" class="btn btn-small btn-danger" title="Delete">✕</button>
|
||||
<button data-action="delete-attachment" data-attachment-id="${att.attachment_id}" class="btn btn-small btn-danger" title="Delete">✕</button>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
@@ -1150,7 +1150,7 @@ function showMentionSuggestions(query, textarea) {
|
||||
let html = '';
|
||||
filtered.forEach((user, index) => {
|
||||
const isSelected = index === 0 ? 'selected' : '';
|
||||
html += `<div class="mention-option ${isSelected}" data-username="${escapeHtml(user.username)}" onclick="selectMention('${escapeHtml(user.username)}')">
|
||||
html += `<div class="mention-option ${isSelected}" data-username="${escapeHtml(user.username)}" data-action="select-mention">
|
||||
<span class="mention-username">@${escapeHtml(user.username)}</span>
|
||||
${user.display_name ? `<span class="mention-displayname">${escapeHtml(user.display_name)}</span>` : ''}
|
||||
</div>`;
|
||||
@@ -1212,6 +1212,31 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
el.innerHTML = highlightMentions(el.innerHTML);
|
||||
}
|
||||
});
|
||||
|
||||
// Event delegation for dynamically created elements
|
||||
document.addEventListener('click', function(e) {
|
||||
const target = e.target.closest('[data-action]');
|
||||
if (!target) return;
|
||||
|
||||
const action = target.dataset.action;
|
||||
switch (action) {
|
||||
case 'remove-dependency':
|
||||
removeDependency(target.dataset.dependencyId);
|
||||
break;
|
||||
case 'delete-attachment':
|
||||
deleteAttachment(target.dataset.attachmentId);
|
||||
break;
|
||||
case 'select-mention':
|
||||
selectMention(target.dataset.username);
|
||||
break;
|
||||
case 'save-edit-comment':
|
||||
saveEditComment(parseInt(target.dataset.commentId));
|
||||
break;
|
||||
case 'cancel-edit-comment':
|
||||
cancelEditComment(parseInt(target.dataset.commentId));
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================
|
||||
@@ -1251,8 +1276,8 @@ function editComment(commentId) {
|
||||
Markdown
|
||||
</label>
|
||||
<div class="comment-edit-buttons">
|
||||
<button type="button" class="btn btn-small" onclick="saveEditComment(${commentId})">Save</button>
|
||||
<button type="button" class="btn btn-secondary btn-small" onclick="cancelEditComment(${commentId})">Cancel</button>
|
||||
<button type="button" class="btn btn-small" data-action="save-edit-comment" data-comment-id="${commentId}">Save</button>
|
||||
<button type="button" class="btn btn-secondary btn-small" data-action="cancel-edit-comment" data-comment-id="${commentId}">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user