Fix CSP blocking inline handlers - add unsafe-inline fallback
- Refactored TicketView.php to use event listeners instead of onclick - Added unsafe-inline to CSP as fallback for legacy handlers in other views - TODO: Complete refactoring of DashboardView and admin views Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -26,8 +26,10 @@ class SecurityHeadersMiddleware {
|
|||||||
$nonce = self::getNonce();
|
$nonce = self::getNonce();
|
||||||
|
|
||||||
// Content Security Policy - restricts where resources can be loaded from
|
// Content Security Policy - restricts where resources can be loaded from
|
||||||
// Using nonce for inline scripts instead of unsafe-inline for better security
|
// Nonces are used for <script> tags, but 'unsafe-inline' is needed for legacy onclick handlers
|
||||||
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{$nonce}'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self';");
|
// TODO: Refactor all inline event handlers (onclick, etc.) to use addEventListener,
|
||||||
|
// then remove 'unsafe-inline' from script-src for full CSP protection
|
||||||
|
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'nonce-{$nonce}'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self';");
|
||||||
|
|
||||||
// Prevent clickjacking by disallowing framing
|
// Prevent clickjacking by disallowing framing
|
||||||
header("X-Frame-Options: DENY");
|
header("X-Frame-Options: DENY");
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<?php if ($GLOBALS['currentUser']['is_admin']): ?>
|
<?php if ($GLOBALS['currentUser']['is_admin']): ?>
|
||||||
<span class="admin-badge">Admin</span>
|
<span class="admin-badge">Admin</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<button class="settings-icon" title="Settings (Alt+S)" onclick="openSettingsModal()">⚙</button>
|
<button class="settings-icon" title="Settings (Alt+S)" id="settingsBtn">⚙</button>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -226,7 +226,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button id="editButton" class="btn" onclick="toggleEditMode()">Edit Ticket</button>
|
<button id="editButton" class="btn">Edit Ticket</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -240,11 +240,11 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<div class="ascii-section-header">Content Sections</div>
|
<div class="ascii-section-header">Content Sections</div>
|
||||||
<div class="ascii-content">
|
<div class="ascii-content">
|
||||||
<div class="ticket-tabs">
|
<div class="ticket-tabs">
|
||||||
<button class="tab-btn active" onclick="showTab('description')">Description</button>
|
<button class="tab-btn active" data-tab="description">Description</button>
|
||||||
<button class="tab-btn" onclick="showTab('comments')">Comments</button>
|
<button class="tab-btn" data-tab="comments">Comments</button>
|
||||||
<button class="tab-btn" onclick="showTab('attachments')">Attachments</button>
|
<button class="tab-btn" data-tab="attachments">Attachments</button>
|
||||||
<button class="tab-btn" onclick="showTab('dependencies')">Dependencies</button>
|
<button class="tab-btn" data-tab="dependencies">Dependencies</button>
|
||||||
<button class="tab-btn" onclick="showTab('activity')">Activity</button>
|
<button class="tab-btn" data-tab="activity">Activity</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -288,7 +288,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<span class="toggle-label">Preview Markdown</span>
|
<span class="toggle-label">Preview Markdown</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button onclick="addComment()" class="btn">Add Comment</button>
|
<button id="addCommentBtn" class="btn">Add Comment</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="markdownPreview" class="markdown-preview" style="display: none;"></div>
|
<div id="markdownPreview" class="markdown-preview" style="display: none;"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -320,8 +320,8 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
// Edit/Delete buttons for owner or admin
|
// Edit/Delete buttons for owner or admin
|
||||||
if ($canModify) {
|
if ($canModify) {
|
||||||
echo "<div class='comment-actions'>";
|
echo "<div class='comment-actions'>";
|
||||||
echo "<button type='button' class='comment-action-btn edit-btn' onclick='editComment({$commentId})' title='Edit comment'>✏️</button>";
|
echo "<button type='button' class='comment-action-btn edit-btn' data-action='edit-comment' data-comment-id='{$commentId}' title='Edit comment'>✏️</button>";
|
||||||
echo "<button type='button' class='comment-action-btn delete-btn' onclick='deleteComment({$commentId})' title='Delete comment'>🗑️</button>";
|
echo "<button type='button' class='comment-action-btn delete-btn' data-action='delete-comment' data-comment-id='{$commentId}' title='Delete comment'>🗑️</button>";
|
||||||
echo "</div>";
|
echo "</div>";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,7 +357,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<p>Drag and drop files here or click to browse</p>
|
<p>Drag and drop files here or click to browse</p>
|
||||||
<p class="upload-hint">Max file size: <?php echo $GLOBALS['config']['MAX_UPLOAD_SIZE'] ? number_format($GLOBALS['config']['MAX_UPLOAD_SIZE'] / 1048576, 0) . 'MB' : '10MB'; ?></p>
|
<p class="upload-hint">Max file size: <?php echo $GLOBALS['config']['MAX_UPLOAD_SIZE'] ? number_format($GLOBALS['config']['MAX_UPLOAD_SIZE'] / 1048576, 0) . 'MB' : '10MB'; ?></p>
|
||||||
<input type="file" id="fileInput" multiple style="display: none;">
|
<input type="file" id="fileInput" multiple style="display: none;">
|
||||||
<button type="button" onclick="document.getElementById('fileInput').click();" class="btn" style="margin-top: 1rem;">Browse Files</button>
|
<button type="button" id="browseFilesBtn" class="btn" style="margin-top: 1rem;">Browse Files</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="uploadProgress" style="display: none; margin-top: 1rem;">
|
<div id="uploadProgress" style="display: none; margin-top: 1rem;">
|
||||||
@@ -393,7 +393,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<option value="relates_to">Relates To</option>
|
<option value="relates_to">Relates To</option>
|
||||||
<option value="duplicates">Duplicates</option>
|
<option value="duplicates">Duplicates</option>
|
||||||
</select>
|
</select>
|
||||||
<button onclick="addDependency()" class="btn">Add</button>
|
<button id="addDependencyBtn" class="btn">Add</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -447,30 +447,124 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
</div>
|
</div>
|
||||||
<!-- END OUTER FRAME -->
|
<!-- END OUTER FRAME -->
|
||||||
<script nonce="<?php echo $nonce; ?>">
|
<script nonce="<?php echo $nonce; ?>">
|
||||||
// Initialize the ticket view
|
// Initialize the ticket view and attach event listeners (CSP-compliant)
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Ticket data alias for compatibility
|
||||||
|
window.ticketData.id = window.ticketData.ticket_id;
|
||||||
|
|
||||||
|
// Initialize with description tab
|
||||||
if (typeof showTab === 'function') {
|
if (typeof showTab === 'function') {
|
||||||
showTab('description');
|
showTab('description');
|
||||||
} else {
|
}
|
||||||
console.error('showTab function not defined');
|
|
||||||
|
// Tab buttons - use event delegation
|
||||||
|
document.querySelectorAll('.tab-btn[data-tab]').forEach(function(btn) {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
var tab = this.getAttribute('data-tab');
|
||||||
|
if (typeof showTab === 'function') {
|
||||||
|
showTab(tab);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Settings button
|
||||||
|
var settingsBtn = document.getElementById('settingsBtn');
|
||||||
|
if (settingsBtn) {
|
||||||
|
settingsBtn.addEventListener('click', function() {
|
||||||
|
if (typeof openSettingsModal === 'function') openSettingsModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit button
|
||||||
|
var editBtn = document.getElementById('editButton');
|
||||||
|
if (editBtn) {
|
||||||
|
editBtn.addEventListener('click', function() {
|
||||||
|
if (typeof toggleEditMode === 'function') toggleEditMode();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add comment button
|
||||||
|
var addCommentBtn = document.getElementById('addCommentBtn');
|
||||||
|
if (addCommentBtn) {
|
||||||
|
addCommentBtn.addEventListener('click', function() {
|
||||||
|
if (typeof addComment === 'function') addComment();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comment edit/delete buttons - use event delegation
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
var target = e.target.closest('[data-action]');
|
||||||
|
if (!target) return;
|
||||||
|
|
||||||
|
var action = target.getAttribute('data-action');
|
||||||
|
var commentId = target.getAttribute('data-comment-id');
|
||||||
|
|
||||||
|
if (action === 'edit-comment' && commentId) {
|
||||||
|
if (typeof editComment === 'function') editComment(parseInt(commentId));
|
||||||
|
} else if (action === 'delete-comment' && commentId) {
|
||||||
|
if (typeof deleteComment === 'function') deleteComment(parseInt(commentId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Browse files button
|
||||||
|
var browseFilesBtn = document.getElementById('browseFilesBtn');
|
||||||
|
if (browseFilesBtn) {
|
||||||
|
browseFilesBtn.addEventListener('click', function() {
|
||||||
|
document.getElementById('fileInput').click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add dependency button
|
||||||
|
var addDepBtn = document.getElementById('addDependencyBtn');
|
||||||
|
if (addDepBtn) {
|
||||||
|
addDepBtn.addEventListener('click', function() {
|
||||||
|
if (typeof addDependency === 'function') addDependency();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings modal buttons
|
||||||
|
var closeSettingsBtn = document.getElementById('closeSettingsBtn');
|
||||||
|
if (closeSettingsBtn) {
|
||||||
|
closeSettingsBtn.addEventListener('click', function() {
|
||||||
|
if (typeof closeSettingsModal === 'function') closeSettingsModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var saveSettingsBtn = document.getElementById('saveSettingsBtn');
|
||||||
|
if (saveSettingsBtn) {
|
||||||
|
saveSettingsBtn.addEventListener('click', function() {
|
||||||
|
if (typeof saveSettings === 'function') saveSettings();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var cancelSettingsBtn = document.getElementById('cancelSettingsBtn');
|
||||||
|
if (cancelSettingsBtn) {
|
||||||
|
cancelSettingsBtn.addEventListener('click', function() {
|
||||||
|
if (typeof closeSettingsModal === 'function') closeSettingsModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings modal backdrop click
|
||||||
|
var settingsModal = document.getElementById('settingsModal');
|
||||||
|
if (settingsModal) {
|
||||||
|
settingsModal.addEventListener('click', function(e) {
|
||||||
|
if (e.target.classList.contains('settings-modal')) {
|
||||||
|
if (typeof closeSettingsModal === 'function') closeSettingsModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<script nonce="<?php echo $nonce; ?>">
|
|
||||||
// Ticket data already initialized in head, add id alias for compatibility
|
|
||||||
window.ticketData.id = window.ticketData.ticket_id;
|
|
||||||
console.log('Ticket data loaded:', window.ticketData);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Settings Modal (same as dashboard) -->
|
<!-- Settings Modal (same as dashboard) -->
|
||||||
<div class="settings-modal" id="settingsModal" style="display: none;" onclick="closeOnBackdropClick(event)">
|
<div class="settings-modal" id="settingsModal" style="display: none;">
|
||||||
<div class="settings-content">
|
<div class="settings-content">
|
||||||
<span class="bottom-left-corner">╚</span>
|
<span class="bottom-left-corner">╚</span>
|
||||||
<span class="bottom-right-corner">╝</span>
|
<span class="bottom-right-corner">╝</span>
|
||||||
|
|
||||||
<div class="settings-header">
|
<div class="settings-header">
|
||||||
<h3>⚙ System Preferences</h3>
|
<h3>⚙ System Preferences</h3>
|
||||||
<button class="close-settings" onclick="closeSettingsModal()">✗</button>
|
<button class="close-settings" id="closeSettingsBtn">✗</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-body">
|
<div class="settings-body">
|
||||||
@@ -592,8 +686,8 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-footer">
|
<div class="settings-footer">
|
||||||
<button class="btn btn-primary" onclick="saveSettings()">Save Preferences</button>
|
<button class="btn btn-primary" id="saveSettingsBtn">Save Preferences</button>
|
||||||
<button class="btn btn-secondary" onclick="closeSettingsModal()">Cancel</button>
|
<button class="btn btn-secondary" id="cancelSettingsBtn">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user