[FEAT] #384 p.5 — allegati file su Policy (sezione Allegati nel dettaglio) + allowlist server-side upload evidenze
- 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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
6f3ca4b4bd
commit
8a8045904b
@@ -64,6 +64,14 @@ class AuditController extends BaseController
|
|||||||
$this->jsonError('File troppo grande (max 10MB)', 400, 'FILE_TOO_LARGE');
|
$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();
|
$orgId = $this->getCurrentOrgId();
|
||||||
$uploadDir = UPLOAD_PATH . "/evidence/{$orgId}";
|
$uploadDir = UPLOAD_PATH . "/evidence/{$orgId}";
|
||||||
|
|
||||||
|
|||||||
@@ -627,7 +627,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card" style="margin-top:20px;">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Allegati</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="policy-attachments-${policy.id}" aria-live="polite">
|
||||||
|
<p style="color:var(--gray-500);font-size:0.85rem;margin:0;">Caricamento allegati...</p>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:14px;padding-top:14px;border-top:1px solid var(--gray-200);">
|
||||||
|
<input type="file" id="policy-att-file-${policy.id}" class="form-input" style="flex:1;min-width:220px;"
|
||||||
|
accept=".pdf,.png,.jpg,.jpeg,.gif,.webp,.txt,.csv,.xlsx,.xls,.docx,.doc,.pptx,.ppt,.odt,.ods,.zip"
|
||||||
|
aria-label="Seleziona un file da allegare alla policy" aria-describedby="policy-att-hint-${policy.id}">
|
||||||
|
<button class="btn btn-primary" onclick="caricaAllegato(${policy.id})">
|
||||||
|
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path d="M10 3a1 1 0 01.7.29l3 3a1 1 0 11-1.4 1.42L11 6.4V13a1 1 0 11-2 0V6.4L7.7 7.71a1 1 0 01-1.4-1.42l3-3A1 1 0 0110 3zM4 15a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1z"/></svg>
|
||||||
|
Carica allegato
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p id="policy-att-hint-${policy.id}" style="font-size:0.78rem;color:var(--gray-500);margin:8px 0 0;">Formati ammessi: PDF, immagini, documenti Office, txt, csv, zip. Dimensione massima 10 MB.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
|
loadPolicyAttachments(policy.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function backToList() {
|
function backToList() {
|
||||||
@@ -637,6 +659,84 @@
|
|||||||
currentView = 'list';
|
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 = '<p style="color:var(--gray-500);font-size:0.85rem;margin:0;">Nessun allegato. Carica un documento di supporto alla policy.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
box.innerHTML = files.map(f => `
|
||||||
|
<div style="display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid var(--gray-100);">
|
||||||
|
<svg viewBox="0 0 20 20" fill="var(--primary, #0066CC)" width="18" height="18" style="flex-shrink:0;"><path d="M8 2a2 2 0 00-2 2v12a2 2 0 002 2h8a2 2 0 002-2V7.414A2 2 0 0017.414 6L14 2.586A2 2 0 0012.586 2H8z"/></svg>
|
||||||
|
<a href="/uploads/${escapeHtml(f.file_path)}" target="_blank" rel="noopener" style="flex:1;word-break:break-all;">${escapeHtml(f.file_name)}</a>
|
||||||
|
<span style="color:var(--gray-500);font-size:0.78rem;white-space:nowrap;">${fmtFileSize(f.file_size)}</span>
|
||||||
|
<span style="color:var(--gray-500);font-size:0.78rem;white-space:nowrap;">${formatDate(f.created_at)}${f.uploaded_by_name ? ' · ' + escapeHtml(f.uploaded_by_name) : ''}</span>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
} catch (e) {
|
||||||
|
box.innerHTML = '<p style="color:var(--danger, #b91c1c);font-size:0.85rem;margin:0;">Errore nel caricamento degli allegati.</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ─────────────────────────────────
|
// ── Create / Edit Modal ─────────────────────────────────
|
||||||
function openCreateModal() {
|
function openCreateModal() {
|
||||||
showPolicyFormModal(null);
|
showPolicyFormModal(null);
|
||||||
|
|||||||
+1
-1
@@ -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)."}
|
||||||
|
|||||||
Reference in New Issue
Block a user