diff --git a/api/webhooks/jira.php b/api/webhooks/jira.php index ad3a800..bbc9701 100644 --- a/api/webhooks/jira.php +++ b/api/webhooks/jira.php @@ -4,16 +4,28 @@ * Ontvangt webhook events van Atlassian Jira Cloud (issue_created, issue_updated, etc.) */ +require_once __DIR__ . '/../../config/config.php'; require_once __DIR__ . '/../../config/database.php'; header('Content-Type: application/json'); +// Beveiligingscontrole: Verifieer geheime webhook token indien geconfigureerd in .env +$configuredSecret = fociz_env('JIRA_WEBHOOK_SECRET', ''); +if (!empty($configuredSecret)) { + $incomingSecret = $_GET['secret'] ?? ($_SERVER['HTTP_X_ATLASSIAN_WEBHOOK_SECRET'] ?? ''); + if (!hash_equals($configuredSecret, $incomingSecret)) { + http_response_code(401); + echo json_encode(['error' => 'Onbevoegde webhook aanroep']); + exit; + } +} + $rawPayload = file_get_contents('php://input'); $data = json_decode($rawPayload, true); if (!$data) { http_response_code(400); - echo json_encode(['error' => 'Invalid JSON payload']); + echo json_encode(['error' => 'Ongeldige JSON payload']); exit; } diff --git a/config/config.php b/config/config.php index fd30565..04a051c 100644 --- a/config/config.php +++ b/config/config.php @@ -17,6 +17,9 @@ if (!headers_sent()) { header('X-XSS-Protection: 1; mode=block'); header('Referrer-Policy: strict-origin-when-cross-origin'); header('Permissions-Policy: camera=(), microphone=(), geolocation=()'); + if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') { + header('Strict-Transport-Security: max-age=31536000; includeSubDomains'); + } header_remove('X-Powered-By'); } @ini_set('expose_php', 'off'); diff --git a/includes/integrations/jira.php b/includes/integrations/jira.php index f88d5a7..4b93189 100644 --- a/includes/integrations/jira.php +++ b/includes/integrations/jira.php @@ -221,6 +221,7 @@ function jira_fetch_issues(string $domain, string $email, string $apiToken, stri $authHeader = jira_get_auth_header($email, $apiToken); $fieldsList = [ + '*navigable', 'summary', 'status', 'priority', @@ -233,6 +234,8 @@ function jira_fetch_issues(string $domain, string $email, string $apiToken, stri 'updated', 'created', 'description', + 'labels', + 'components', ]; // 1. Moderne Atlassian Jira Cloud endpoint (/rest/api/3/search/jql) conform CHANGE-2046 via POST @@ -325,29 +328,67 @@ function jira_fetch_issues(string $domain, string $email, string $apiToken, stri $descriptionText = jira_adf_to_text($fields['description'] ?? ''); - // Detectie van custom field "Onderwerp logging" + // Detectie van custom field "Onderwerp logging" of routeringsveld $onderwerpLogging = ''; + $knownTopics = ['koppelingen', 'sso-koppeling', 'rest-endpoints', 'datadump', 'uwv-koppeling', 'migratie', 'incident', 'vraag', 'algemeen']; + foreach ($fields as $fKey => $fVal) { if (str_starts_with($fKey, 'customfield_')) { - if (is_array($fVal) && isset($fVal['value']) && is_string($fVal['value'])) { - $valStr = trim($fVal['value']); - $known = ['koppelingen', 'sso-koppeling', 'rest-endpoints', 'datadump', 'uwv-koppeling', 'migratie', 'incident', 'vraag', 'algemeen']; - if (in_array(strtolower($valStr), $known)) { - $onderwerpLogging = $valStr; + if (is_array($fVal)) { + if (isset($fVal['value']) && is_string($fVal['value'])) { + $valStr = trim($fVal['value']); + if (!empty($valStr)) { + foreach ($knownTopics as $kt) { + if (str_contains(strtolower($valStr), $kt)) { + $onderwerpLogging = $valStr; + break 2; + } + } + if (empty($onderwerpLogging) && (stripos($fKey, 'onderwerp') !== false || stripos($fKey, 'logging') !== false)) { + $onderwerpLogging = $valStr; + } + } } } elseif (is_string($fVal) && !empty($fVal)) { $valStr = trim($fVal); - $known = ['koppelingen', 'sso-koppeling', 'rest-endpoints', 'datadump', 'uwv-koppeling', 'migratie']; - if (in_array(strtolower($valStr), $known)) { - $onderwerpLogging = $valStr; + foreach ($knownTopics as $kt) { + if (str_contains(strtolower($valStr), $kt)) { + $onderwerpLogging = $valStr; + break 2; + } } } } } + if (empty($onderwerpLogging) && !empty($fields['onderwerp_logging'])) { $onderwerpLogging = is_string($fields['onderwerp_logging']) ? $fields['onderwerp_logging'] : ($fields['onderwerp_logging']['value'] ?? ''); } + // Fallback: kijk naar labels & componenten + if (empty($onderwerpLogging) && !empty($fields['labels']) && is_array($fields['labels'])) { + foreach ($fields['labels'] as $lbl) { + $lblLower = strtolower(trim($lbl)); + foreach ($knownTopics as $kt) { + if (str_contains($lblLower, $kt)) { + $onderwerpLogging = ucfirst($kt); + break 2; + } + } + } + } + + // Fallback: detecteer onderwerp op basis van steekwoorden in summary + if (empty($onderwerpLogging)) { + $sumLower = strtolower($summary); + if (str_contains($sumLower, 'sso')) $onderwerpLogging = 'SSO-koppeling'; + elseif (str_contains($sumLower, 'endpoint') || str_contains($sumLower, 'rest') || str_contains($sumLower, 'api')) $onderwerpLogging = 'Rest-endpoints'; + elseif (str_contains($sumLower, 'uwv')) $onderwerpLogging = 'UWV-koppeling'; + elseif (str_contains($sumLower, 'datadump') || str_contains($sumLower, 'dump')) $onderwerpLogging = 'Datadump'; + elseif (str_contains($sumLower, 'migratie')) $onderwerpLogging = 'Migratie'; + elseif (str_contains($sumLower, 'koppeling')) $onderwerpLogging = 'Koppelingen'; + } + $issues[] = [ 'key' => $key, 'summary' => $summary, diff --git a/jira.php b/jira.php index e9cdbf6..76b0e88 100644 --- a/jira.php +++ b/jira.php @@ -320,6 +320,17 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT 0 + + + + = htmlspecialchars($qf['filter_name'] ?? $code) ?> + 0 + + + @@ -677,6 +688,7 @@ let currentProject = ''; let currentQueue = jiraData.default_queue_filter || ''; let activeUrgentOnly = false; let expandedKeys = new Set(); +const ticketStore = new Map(); // Bereken statussen en KPI statistieken function calculateKpis() { @@ -758,9 +770,10 @@ function onProjectSelectChange(proj) { function updateProjectPills() { const pills = document.querySelectorAll('.project-pill'); + const curNorm = (currentProject || '').trim().toLowerCase(); pills.forEach(p => { - const pKey = p.getAttribute('data-project') || ''; - if (pKey === currentProject) { + const pKey = (p.getAttribute('data-project') || '').trim().toLowerCase(); + if (pKey === curNorm) { p.className = "project-pill flex items-center space-x-2 px-3.5 py-2 rounded-xl text-xs font-bold transition cursor-pointer bg-indigo-600 text-white shadow-sm shadow-indigo-600/20 flex-shrink-0"; } else { p.className = "project-pill flex items-center space-x-2 px-3.5 py-2 rounded-xl text-xs font-bold transition cursor-pointer bg-slate-100 dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700/80 border border-slate-200/80 dark:border-slate-700/80 flex-shrink-0"; @@ -768,6 +781,40 @@ function updateProjectPills() { }); } +// Hulpfunctie: Bepaal of een ticket tot een specifieke wachtrij behoort (DX, SD of aangepast) +function isIssueInQueue(iss, qCode) { + if (!qCode) return true; + const code = qCode.toUpperCase(); + const ol = (iss.onderwerp_logging || '').toLowerCase().trim(); + const sum = (iss.summary || '').toLowerCase(); + const pKey = (iss.project_key || '').toUpperCase().trim(); + + const dxTerms = ['koppeling', 'sso', 'endpoint', 'rest', 'datadump', 'uwv', 'migratie', 'api', 'webhook', 'integrat']; + const matchesDx = dxTerms.some(t => ol.includes(t) || sum.includes(t)); + + if (code === 'DX') { + return matchesDx; + } + if (code === 'SD') { + // SD is regulier Servicedesk (alle tickets die niet tot DX routering behoren) + return !matchesDx; + } + + // Aangepaste wachtrijfilter uit de database + const fObj = (jiraData.queue_filters || []).find(f => f.filter_code.toUpperCase() === code); + if (fObj) { + if (fObj.project_key && pKey && fObj.project_key.toUpperCase() !== pKey) { + return false; + } + const clause = (fObj.jql_clause || '').toLowerCase(); + if (ol && (clause.includes(ol) || ol.includes(code.toLowerCase()))) { + return true; + } + if (clause.includes('koppeling') && matchesDx) return true; + } + return true; +} + // Wachtrijfilter selectie function selectQueueFilter(code) { currentQueue = code; @@ -796,18 +843,34 @@ function updateQueueCounts() { let currentList = all; if (currentTab === 'assigned') currentList = assigned; else if (currentTab === 'team') currentList = team; - - if (currentProject) { - currentList = currentList.filter(iss => iss.project === currentProject); + else if (currentTab === 'in_progress') { + currentList = all.filter(iss => { + const s = (iss.status || '').toLowerCase(); + const sc = (iss.status_category || '').toLowerCase(); + return s.includes('progress') || s.includes('bezig') || s.includes('review') || sc === 'indeterminate'; + }); + } else if (currentTab === 'done') { + currentList = all.filter(iss => { + const s = (iss.status || '').toLowerCase(); + const sc = (iss.status_category || '').toLowerCase(); + return s.includes('done') || s.includes('afgerond') || s.includes('gesloten') || sc === 'done'; + }); + } + + if (currentProject) { + const pNorm = currentProject.trim().toLowerCase(); + currentList = currentList.filter(iss => { + const pName = (iss.project || '').trim().toLowerCase(); + const pKey = (iss.project_key || '').trim().toLowerCase(); + return pName === pNorm || pKey === pNorm; + }); } - const dxValues = ['koppelingen', 'sso-koppeling', 'rest-endpoints', 'datadump', 'uwv-koppeling', 'migratie']; let dxCount = 0; let sdCount = 0; currentList.forEach(iss => { - const ol = (iss.onderwerp_logging || '').toLowerCase().trim(); - if (dxValues.includes(ol)) { + if (isIssueInQueue(iss, 'DX')) { dxCount++; } else { sdCount++; @@ -821,6 +884,14 @@ function updateQueueCounts() { if (elAll) elAll.innerText = currentList.length; if (elDx) elDx.innerText = dxCount; if (elSd) elSd.innerText = sdCount; + + (jiraData.queue_filters || []).forEach(f => { + const el = document.getElementById(`queueCount_${f.filter_code}`); + if (el && f.filter_code !== 'DX' && f.filter_code !== 'SD') { + const cnt = currentList.filter(iss => isIssueInQueue(iss, f.filter_code)).length; + el.innerText = cnt; + } + }); } // Snelkoppeling KPI-kaart Urgent @@ -869,9 +940,9 @@ function resetAllFilters() { function applyFilters() { const searchVal = (document.getElementById('jiraSearchInput')?.value || '').trim().toLowerCase(); const projectVal = currentProject || (document.getElementById('filterProject')?.value || ''); - const typeVal = document.getElementById('filterType')?.value || ''; - const priorityVal = document.getElementById('filterPriority')?.value || ''; - const statusVal = document.getElementById('filterStatus')?.value || ''; + const typeVal = (document.getElementById('filterType')?.value || '').trim().toLowerCase(); + const priorityVal = (document.getElementById('filterPriority')?.value || '').trim(); + const statusVal = (document.getElementById('filterStatus')?.value || '').trim().toLowerCase(); const clearBtn = document.getElementById('clearSearchBtn'); if (clearBtn) { @@ -902,55 +973,54 @@ function applyFilters() { }); } - const dxValues = ['koppelingen', 'sso-koppeling', 'rest-endpoints', 'datadump', 'uwv-koppeling', 'migratie']; - // Pas zoek-, project-, wachtrij-, type-, prioriteit- en statusfilters toe const filtered = sourceList.filter(iss => { if (searchVal) { const keyMatch = (iss.key || '').toLowerCase().includes(searchVal); const sumMatch = (iss.summary || '').toLowerCase().includes(searchVal); const prjMatch = (iss.project || '').toLowerCase().includes(searchVal); + const prjKeyMatch = (iss.project_key || '').toLowerCase().includes(searchVal); const descMatch = (iss.description || '').toLowerCase().includes(searchVal); const repMatch = (iss.reporter_name || '').toLowerCase().includes(searchVal); + const assMatch = (iss.assignee_name || '').toLowerCase().includes(searchVal); const olMatch = (iss.onderwerp_logging || '').toLowerCase().includes(searchVal); - if (!keyMatch && !sumMatch && !prjMatch && !descMatch && !repMatch && !olMatch) return false; + if (!keyMatch && !sumMatch && !prjMatch && !prjKeyMatch && !descMatch && !repMatch && !assMatch && !olMatch) return false; } - if (projectVal && iss.project !== projectVal) { - return false; - } - - if (currentQueue) { - const qCode = currentQueue.toUpperCase(); - const ol = (iss.onderwerp_logging || '').toLowerCase().trim(); - - if (qCode === 'DX') { - if (!dxValues.includes(ol)) return false; - } else if (qCode === 'SD') { - if (ol !== '' && ol !== 'empty' && dxValues.includes(ol)) return false; - } else { - const fObj = (jiraData.queue_filters || []).find(f => f.filter_code.toUpperCase() === qCode); - if (fObj) { - if (ol && !ol.includes(qCode.toLowerCase()) && !fObj.jql_clause.toLowerCase().includes(ol)) { - return false; - } - } - } - } - - if (typeVal && iss.issue_type !== typeVal) { - return false; - } - - if (priorityVal) { - if (priorityVal === 'High') { - if (iss.priority !== 'High' && iss.priority !== 'Highest') return false; - } else if (iss.priority !== priorityVal) { + if (projectVal) { + const pNorm = projectVal.trim().toLowerCase(); + const pName = (iss.project || '').trim().toLowerCase(); + const pKey = (iss.project_key || '').trim().toLowerCase(); + if (pName !== pNorm && pKey !== pNorm) { return false; } } - if (statusVal && iss.status !== statusVal) { + if (currentQueue) { + if (!isIssueInQueue(iss, currentQueue)) { + return false; + } + } + + if (typeVal && (iss.issue_type || '').toLowerCase() !== typeVal) { + return false; + } + + if (priorityVal) { + const pVal = priorityVal.toLowerCase(); + const ip = (iss.priority || '').toLowerCase(); + if (pVal === 'high' || pVal === 'highest' || pVal === 'p1') { + if (!['highest', 'high', 'hoogste', 'hoog', 'urgent', 'p1'].includes(ip)) return false; + } else if (pVal === 'medium' || pVal === 'p2') { + if (!['medium', 'gemiddeld', 'normaal', 'p2'].includes(ip)) return false; + } else if (pVal === 'low' || pVal === 'lowest' || pVal === 'p3') { + if (!['low', 'lowest', 'laag', 'laagste', 'p3'].includes(ip)) return false; + } else if (ip !== pVal) { + return false; + } + } + + if (statusVal && (iss.status || '').toLowerCase() !== statusVal) { return false; } @@ -1010,6 +1080,7 @@ function renderTickets(tickets) { let html = ''; tickets.forEach(ticket => { + ticketStore.set(ticket.key, ticket); // Issue type styling & icon let typeBadgeClass = 'bg-sky-50 text-sky-700 dark:bg-sky-950/40 dark:text-sky-400 border-sky-200 dark:border-sky-800/60'; let typeIcon = 'check-square'; @@ -1118,9 +1189,6 @@ function renderTickets(tickets) { const detailsClass = isExpanded ? '' : 'hidden'; const chevronRotateClass = isExpanded ? 'rotate-180' : ''; - // JSON stringified ticket data voor modalaanroep - const ticketSafeJson = JSON.stringify(ticket).replace(/"/g, '"'); - html += ` @@ -1187,12 +1255,12 @@ function renderTickets(tickets) { - + Plan in Dagplanner - + Naar Bord @@ -1263,11 +1331,11 @@ function renderTickets(tickets) { - + Inplannen in Dagplanner - + Naar Kanban Bord @@ -1289,7 +1357,12 @@ function renderTickets(tickets) { } // Modal 1: Plan in Dagplanner -function openPlanModal(ticket) { +function openPlanModal(item) { + const ticket = (typeof item === 'string') + ? (ticketStore.get(item) || (jiraData.assigned_issues || []).concat(jiraData.team_issues || []).find(t => t.key === item)) + : item; + if (!ticket) return; + document.getElementById('planIssueKey').value = ticket.key; document.getElementById('planIssueSummary').value = ticket.summary; document.getElementById('planIssueUrl').value = ticket.url; @@ -1378,7 +1451,12 @@ async function submitPlanTask(e) { } // Modal 2: Plaats op Kanban Bord -function openBoardModal(ticket) { +function openBoardModal(item) { + const ticket = (typeof item === 'string') + ? (ticketStore.get(item) || (jiraData.assigned_issues || []).concat(jiraData.team_issues || []).find(t => t.key === item)) + : item; + if (!ticket) return; + document.getElementById('boardIssueKey').value = ticket.key; document.getElementById('boardIssueSummary').value = ticket.summary; document.getElementById('boardIssueUrl').value = ticket.url; diff --git a/mail.php b/mail.php index 9577c8d..6202cfc 100644 --- a/mail.php +++ b/mail.php @@ -535,7 +535,7 @@ async function selectMail(id) { const res = await fetch(`/api/mail.php?action=get_message&id=${encodeURIComponent(id)}`); const data = await res.json(); if (data.success && data.message) { - bodyEl.innerHTML = data.message.body_html || `${escapeHtml(data.message.body_text || found.body_preview)}`; + renderMailBodyContent(bodyEl, data.message.body_html, data.message.body_text || found.body_preview); found.is_read = true; updateUnreadCount(); renderMailList(allMessages); @@ -543,10 +543,123 @@ async function selectMail(id) { bodyEl.innerHTML = `${data.error || 'Kon bericht niet inladen.'}`; } } catch(err) { - bodyEl.innerHTML = `${escapeHtml(found.body_preview)}`; + renderMailBodyContent(bodyEl, null, found.body_preview); } } +// Grondige HTML sanitization voor inkomende e-mails (XSS & exploit preventie) +function sanitizeEmailHtml(html) { + if (!html) return ''; + + // 1. Verwijder alle script tags en inhoud + let cleaned = html.replace(/
${escapeHtml(data.message.body_text || found.body_preview)}
${escapeHtml(found.body_preview)}