Implement comprehensive improvement plan (Phases 1-6)
Security (Phase 1-2): - Add SecurityHeadersMiddleware with CSP, X-Frame-Options, etc. - Add RateLimitMiddleware for API rate limiting - Add security event logging to AuditLogModel - Add ResponseHelper for standardized API responses - Update config.php with security constants Database (Phase 3): - Add migration 014 for additional indexes - Add migration 015 for ticket dependencies - Add migration 016 for ticket attachments - Add migration 017 for recurring tickets - Add migration 018 for custom fields Features (Phase 4-5): - Add ticket dependencies with DependencyModel and API - Add duplicate detection with check_duplicates API - Add file attachments with AttachmentModel and upload/download APIs - Add @mentions with autocomplete and highlighting - Add quick actions on dashboard rows Collaboration (Phase 5): - Add mention extraction in CommentModel - Add mention autocomplete dropdown in ticket.js - Add mention highlighting CSS styles Admin & Export (Phase 6): - Add StatsModel for dashboard widgets - Add dashboard stats cards (open, critical, unassigned, etc.) - Add CSV/JSON export via export_tickets API - Add rich text editor toolbar in markdown.js - Add RecurringTicketModel with cron job - Add CustomFieldModel for per-category fields - Add admin views: RecurringTickets, CustomFields, Workflow, Templates, AuditLog, UserActivity - Add admin APIs: manage_workflows, manage_templates, manage_recurring, custom_fields, get_users - Add admin routes in index.php Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -27,8 +27,15 @@ function parseMarkdown(markdown) {
|
||||
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
||||
html = html.replace(/_(.+?)_/g, '<em>$1</em>');
|
||||
|
||||
// Links [text](url)
|
||||
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
|
||||
// Links [text](url) - only allow safe protocols
|
||||
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function(match, text, url) {
|
||||
// Only allow http, https, mailto protocols
|
||||
if (/^(https?:|mailto:|\/)/i.test(url)) {
|
||||
return '<a href="' + url + '" target="_blank" rel="noopener noreferrer">' + text + '</a>';
|
||||
}
|
||||
// Block potentially dangerous protocols (javascript:, data:, etc.)
|
||||
return text;
|
||||
});
|
||||
|
||||
// Headers (# H1, ## H2, etc.)
|
||||
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
|
||||
@@ -75,3 +82,191 @@ document.addEventListener('DOMContentLoaded', renderMarkdownElements);
|
||||
// Expose for manual use
|
||||
window.parseMarkdown = parseMarkdown;
|
||||
window.renderMarkdownElements = renderMarkdownElements;
|
||||
|
||||
// ========================================
|
||||
// Rich Text Editor Toolbar Functions
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Insert markdown formatting around selection
|
||||
*/
|
||||
function insertMarkdownFormat(textareaId, prefix, suffix) {
|
||||
const textarea = document.getElementById(textareaId);
|
||||
if (!textarea) return;
|
||||
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const text = textarea.value;
|
||||
const selectedText = text.substring(start, end);
|
||||
|
||||
// Insert formatting
|
||||
const newText = text.substring(0, start) + prefix + selectedText + suffix + text.substring(end);
|
||||
textarea.value = newText;
|
||||
|
||||
// Set cursor position
|
||||
if (selectedText) {
|
||||
textarea.setSelectionRange(start + prefix.length, end + prefix.length);
|
||||
} else {
|
||||
textarea.setSelectionRange(start + prefix.length, start + prefix.length);
|
||||
}
|
||||
|
||||
textarea.focus();
|
||||
|
||||
// Trigger input event to update preview if enabled
|
||||
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert markdown at cursor position
|
||||
*/
|
||||
function insertMarkdownText(textareaId, text) {
|
||||
const textarea = document.getElementById(textareaId);
|
||||
if (!textarea) return;
|
||||
|
||||
const start = textarea.selectionStart;
|
||||
const value = textarea.value;
|
||||
|
||||
textarea.value = value.substring(0, start) + text + value.substring(start);
|
||||
textarea.setSelectionRange(start + text.length, start + text.length);
|
||||
textarea.focus();
|
||||
|
||||
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Toolbar button handlers
|
||||
*/
|
||||
function toolbarBold(textareaId) {
|
||||
insertMarkdownFormat(textareaId, '**', '**');
|
||||
}
|
||||
|
||||
function toolbarItalic(textareaId) {
|
||||
insertMarkdownFormat(textareaId, '_', '_');
|
||||
}
|
||||
|
||||
function toolbarCode(textareaId) {
|
||||
const textarea = document.getElementById(textareaId);
|
||||
if (!textarea) return;
|
||||
|
||||
const selectedText = textarea.value.substring(textarea.selectionStart, textarea.selectionEnd);
|
||||
|
||||
// Use code block for multi-line, inline code for single line
|
||||
if (selectedText.includes('\n')) {
|
||||
insertMarkdownFormat(textareaId, '```\n', '\n```');
|
||||
} else {
|
||||
insertMarkdownFormat(textareaId, '`', '`');
|
||||
}
|
||||
}
|
||||
|
||||
function toolbarLink(textareaId) {
|
||||
const textarea = document.getElementById(textareaId);
|
||||
if (!textarea) return;
|
||||
|
||||
const selectedText = textarea.value.substring(textarea.selectionStart, textarea.selectionEnd);
|
||||
|
||||
if (selectedText) {
|
||||
// Wrap selected text as link text
|
||||
insertMarkdownFormat(textareaId, '[', '](url)');
|
||||
} else {
|
||||
insertMarkdownText(textareaId, '[link text](url)');
|
||||
}
|
||||
}
|
||||
|
||||
function toolbarList(textareaId) {
|
||||
const textarea = document.getElementById(textareaId);
|
||||
if (!textarea) return;
|
||||
|
||||
const start = textarea.selectionStart;
|
||||
const text = textarea.value;
|
||||
|
||||
// Find start of current line
|
||||
let lineStart = start;
|
||||
while (lineStart > 0 && text[lineStart - 1] !== '\n') {
|
||||
lineStart--;
|
||||
}
|
||||
|
||||
// Insert list marker at beginning of line
|
||||
textarea.value = text.substring(0, lineStart) + '- ' + text.substring(lineStart);
|
||||
textarea.setSelectionRange(start + 2, start + 2);
|
||||
textarea.focus();
|
||||
|
||||
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
|
||||
function toolbarHeading(textareaId) {
|
||||
const textarea = document.getElementById(textareaId);
|
||||
if (!textarea) return;
|
||||
|
||||
const start = textarea.selectionStart;
|
||||
const text = textarea.value;
|
||||
|
||||
// Find start of current line
|
||||
let lineStart = start;
|
||||
while (lineStart > 0 && text[lineStart - 1] !== '\n') {
|
||||
lineStart--;
|
||||
}
|
||||
|
||||
// Insert heading marker at beginning of line
|
||||
textarea.value = text.substring(0, lineStart) + '## ' + text.substring(lineStart);
|
||||
textarea.setSelectionRange(start + 3, start + 3);
|
||||
textarea.focus();
|
||||
|
||||
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
|
||||
function toolbarQuote(textareaId) {
|
||||
const textarea = document.getElementById(textareaId);
|
||||
if (!textarea) return;
|
||||
|
||||
const start = textarea.selectionStart;
|
||||
const text = textarea.value;
|
||||
|
||||
// Find start of current line
|
||||
let lineStart = start;
|
||||
while (lineStart > 0 && text[lineStart - 1] !== '\n') {
|
||||
lineStart--;
|
||||
}
|
||||
|
||||
// Insert quote marker at beginning of line
|
||||
textarea.value = text.substring(0, lineStart) + '> ' + text.substring(lineStart);
|
||||
textarea.setSelectionRange(start + 2, start + 2);
|
||||
textarea.focus();
|
||||
|
||||
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and insert toolbar HTML for a textarea
|
||||
*/
|
||||
function createEditorToolbar(textareaId, containerId) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
|
||||
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>
|
||||
<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>
|
||||
<span class="toolbar-separator"></span>
|
||||
<button type="button" onclick="toolbarLink('${textareaId}')" title="Link">🔗</button>
|
||||
`;
|
||||
|
||||
container.insertBefore(toolbar, container.firstChild);
|
||||
}
|
||||
|
||||
// Expose toolbar functions globally
|
||||
window.toolbarBold = toolbarBold;
|
||||
window.toolbarItalic = toolbarItalic;
|
||||
window.toolbarCode = toolbarCode;
|
||||
window.toolbarLink = toolbarLink;
|
||||
window.toolbarList = toolbarList;
|
||||
window.toolbarHeading = toolbarHeading;
|
||||
window.toolbarQuote = toolbarQuote;
|
||||
window.createEditorToolbar = createEditorToolbar;
|
||||
window.insertMarkdownFormat = insertMarkdownFormat;
|
||||
window.insertMarkdownText = insertMarkdownText;
|
||||
|
||||
Reference in New Issue
Block a user