From 8a8045904b2d305e1e13ef013c52175832919a4d Mon Sep 17 00:00:00 2001 From: DevEnv nis2-agile Date: Thu, 18 Jun 2026 19:00:01 +0200 Subject: [PATCH] =?UTF-8?q?[FEAT]=20#384=20p.5=20=E2=80=94=20allegati=20fi?= =?UTF-8?q?le=20su=20Policy=20(sezione=20Allegati=20nel=20dettaglio)=20+?= =?UTF-8?q?=20allowlist=20server-side=20upload=20evidenze?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - policies.html: sezione 'Allegati' nel dettaglio policy (Bootstrap Italia card), upload (entity_type=policy) via /api/audit/evidence/upload, lista con download/autore/data - AuditController::uploadEvidence: allowlist estensioni server-side (anti stored-XSS same-origin) - version.json -> 1.23.5 Co-Authored-By: Claude Opus 4.8 --- application/controllers/AuditController.php | 8 ++ public/policies.html | 100 ++++++++++++++++++++ public/version.json | 2 +- 3 files changed, 109 insertions(+), 1 deletion(-) diff --git a/application/controllers/AuditController.php b/application/controllers/AuditController.php index 37a7048..73703b1 100644 --- a/application/controllers/AuditController.php +++ b/application/controllers/AuditController.php @@ -64,6 +64,14 @@ class AuditController extends BaseController $this->jsonError('File troppo grande (max 10MB)', 400, 'FILE_TOO_LARGE'); } + // Allowlist estensioni (anti stored-XSS same-origin: niente html/svg/js eseguibili). + // Allineata a StakeholderPortalController::ALLOWED_EXT. + $allowedExt = ['pdf','png','jpg','jpeg','gif','webp','txt','csv','xlsx','xls','docx','doc','pptx','ppt','odt','ods','zip']; + $extCheck = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); + if ($extCheck === '' || !in_array($extCheck, $allowedExt, true)) { + $this->jsonError('Tipo di file non consentito. Formati ammessi: PDF, immagini, documenti Office, txt, csv, zip.', 422, 'BAD_FILE_TYPE'); + } + $orgId = $this->getCurrentOrgId(); $uploadDir = UPLOAD_PATH . "/evidence/{$orgId}"; diff --git a/public/policies.html b/public/policies.html index 4d42bc0..46a7376 100644 --- a/public/policies.html +++ b/public/policies.html @@ -627,7 +627,29 @@ + +
+
+

Allegati

+
+
+
+

Caricamento allegati...

+
+
+ + +
+

Formati ammessi: PDF, immagini, documenti Office, txt, csv, zip. Dimensione massima 10 MB.

