All checks were successful
Deploy bericht / deploy (push) Successful in 1s
ajax/verify_signature.php: - Liest Metadaten aus .meta.json neben einer Unterschrift-Seite - Rechnet SHA256 über die zum Signaturzeitpunkt existierenden Seiten (erste N laut meta.page_count_at_signing) - Vergleicht mit dem gespeicherten Hash → verified true/false - Liefert reason bei Fehlschlag (Seitenanzahl oder Inhalt geändert) bericht_card.php: - Seiten mit .meta.json kriegen 🔒-Badge + grünen Rand - 🔍-Button neben dem Löschen-Button öffnet Verifikations-Modal editor.js: - showSignatureVerifyResult-Modal zeigt alle Metadaten, beide Hashes, GPS als OpenStreetMap-Link, IP, User, Zeit - Grün/Rot je nach Verifikationsergebnis api/pages.php: - Neuer Action reorder: POST mit {order:[ids]} schreibt neue page_order-Werte als Transaktion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> [deploy]
236 lines
9.9 KiB
PHP
236 lines
9.9 KiB
PHP
<?php
|
|
/* Page-Operationen für Bericht-Seiten (PWA).
|
|
*
|
|
* DELETE /api/pages.php?id=<page_id> — Seite löschen
|
|
* POST /api/pages.php?id=<page_id> — Body { note: "..." } — Notiz setzen
|
|
* Body { rotation: 90 } — rotieren
|
|
* POST /api/pages.php?action=signature&bericht_id=<id>
|
|
* multipart: file=<PNG> — Unterschrift als neue Seite am Bericht anhängen
|
|
*/
|
|
require_once __DIR__.'/_inc.php';
|
|
|
|
api_authenticate();
|
|
global $db, $user;
|
|
|
|
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);
|
|
if (!$bericht_id) api_fail('bericht_id fehlt');
|
|
if (empty($_FILES['file']['tmp_name'])) api_fail('file fehlt');
|
|
|
|
$bericht = new Bericht($db);
|
|
if ($bericht->fetch($bericht_id) <= 0) api_fail('Bericht nicht gefunden', 404);
|
|
|
|
// Parent-Objekt (Kunde) holen — wir brauchen den Kundennamen für die Metadaten
|
|
$parent = bericht_fetch_parent($db, $bericht->element_type, $bericht->fk_element);
|
|
$kundename = $parent && !empty($parent->thirdparty) ? ($parent->thirdparty->name ?? '') : '';
|
|
$parent_ref = $parent ? $parent->ref : '';
|
|
|
|
// Zusatz-Felder vom Client
|
|
$signer_name = trim((string) ($_POST['signer_name'] ?? ''));
|
|
$gps_lat = isset($_POST['gps_lat']) ? (float) $_POST['gps_lat'] : null;
|
|
$gps_lon = isset($_POST['gps_lon']) ? (float) $_POST['gps_lon'] : null;
|
|
|
|
require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
|
|
$workdir = DOL_DATA_ROOT.'/bericht/work/'.$bericht_id;
|
|
if (!is_dir($workdir)) dol_mkdir($workdir);
|
|
|
|
// Hash-Verkettung: SHA256 über alle vorherigen Seiten-Quelldateien
|
|
$prev_pages = BerichtPage::fetchAllForBericht($db, $bericht_id);
|
|
$hash_ctx = hash_init('sha256');
|
|
hash_update($hash_ctx, 'bericht:'.$bericht_id.'|ref:'.$bericht->ref);
|
|
foreach ($prev_pages 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 ?? ''));
|
|
}
|
|
$chain_hash = hash_final($hash_ctx);
|
|
|
|
// Rohe Unterschrift vom Upload
|
|
$raw_sig_png = file_get_contents($_FILES['file']['tmp_name']);
|
|
if (!$raw_sig_png) api_fail('Upload fehlgeschlagen', 500);
|
|
$sig_img = @imagecreatefromstring($raw_sig_png);
|
|
if (!$sig_img) api_fail('PNG ungültig');
|
|
$sig_w = imagesx($sig_img);
|
|
$sig_h = imagesy($sig_img);
|
|
|
|
// Composite-Canvas: Metadaten oben, Unterschrift in der Mitte, Footer mit Hash unten
|
|
$canvas_w = max($sig_w, 1200);
|
|
$header_h = 180;
|
|
$footer_h = 120;
|
|
$sig_area_h = 500;
|
|
$canvas_h = $header_h + $sig_area_h + $footer_h;
|
|
$canvas = imagecreatetruecolor($canvas_w, $canvas_h);
|
|
|
|
$white = imagecolorallocate($canvas, 255, 255, 255);
|
|
$black = imagecolorallocate($canvas, 0, 0, 0);
|
|
$grey = imagecolorallocate($canvas, 120, 120, 120);
|
|
$line_grey = imagecolorallocate($canvas, 200, 200, 200);
|
|
$dark_blue = imagecolorallocate($canvas, 51, 122, 183);
|
|
imagefill($canvas, 0, 0, $white);
|
|
|
|
// Header-Box
|
|
imagefilledrectangle($canvas, 0, 0, $canvas_w, $header_h, imagecolorallocate($canvas, 245, 248, 252));
|
|
imagerectangle($canvas, 0, 0, $canvas_w - 1, $header_h, $line_grey);
|
|
|
|
// Überschrift
|
|
$title = 'ABNAHMEUNTERSCHRIFT';
|
|
imagestring($canvas, 5, 20, 15, $title, $dark_blue);
|
|
|
|
// Metadaten Zeile 1
|
|
$line1 = 'Bericht: '.$bericht->ref.' · '.trim($bericht->element_type.' '.$parent_ref);
|
|
imagestring($canvas, 4, 20, 50, $line1, $black);
|
|
// Zeile 2: Kunde
|
|
$line2 = 'Kunde: '.$kundename;
|
|
imagestring($canvas, 4, 20, 75, $line2, $black);
|
|
// Zeile 3: Datum + Zeit (Server) + Unterzeichner
|
|
$datetime = date('Y-m-d H:i:s');
|
|
$line3 = 'Zeit (Server): '.$datetime.' · Unterzeichner: '.($signer_name ?: '—');
|
|
imagestring($canvas, 4, 20, 100, $line3, $black);
|
|
// Zeile 4: GPS
|
|
if ($gps_lat !== null && $gps_lon !== null) {
|
|
$line4 = sprintf('GPS: %.6f, %.6f', $gps_lat, $gps_lon);
|
|
imagestring($canvas, 3, 20, 125, $line4, $black);
|
|
}
|
|
// Bestätigungstext
|
|
$bestaetigung = 'Ich bestaetige hiermit die ordnungsgemaesse Ausfuehrung der dokumentierten Arbeiten.';
|
|
imagestring($canvas, 3, 20, 150, $bestaetigung, $black);
|
|
|
|
// Unterschrift-Bereich mittig einsetzen, auf Breite skalieren
|
|
$max_sig_w = $canvas_w - 80;
|
|
$max_sig_h = $sig_area_h - 60;
|
|
$ratio = min($max_sig_w / $sig_w, $max_sig_h / $sig_h);
|
|
$dw = (int) ($sig_w * $ratio);
|
|
$dh = (int) ($sig_h * $ratio);
|
|
$dx = (int) (($canvas_w - $dw) / 2);
|
|
$dy = $header_h + 30;
|
|
imagecopyresampled($canvas, $sig_img, $dx, $dy, 0, 0, $dw, $dh, $sig_w, $sig_h);
|
|
imagedestroy($sig_img);
|
|
|
|
// Trennlinie unter der Unterschrift
|
|
imageline($canvas, 40, $header_h + $sig_area_h - 20, $canvas_w - 40, $header_h + $sig_area_h - 20, $grey);
|
|
imagestring($canvas, 3, (int) ($canvas_w / 2 - 50), $header_h + $sig_area_h - 15,
|
|
'Unterschrift Kunde', $grey);
|
|
|
|
// Footer-Box mit Hash
|
|
$fy = $header_h + $sig_area_h;
|
|
imagefilledrectangle($canvas, 0, $fy, $canvas_w, $canvas_h, imagecolorallocate($canvas, 248, 248, 248));
|
|
imagerectangle($canvas, 0, $fy, $canvas_w - 1, $canvas_h - 1, $line_grey);
|
|
imagestring($canvas, 3, 20, $fy + 15, 'Integritaetspruefung (SHA-256 ueber alle vorherigen Seiten):', $grey);
|
|
imagestring($canvas, 2, 20, $fy + 40, $chain_hash, $black);
|
|
imagestring($canvas, 2, 20, $fy + 70, 'Erfasst: '.$datetime.' · Server: '.($_SERVER['SERVER_NAME'] ?? ''), $grey);
|
|
|
|
// Schreiben
|
|
$filename = 'signature_'.dol_print_date(dol_now(), '%Y%m%d_%H%M%S').'.png';
|
|
$target = $workdir.'/'.$filename;
|
|
if (!imagepng($canvas, $target, 6)) api_fail('PNG schreiben fehlgeschlagen', 500);
|
|
imagedestroy($canvas);
|
|
$relpath = str_replace(DOL_DATA_ROOT.'/', '', $target);
|
|
|
|
// Metadaten als JSON neben der Datei speichern (für spätere Verifikation)
|
|
$meta = array(
|
|
'bericht_id' => $bericht_id,
|
|
'bericht_ref' => $bericht->ref,
|
|
'parent_type' => $bericht->element_type,
|
|
'parent_ref' => $parent_ref,
|
|
'kunde' => $kundename,
|
|
'signer_name' => $signer_name,
|
|
'signed_at' => $datetime,
|
|
'signed_at_unix' => time(),
|
|
'user_id' => $user->id,
|
|
'user_login' => $user->login,
|
|
'gps_lat' => $gps_lat,
|
|
'gps_lon' => $gps_lon,
|
|
'chain_hash_sha256' => $chain_hash,
|
|
'page_count_at_signing' => count($prev_pages),
|
|
'remote_ip' => $_SERVER['REMOTE_ADDR'] ?? '',
|
|
);
|
|
@file_put_contents($target.'.meta.json', json_encode($meta, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
|
|
$resm = $db->query("SELECT COALESCE(MAX(page_order),0) AS m FROM ".$db->prefix()."bericht_page WHERE fk_bericht = ".$bericht_id);
|
|
$next = ($resm && ($o = $db->fetch_object($resm))) ? ((int) $o->m) + 1 : 1;
|
|
|
|
$page = new BerichtPage($db);
|
|
$page->fk_bericht = $bericht_id;
|
|
$page->page_order = $next;
|
|
$page->source_type = 'upload';
|
|
$page->source_path = $relpath;
|
|
$page->note = 'Unterschrift '.($signer_name ?: 'Kunde').' — '.$datetime;
|
|
if ($page->create() <= 0) api_fail('Page-Insert fehlgeschlagen', 500);
|
|
|
|
api_ok(array(
|
|
'page_id' => $page->id,
|
|
'filename' => $filename,
|
|
'chain_hash' => $chain_hash,
|
|
'signed_at' => $datetime,
|
|
));
|
|
}
|
|
|
|
$page_id = (int) ($_GET['id'] ?? 0);
|
|
if (!$page_id) api_fail('id fehlt');
|
|
|
|
// Page laden + prüfen dass sie zum User-scope gehört (einfach: Admin oder Bericht-schreiben)
|
|
$pres = $db->query("SELECT rowid, fk_bericht, source_path, source_type FROM ".$db->prefix()."bericht_page WHERE rowid = ".$page_id);
|
|
if (!$pres || !($prow = $db->fetch_object($pres))) api_fail('Seite nicht gefunden', 404);
|
|
|
|
/* ---------- DELETE page ---------- */
|
|
if ($method === 'DELETE' || ($method === 'POST' && ($_GET['delete'] ?? '') === '1')) {
|
|
if (!$user->hasRight('bericht', 'delete')) api_fail('Permission denied', 403);
|
|
|
|
// Quell-Datei löschen nur wenn sie im bericht/work/ liegt (nicht bei Anhängen von Auftrag/Rechnung)
|
|
if ($prow->source_path && strpos($prow->source_path, 'bericht/work/') === 0) {
|
|
$full = bericht_resolve_data_path($prow->source_path);
|
|
if ($full && file_exists($full)) @unlink($full);
|
|
}
|
|
// Multi-Image-Slot-Einträge mit löschen
|
|
$db->query("DELETE FROM ".$db->prefix()."bericht_page_image WHERE fk_page = ".$page_id);
|
|
$db->query("DELETE FROM ".$db->prefix()."bericht_page WHERE rowid = ".$page_id);
|
|
api_ok();
|
|
}
|
|
|
|
/* ---------- UPDATE page (note, rotation) ---------- */
|
|
if ($method === 'POST') {
|
|
$in = api_input();
|
|
$sets = array();
|
|
if (isset($in['note'])) {
|
|
$note = (string) $in['note'];
|
|
$sets[] = "note = ".($note !== '' ? "'".$db->escape($note)."'" : "NULL");
|
|
}
|
|
if (isset($in['rotation'])) {
|
|
$rot = (int) $in['rotation'];
|
|
$rot = (($rot % 360) + 360) % 360;
|
|
$sets[] = "rotation = ".$rot;
|
|
}
|
|
if (empty($sets)) api_fail('Nichts zu aktualisieren');
|
|
|
|
$sql = "UPDATE ".$db->prefix()."bericht_page SET ".implode(',', $sets)." WHERE rowid = ".$page_id;
|
|
if (!$db->query($sql)) api_fail($db->lasterror(), 500);
|
|
api_ok();
|
|
}
|
|
|
|
api_fail('Methode nicht unterstützt', 405);
|