diff --git a/ajax/verify_signature.php b/ajax/verify_signature.php new file mode 100644 index 0000000..e7ae199 --- /dev/null +++ b/ajax/verify_signature.php @@ -0,0 +1,102 @@ +hasRight('bericht', 'read')) bericht_ajax_fail('Permission denied', 403); + +$pageid = (int) ($_POST['pageid'] ?? $_GET['pageid'] ?? 0); +if (!$pageid) bericht_ajax_fail('pageid fehlt'); + +$sqlr = $db->query("SELECT rowid, fk_bericht, source_path FROM ".$db->prefix()."bericht_page WHERE rowid = ".$pageid); +if (!$sqlr || !($row = $db->fetch_object($sqlr))) bericht_ajax_fail('Seite nicht gefunden', 404); + +$full = bericht_resolve_data_path($row->source_path); +if (!$full || !file_exists($full)) bericht_ajax_fail('Datei nicht gefunden', 404); + +$meta_path = $full.'.meta.json'; +if (!file_exists($meta_path)) { + bericht_ajax_ok(array( + 'verified' => false, + 'reason' => 'Keine Metadaten-Datei — diese Seite ist keine aufgewertete Unterschrift', + )); +} + +$meta = json_decode(file_get_contents($meta_path), true); +if (!is_array($meta) || empty($meta['chain_hash_sha256'])) { + bericht_ajax_ok(array( + 'verified' => false, + 'reason' => 'Metadaten ungültig oder kein Hash enthalten', + )); +} + +// Wir brauchen den Bericht +$bericht = new Bericht($db); +if ($bericht->fetch($row->fk_bericht) <= 0) bericht_ajax_fail('Bericht nicht gefunden', 404); + +// Die zum Signaturzeitpunkt existierenden Seiten: alle mit page_order < unserer. +// Sort nach page_order wie beim Signieren. +$prev_pages = BerichtPage::fetchAllForBericht($db, $row->fk_bericht); +// Nur die Seiten die VOR der Signatur waren — wir nehmen die ersten N entsprechend meta.page_count_at_signing +$expected_count = (int) ($meta['page_count_at_signing'] ?? 0); +$to_hash = array_slice($prev_pages, 0, $expected_count); + +$current_count = 0; +foreach ($prev_pages as $pp) { + if ($pp->id == $row->rowid) break; + $current_count++; +} + +// Hash nachrechnen +$hash_ctx = hash_init('sha256'); +hash_update($hash_ctx, 'bericht:'.$bericht->id.'|ref:'.$bericht->ref); +foreach ($to_hash as $pp) { + $full_pp = bericht_resolve_data_path($pp->source_path); + if ($full_pp && file_exists($full_pp)) { + hash_update_file($hash_ctx, $full_pp); + } + hash_update($hash_ctx, '|order:'.$pp->page_order.'|note:'.($pp->note ?? '')); +} +$current_hash = hash_final($hash_ctx); +$stored_hash = $meta['chain_hash_sha256']; + +$verified = hash_equals($stored_hash, $current_hash); + +$result = array( + 'verified' => $verified, + 'stored_hash' => $stored_hash, + 'current_hash' => $current_hash, + 'expected_page_count' => $expected_count, + 'current_page_count' => $current_count, + 'meta' => array( + 'signer_name' => $meta['signer_name'] ?? '', + 'signed_at' => $meta['signed_at'] ?? '', + 'user_login' => $meta['user_login'] ?? '', + 'kunde' => $meta['kunde'] ?? '', + 'parent_ref' => $meta['parent_ref'] ?? '', + 'gps_lat' => $meta['gps_lat'] ?? null, + 'gps_lon' => $meta['gps_lon'] ?? null, + 'remote_ip' => $meta['remote_ip'] ?? '', + ), +); + +if (!$verified) { + if ($current_count !== $expected_count) { + $result['reason'] = "Seitenanzahl hat sich geändert ($expected_count → $current_count)"; + } else { + $result['reason'] = "Hash der Vorseiten stimmt nicht — eine Seite wurde nach der Unterschrift verändert"; + } +} + +bericht_ajax_ok($result); diff --git a/api/pages.php b/api/pages.php index 1bf253f..29ae2c0 100644 --- a/api/pages.php +++ b/api/pages.php @@ -17,6 +17,24 @@ if (!$user->hasRight('bericht', 'write')) api_fail('Permission denied', 403); $method = $_SERVER['REQUEST_METHOD']; $action = $_GET['action'] ?? ''; +/* ---------- Seiten umsortieren ---------- */ +if ($method === 'POST' && $action === 'reorder') { + $in = api_input(); + $ids = $in['order'] ?? null; + if (!is_array($ids) || empty($ids)) api_fail('order-Array fehlt'); + $db->begin(); + foreach ($ids as $pos => $pageid) { + $pid = (int) $pageid; + if ($pid <= 0) continue; + if (!$db->query("UPDATE ".$db->prefix()."bericht_page SET page_order = ".((int) ($pos + 1))." WHERE rowid = ".$pid)) { + $db->rollback(); + api_fail($db->lasterror(), 500); + } + } + $db->commit(); + api_ok(); +} + /* ---------- Unterschrift als neue Seite anhängen ---------- */ if ($method === 'POST' && $action === 'signature') { $bericht_id = (int) ($_GET['bericht_id'] ?? 0); diff --git a/bericht_card.php b/bericht_card.php index a310bf9..1c51a00 100644 --- a/bericht_card.php +++ b/bericht_card.php @@ -267,6 +267,7 @@ if (!$bericht) { 'set_slot_image' => dol_buildpath('/bericht/ajax/set_slot_image.php', 1), 'create_upload_token' => dol_buildpath('/bericht/ajax/create_upload_token.php', 1), 'list_pages' => dol_buildpath('/bericht/ajax/list_pages.php', 1), + 'verify_signature' => dol_buildpath('/bericht/ajax/verify_signature.php', 1), ), 'lang' => array( 'undo' => $langs->trans("BerichtUndo"), @@ -456,12 +457,25 @@ if (!$bericht) { print ''; print '
'; foreach ($pages as $idx => $p) { - print '
'; + // Ist das eine gehärtete Unterschrift? (hat .meta.json neben der source_path) + $is_signature = false; + if ($p->source_path && strpos($p->note ?? '', 'Unterschrift') === 0) { + $full_check = DOL_DATA_ROOT.'/'.$p->source_path; + if (file_exists($full_check.'.meta.json')) { + $is_signature = true; + } + } + print '
'; print '
'; print ''; print '
'; - print '
'.($idx + 1).'
'; + print '
'.($idx + 1).'' + .($is_signature ? ' 🔒' : '') + .'
'; print '
'; + if ($is_signature) { + print ''; + } print ''; print '
'; print '
'; diff --git a/css/bericht.css b/css/bericht.css index a2cc07f..375f3d6 100644 --- a/css/bericht.css +++ b/css/bericht.css @@ -249,7 +249,7 @@ } .page-thumb-actions { position: absolute; top: 4px; right: 4px; } -.thumb-del { +.thumb-del, .thumb-verify { background: rgba(0,0,0,0.6); color: #fff; border: 1px solid rgba(255,255,255,0.3); @@ -257,8 +257,19 @@ cursor: pointer; padding: 2px 6px; font-size: 12px; + margin-left: 2px; } .thumb-del:hover { background: rgba(217,83,79,0.9); } +.thumb-verify:hover { background: rgba(92,184,92,0.9); } + +.page-thumb.page-signature { + border-color: #5cb85c; + box-shadow: 0 0 0 1px rgba(92,184,92,0.4); +} +.page-thumb.page-signature .sig-badge { + margin-left: 4px; + font-size: 12px; +} .bericht-actions { margin-top: 16px; diff --git a/js/editor.js b/js/editor.js index cff96b6..6661127 100644 --- a/js/editor.js +++ b/js/editor.js @@ -857,6 +857,80 @@ list.classList.toggle('paper-light'); list.classList.toggle('paper-dark'); }); + + // Unterschriften-Verifikation + document.querySelectorAll('.thumb-verify').forEach(btn => { + btn.addEventListener('click', async (e) => { + e.stopPropagation(); + const pageid = btn.dataset.pageid; + btn.textContent = '⏳'; + try { + const fd = new FormData(); + fd.append('token', cfg.token); + fd.append('pageid', pageid); + const r = await fetch(cfg.urls.verify_signature, { method: 'POST', body: fd }); + const data = await r.json(); + btn.textContent = '🔍'; + if (!data.success) { + alert('Fehler: ' + (data.error || 'unbekannt')); + return; + } + showSignatureVerifyResult(data); + } catch (err) { + btn.textContent = '🔍'; + alert('Netzwerkfehler: ' + err.message); + } + }); + }); + } + + /** + * Zeigt das Ergebnis der Unterschriften-Verifikation in einem Modal. + */ + function showSignatureVerifyResult(data) { + const verified = data.verified; + const m = data.meta || {}; + const icon = verified ? '✅' : '⚠️'; + const title = verified ? 'Unterschrift verifiziert' : 'Unterschrift NICHT verifiziert'; + const bg = verified ? '#5cb85c' : '#d9534f'; + + const html = ` +
+
+
+
+

${icon} ${escapeHtml(title)}

+ +
+
+ ${!verified ? `

${escapeHtml(data.reason || '')}

` : ''} + + + + + + + ${m.gps_lat ? `` : ''} + + + + + +
Unterzeichner:${escapeHtml(m.signer_name || '—')}
Kunde:${escapeHtml(m.kunde || '')}
Parent:${escapeHtml(m.parent_ref || '')}
Signiert am:${escapeHtml(m.signed_at || '')}
Erfasst durch:${escapeHtml(m.user_login || '')}
GPS:${m.gps_lat}, ${m.gps_lon}
IP:${escapeHtml(m.remote_ip || '')}
Gespeicherter Hash:${escapeHtml(data.stored_hash || '')}
Aktueller Hash:${escapeHtml(data.current_hash || '')}
Seiten bei Signatur:${data.expected_page_count}
Seiten jetzt vor Signatur:${data.current_page_count}
+
+
+
+ `; + const wrapper = document.createElement('div'); + wrapper.innerHTML = html; + document.body.appendChild(wrapper.firstElementChild); + document.getElementById('sig-verify-close').onclick = () => { + document.getElementById('sig-verify-modal').remove(); + }; + } + + function escapeHtml(s) { + return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); } /**