+
+
`; + loadPolicyAttachments(policy.id); } function backToList() { @@ -637,6 +659,84 @@ currentView = 'list'; } + // ── Allegati Policy (ticket #384 punto 5) ─────────────── + const ATT_ALLOWED_EXT = ['pdf','png','jpg','jpeg','gif','webp','txt','csv','xlsx','xls','docx','doc','pptx','ppt','odt','ods','zip']; + + function fmtFileSize(bytes) { + if (bytes === null || bytes === undefined || bytes === '') return ''; + bytes = Number(bytes); + if (bytes < 1024) return bytes + ' B'; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; + return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; + } + + async function loadPolicyAttachments(policyId) { + const box = document.getElementById('policy-attachments-' + policyId); + if (!box) return; + try { + const headers = { 'Authorization': 'Bearer ' + api.token }; + if (api.orgId) headers['X-Organization-Id'] = api.orgId; + const resp = await fetch(api.baseUrl + '/audit/evidence/list?entity_type=policy&entity_id=' + policyId, { headers }); + const result = await resp.json(); + const files = (result && result.success && Array.isArray(result.data)) ? result.data : []; + if (!files.length) { + box.innerHTML = '

Nessun allegato. Carica un documento di supporto alla policy.

'; + return; + } + box.innerHTML = files.map(f => ` +
+ + ${escapeHtml(f.file_name)} + ${fmtFileSize(f.file_size)} + ${formatDate(f.created_at)}${f.uploaded_by_name ? ' · ' + escapeHtml(f.uploaded_by_name) : ''} +
+ `).join(''); + } catch (e) { + box.innerHTML = '

Errore nel caricamento degli allegati.

'; + } + } + + async function caricaAllegato(policyId) { + const input = document.getElementById('policy-att-file-' + policyId); + if (!input || !input.files || !input.files.length) { + showNotification('Seleziona un file da allegare.', 'warning'); + return; + } + const file = input.files[0]; + const ext = (file.name.split('.').pop() || '').toLowerCase(); + if (!ATT_ALLOWED_EXT.includes(ext)) { + showNotification('Tipo di file non consentito. Formati ammessi: PDF, immagini, documenti Office, txt, csv, zip.', 'error'); + return; + } + if (file.size > 10 * 1024 * 1024) { + showNotification('File troppo grande (max 10 MB).', 'error'); + return; + } + const btn = input.parentElement.querySelector('.btn-primary'); + setButtonLoading(btn, true); + try { + const fd = new FormData(); + fd.append('file', file); + fd.append('entity_type', 'policy'); + fd.append('entity_id', policyId); + const headers = { 'Authorization': 'Bearer ' + api.token }; + if (api.orgId) headers['X-Organization-Id'] = api.orgId; + const resp = await fetch(api.baseUrl + '/audit/evidence/upload', { method: 'POST', headers, body: fd }); + const result = await resp.json(); + setButtonLoading(btn, false); + if (result && result.success) { + showNotification('Allegato caricato con successo.', 'success'); + input.value = ''; + loadPolicyAttachments(policyId); + } else { + showNotification((result && result.message) || 'Errore nel caricamento dell\'allegato.', 'error'); + } + } catch (e) { + setButtonLoading(btn, false); + showNotification('Errore di connessione durante il caricamento.', 'error'); + } + } + // ── Create / Edit Modal ───────────────────────────────── function openCreateModal() { showPolicyFormModal(null); diff --git a/public/version.json b/public/version.json index eacbdf0..916f692 100644 --- a/public/version.json +++ b/public/version.json @@ -1 +1 @@ -{"version": "1.23.4", "build": "2026-06-18-v1.23.4", "date": "2026-06-18", "changelog": "Ticket #384 punto 2.1 (ricodifica): nella pagina 'Misure e Requisiti' i codici delle policy/procedure di default ora si presentano con il prefisso 'Policy.' al posto di 'Proc.' (es. Proc.01 -> Policy.01), in coerenza con l'unificazione lessicale Policy/Procedura del punto 1. Cambia solo l'etichetta mostrata a video: il codice canonico nel catalogo resta invariato come chiave di collegamento requisito<->policy. Restano in lavorazione a fasi: unificazione archivi M:N requisito<->policy (punto 2.2), link attivi bidirezionali tra i due menu (punto 3), allegati nel modale policy (punto 5), matrice RACI assegnabile per policy con R e A obbligatorie in approvazione (punto 6)."} +{"version": "1.23.5", "build": "2026-06-18-v1.23.5", "date": "2026-06-18", "changelog": "Ticket #384 punto 5 (allegati Policy): nel dettaglio di ogni Policy ora c'e' la sezione 'Allegati' per caricare documenti di supporto (PDF, immagini, Office, txt, csv, zip; max 10 MB), elencarli con autore/data e scaricarli. Hardening sicurezza: aggiunta allowlist server-side delle estensioni sull'endpoint di upload evidenze (rifiuto html/svg/js, anti stored-XSS same-origin). Restano in lavorazione a fasi: unificazione archivi M:N requisito<->policy (punto 2.2), link attivi bidirezionali tra i due menu (punto 3), matrice RACI assegnabile per policy con R e A obbligatorie in approvazione (punto 6)."}