dolibarr.netdiag/lib/netdiag_pdf.lib.php
Eduard Wisch 087f37afa6
All checks were successful
Deploy netdiag / deploy (push) Successful in 14s
1.3.0: PDF-Zweig SIP-Erreichbarkeit, Token in der URL nur noch fuer den APK-Download [deploy]
PDF-Zweig netdiagPdfSip()
- Eigener Zweig, weil die Aussage sonst untergeht: in einer flachen
  Schluessel/Wert-Liste sieht "401 Unauthorized" wie ein Fehler aus, ist aber
  der Beweis fuer eine erreichbare Instanz, die nur Zugangsdaten will. Erst
  Befund im Klartext, dann eine Zeile je Transportweg.
- Der Grenzhinweis steht mit im Dokument: gemessen ist die Erreichbarkeit,
  NICHT die Sprachqualitaet.
- Feldnamen-Falle wieder aufgetreten: 'port' gehoert in beiden Feldtabellen
  laengst dem SNMP-Werkzeug ("Switch-Port"). Im Kunden-PDF haette gestanden
  "Switch-Port: 5060" - deshalb 'zielPort'.

WLAN-Kanalanalyse: Warnungen nicht mehr abschneiden
- dol_trunc(..., 160) kappte genau die Begruendung ("... In dicht besiedelter
  Umgebung meist ein Fehle..."). Eine Warnung ohne ihren Grund ist im
  Kundendokument wertlos; MultiCell bricht ohnehin um.

Token in der Adresszeile
- netdiag_api_read_token() nimmt ?jwt= nur noch an, wenn der Endpunkt es
  ausdruecklich erlaubt. Einzige Stelle: update.php?download=1.
- Grund: ein Token in der URL steht in jedem Zugriffs- und Proxy-Log und gilt
  sieben Tage fuer die GESAMTE Kunden-API.
- Warum die Ausnahme bleibt: die App-Fassungen im Feld bauen die
  Download-Adresse mit dem Token darin. Sofort schliessen hiesse, genau die
  Geraete vom Update auszusperren, die die neue APK brauchen. Zu entfernen,
  sobald Eddy den Rollout bestaetigt (Hinweis steht im Code).
- pdf.php nimmt ab sofort ausschliesslich den Authorization-Header.

Gegen die Testinstanz gemessen: ?jwt= liefert bei orders/customers/protocols/
pdf/update-Version jetzt 401, mit Bearer weiterhin 200.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 21:40:26 +02:00

984 lines
35 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
* \file netdiag/lib/netdiag_pdf.lib.php
* \ingroup netdiag
* \brief PDF-Generator für Diagnose-Protokolle
*/
require_once DOL_DOCUMENT_ROOT.'/core/lib/pdf.lib.php';
require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
require_once __DIR__.'/netdiag.lib.php';
require_once __DIR__.'/../class/netdiagdevice.class.php';
require_once __DIR__.'/../class/netdiagmeasurement.class.php';
/**
* Ein Diagnose-Protokoll als PDF erzeugen und im Dokumentenverzeichnis ablegen.
*
* @param DoliDB $db Datenbank-Handler
* @param NetDiagProtocol $protocol Protokoll-Objekt (geladen)
* @param Translate $outputlangs Sprache der Ausgabe
* @return int >0 wenn OK, <=0 bei Fehler
*/
function netdiagGeneratePdf($db, $protocol, $outputlangs)
{
global $conf, $mysoc, $langs;
$outputlangs->loadLangs(array("netdiag@netdiag", "main", "companies", "orders"));
$dir = netdiagGetOutputDir().'/'.dol_sanitizeFileName($protocol->ref);
if (!dol_is_dir($dir)) {
if (dol_mkdir($dir) < 0) {
$protocol->error = 'Konnte Verzeichnis nicht anlegen: '.$dir;
return -1;
}
}
$file = $dir.'/'.dol_sanitizeFileName($protocol->ref).'.pdf';
// Daten laden
$soc = new Societe($db);
if ($protocol->fk_soc > 0) {
$soc->fetch($protocol->fk_soc);
}
$devObj = new NetDiagDevice($db);
$devices = $devObj->fetchAllByProtocol($protocol->id);
$measObj = new NetDiagMeasurement($db);
$measurements = $measObj->fetchAllByProtocol($protocol->id);
// PDF aufbauen
$pdf = pdf_getInstance('A4', 'mm', 'P');
$default_font_size = pdf_getPDFFontSize($outputlangs);
$pdf->SetAutoPageBreak(true, 20);
$pdf->SetMargins(15, 15, 15);
$pdf->SetTitle($protocol->ref);
$pdf->SetSubject($outputlangs->transnoentities("NetDiagProtocol"));
$pdf->SetCreator("Dolibarr ".DOL_VERSION." - NetDiag");
$pdf->SetAuthor($mysoc->name);
$pdf->AddPage();
// Kopf
$pdf->SetFont('', 'B', 16);
$pdf->SetTextColor(0, 70, 130);
$pdf->Cell(0, 8, $outputlangs->transnoentities("NetDiagProtocol").' '.$protocol->ref, 0, 1, 'L');
$pdf->SetTextColor(0, 0, 0);
$pdf->SetFont('', '', 9);
$pdf->Cell(0, 5, $mysoc->name, 0, 1, 'L');
$pdf->Ln(3);
// Stammdaten
$pdf->SetFont('', '', 10);
$infos = array(
$outputlangs->transnoentities("ThirdParty") => ($soc->id > 0 ? $soc->name : '-'),
$outputlangs->transnoentities("DateDiag") => dol_print_date($protocol->date_diag, 'dayhour', false, $outputlangs),
$outputlangs->transnoentities("Location") => ($protocol->standort ? $protocol->standort : '-'),
$outputlangs->transnoentities("Subnet") => ($protocol->subnet ? $protocol->subnet : '-'),
);
foreach ($infos as $k => $v) {
$pdf->SetFont('', 'B', 10);
$pdf->Cell(40, 6, $k.':', 0, 0, 'L');
$pdf->SetFont('', '', 10);
$pdf->Cell(0, 6, $v, 0, 1, 'L');
}
if (!empty($protocol->note)) {
$pdf->Ln(1);
$pdf->SetFont('', 'I', 9);
$pdf->MultiCell(0, 5, $outputlangs->transnoentities("Note").': '.$protocol->note, 0, 'L');
}
$pdf->Ln(4);
// Geräteliste
$pdf->SetFont('', 'B', 12);
$pdf->Cell(0, 7, $outputlangs->transnoentities("NetDiagDevices").' ('.count($devices).')', 0, 1, 'L');
/*
* Spaltenbreiten (Summe muss 180 mm bleiben, sonst passt die Leerzeile
* unten nicht mehr zum Kopf): Gerätetyp hatte 15 mm, der Text durfte aber
* 10 Zeichen lang sein — „Chromecast/TV" lief dadurch über den rechten
* Tabellenrahmen HINAUS in den Druckrand (nachgemessen an ND2026-0015:
* 199,0 mm bei 195 mm Tabellenkante). Cell() schneidet nicht ab, es
* schreibt einfach weiter. MAC und Hostname sind in der Praxis oft leer
* (ARP ist ab Android 10 gesperrt), von dort kommt der Platz.
*/
$cols = array(30, 34, 44, 28, 24, 20);
$heads = array("IpAddress", "MacAddress", "Hostname", "NetDiagVendor", "DeviceType", "NetDiagOpenPorts");
netdiagPdfTableHead($pdf, $cols, $heads, $outputlangs);
foreach ($devices as $dev) {
netdiagPdfEnsureSpace($pdf, 5, function ($p) use ($cols, $heads, $outputlangs) {
netdiagPdfTableHead($p, $cols, $heads, $outputlangs);
});
// Anzeigename wie in der App: eigener Name vor mDNS-/Host-/NetBIOS-Name.
// Ein Drucker ist per DNS oft namenlos, meldet sich per mDNS aber als
// "Brother HL-L2350DW" — im Kundenprotokoll stand bisher nur die IP.
$anzeige = '';
foreach (array($dev->custom_name, $dev->mdns_name, $dev->hostname, $dev->netbios_name) as $kandidat) {
if (!empty($kandidat)) {
$anzeige = $kandidat;
break;
}
}
$pdf->Cell($cols[0], 5, dol_trunc($dev->ip, 18), 1, 0, 'L');
$pdf->Cell($cols[1], 5, dol_trunc($dev->mac, 20), 1, 0, 'L');
$pdf->Cell($cols[2], 5, dol_trunc($anzeige, 26), 1, 0, 'L');
$pdf->Cell($cols[3], 5, dol_trunc($dev->vendor, 16), 1, 0, 'L');
$pdf->Cell($cols[4], 5, dol_trunc($dev->devicetype, 14), 1, 0, 'L');
$pdf->Cell($cols[5], 5, dol_trunc((string) $dev->open_ports, 12), 1, 1, 'L');
}
if (empty($devices)) {
$pdf->Cell(array_sum($cols), 5, '-', 1, 1, 'C');
}
$pdf->Ln(4);
// Messungen
$pdf->SetFont('', 'B', 12);
$pdf->Cell(0, 7, $outputlangs->transnoentities("NetDiagMeasurements").' ('.count($measurements).')', 0, 1, 'L');
// Status 3 = "nicht messbar": der Test konnte nicht durchgeführt werden.
// Bewusst grau und mit eigenem Zeichen — weder grün (wäre gelogen) noch rot
// (wäre eine Aussage über das Kundennetz, die die Messung nicht hergibt).
$statuslabels = array(
0 => $outputlangs->transnoentities("NetDiagMeasureOk"),
1 => $outputlangs->transnoentities("NetDiagMeasureWarn"),
2 => $outputlangs->transnoentities("NetDiagMeasureFail"),
3 => $outputlangs->transnoentities("NetDiagMeasureUnmeasurable"),
);
// Zusätzlich ein Zeichen, damit die Ampel auch im Schwarz-Weiß-Ausdruck lesbar
// bleibt. Bei Status 0 bewusst leer: das Label heißt dort bereits "OK", sonst
// stünde im PDF doppelt "OK OK".
$statusmarks = array(0 => '', 1 => '! ', 2 => 'X ', 3 => '? ');
foreach ($measurements as $m) {
// Bekannte Falschaussagen aus Altmessungen geraderücken (siehe
// netdiagAltlastKorrektur() — betrifft das entfernte Werkzeug dhcpcheck,
// dessen „0 DHCP-Server (!)" in Rot in ausgelieferten Protokollen steht).
$korr = netdiagAltlastKorrektur($m->tool, $m->measure_status, $m->label, $m->result);
$st = (int) $korr['status'];
if ($st < 0 || $st > 3) {
$st = 1; // unbekannter Wert -> als Warnung behandeln, nie als OK
}
if ($st == 2) {
$pdf->SetFillColor(230, 130, 130);
} elseif ($st == 1) {
$pdf->SetFillColor(245, 215, 130);
} elseif ($st == 3) {
$pdf->SetFillColor(210, 210, 210);
} else {
$pdf->SetFillColor(170, 215, 170);
}
// Titelzeile und Ergebnis zusammenhalten: sonst steht am Seitenende die
// Überschrift samt Ampel allein und darunter ein leerer, unten offener
// Rahmen — das Ergebnis beginnt erst auf der nächsten Seite. Bei einem
// Testlauf über 61 Umbruchlagen trat das in 5 Fällen auf und liest sich
// wie ein abgebrochenes Dokument.
netdiagPdfEnsureSpace($pdf, 11);
$pdf->SetFont('', 'B', 9);
// Klarname statt interner ID — „ipscan" sagt dem Kunden nichts
$title = netdiagToolName($m->tool).($korr['label'] ? ' — '.$korr['label'] : '');
$pdf->Cell(150, 6, dol_trunc($title, 80), 1, 0, 'L');
$pdf->Cell(30, 6, $statusmarks[$st].$statuslabels[$st], 1, 1, 'C', true);
$pdf->SetFont('', '', 8);
// Messparameter: ohne sie steht das Ergebnis ohne Bezug da — man sieht
// nicht, gegen welches Ziel gemessen wurde (Prod-Messung #126).
if ($korr['hinweis'] !== '') {
$pdf->SetFont('', 'I', 7);
$pdf->MultiCell(180, 4, netdiagPdfText($korr['hinweis']), 1, 'L');
$pdf->SetFont('', '', 8);
}
$params = netdiagFormatParams($m->params);
if ($params !== '') {
$pdf->SetFont('', 'I', 7);
$pdf->MultiCell(180, 4, netdiagPdfText($params), 1, 'L');
$pdf->SetFont('', '', 8);
}
/*
* Bei einer als Altlast korrigierten Messung KEINE Zahlen mehr drucken:
* „Gefundene Geräte: 0 | DHCP-Server: keine" widerspricht der Aussage
* „nicht messbar" direkt darüber und liest sich wieder wie ein Befund.
* Der Erklärtext steht bereits hinter der Titelzeile.
*/
if ($korr['hinweis'] !== '') {
continue;
}
if ($m->tool === 'stresstest') {
netdiagPdfStressTest($pdf, $m->result);
} elseif ($m->tool === 'wifikanal') {
netdiagPdfWifiKanal($pdf, $m->result);
} elseif ($m->tool === 'monitor') {
netdiagPdfMonitor($pdf, $m->result);
} elseif ($m->tool === 'internetkette') {
netdiagPdfKette($pdf, $m->result);
} elseif ($m->tool === 'dnscheck') {
netdiagPdfDns($pdf, $m->result);
} elseif ($m->tool === 'geraetediff') {
netdiagPdfGeraeteDiff($pdf, $m->result);
} elseif ($m->tool === 'sipcheck') {
netdiagPdfSip($pdf, $m->result);
} else {
$pdf->MultiCell(180, 5, netdiagPdfFlattenResult($m->result), 1, 'L');
}
}
if (empty($measurements)) {
$pdf->SetFont('', '', 9);
$pdf->Cell(180, 5, '-', 1, 1, 'C');
}
// Fuß
$pdf->Ln(6);
$pdf->SetFont('', 'I', 8);
$pdf->SetTextColor(120, 120, 120);
$pdf->Cell(0, 5, $outputlangs->transnoentities("NetDiagProtocol").' '.$protocol->ref.' — '.dol_print_date(dol_now(), 'dayhour', false, $outputlangs), 0, 1, 'L');
// Speichern
$pdf->Output($file, 'F');
dolChmod($file);
return 1;
}
/**
* Zeichen ersetzen, die der PDF-Standardzeichensatz nicht darstellen kann.
*
* `pdf_getPDFFont()` liefert in dieser Dolibarr-Installation Helvetica — einen
* Core-Font mit WinAnsi-Kodierung, kein Unicode. Alles außerhalb dieses Vorrats
* druckt TCPDF als „?", ohne zu warnen (bekannt aus KB #1025, dort mit dem
* Ankreuzkästchen ☐).
*
* Aufgefallen am Pfeil in der Internet-Kette: aus
* „Namensauflösung: www.google.de → 142.250.185.67" wurde im Kundendokument
* „www.google.de ? 142.250.185.67" — der Pfeil ist dort kein Schmuck, er
* trennt Frage und Antwort.
*
* Bewusst eine Ersetzung statt einer Unicode-Schrift: `dejavusans` würde jedes
* Zeichen können, aber Schriftbild und Dateigröße aller Protokolle ändern.
* Gedankenstrich, Mittelpunkt, Auslassungspunkte und Anführungszeichen sind in
* WinAnsi enthalten und bleiben unangetastet (im Dokument nachgeprüft).
*
* @param string $s Text aus der App oder aus Altdaten
* @return string druckbarer Text
*/
function netdiagPdfText($s)
{
return strtr((string) $s, array(
'→' => '->',
'←' => '<-',
'↔' => '<->',
'⇒' => '=>',
'✓' => 'ja',
'✔' => 'ja',
'✗' => 'nein',
'✘' => 'nein',
'≤' => '<=',
'≥' => '>=',
'≠' => '!=',
'☐' => '[ ]',
'☑' => '[x]',
'⚠' => '!',
'≈' => 'ca. ',
));
}
/**
* Mess-Ergebnis (JSON) als einzeiligen Text für PDF-Ausgabe.
*
* @param string $json JSON-String
* @return string Lesbarer Text
*/
function netdiagPdfFlattenResult($json)
{
if (empty($json)) {
return '-';
}
$data = json_decode($json, true);
if (!is_array($data)) {
return netdiagPdfText(netdiagKundentext((string) $json));
}
$parts = array();
foreach ($data as $key => $val) {
$feld = netdiagFeldFuerKunde((string) $key, $val);
if ($feld !== null) {
$parts[] = $feld;
}
}
return empty($parts) ? '-' : netdiagPdfText(implode(' | ', $parts));
}
/**
* Sorgt dafür, dass die nächsten `$hoeheMm` Millimeter noch auf die aktuelle
* Seite passen — sonst wird vorher umgebrochen und optional eine
* Wiederholungszeile (z.B. der Tabellenkopf) ausgegeben.
*
* Der automatische Seitenumbruch von TCPDF reicht dafür nicht: er greift erst,
* wenn die Zelle schon geschrieben wird, und zerreißt dabei zusammengehörige
* Blöcke (Tabellenkopf/Datenzeilen, Messungstitel/Messergebnis).
*
* @param TCPDF $pdf PDF-Objekt
* @param float $hoeheMm benötigte Resthöhe in mm
* @param callable|null $nachUmbruch wird nach einem Seitenwechsel aufgerufen
* @return bool true, wenn umgebrochen wurde
*/
function netdiagPdfEnsureSpace($pdf, $hoeheMm, $nachUmbruch = null)
{
$rest = ($pdf->getPageHeight() - $pdf->getBreakMargin()) - $pdf->GetY();
if ($rest >= $hoeheMm) {
return false;
}
$pdf->AddPage();
if (is_callable($nachUmbruch)) {
$nachUmbruch($pdf);
}
return true;
}
/**
* Kopfzeile einer Tabelle ausgeben (blau hinterlegt) und danach Schrift/Farbe
* für die Datenzeilen wiederherstellen. Als eigene Funktion, damit dieselbe
* Zeile nach einem Seitenumbruch wiederholt werden kann — vorher standen die
* Spalten ab Seite 2 ohne jede Beschriftung da.
*
* @param TCPDF $pdf PDF-Objekt
* @param array<int,int> $cols Spaltenbreiten in mm
* @param array<int,string> $heads Sprachschlüssel der Überschriften
* @param Translate $outputlangs Sprache
* @return void
*/
function netdiagPdfTableHead($pdf, $cols, $heads, $outputlangs)
{
$pdf->SetFillColor(0, 70, 130);
$pdf->SetTextColor(255, 255, 255);
$pdf->SetFont('', 'B', 8);
foreach ($heads as $idx => $h) {
$pdf->Cell($cols[$idx], 6, $outputlangs->transnoentities($h), 1, 0, 'L', true);
}
$pdf->Ln();
$pdf->SetTextColor(0, 0, 0);
$pdf->SetFont('', '', 8);
}
/**
* Dauer-/Stresstest strukturiert ins PDF schreiben statt als Fließtext.
*
* netdiagPdfFlattenResult() würde Kennzahlen, Ausfallliste und Zeitreihe zu
* einem einzigen langen Absatz zusammenkleben — bei einer Stunde Laufzeit mit
* 60 Minuten-Buckets kaum lesbar. Hier stattdessen eine kompakte
* Kennzahlenzeile plus eine echte Ausfalltabelle. Die Zeitreihe (`verlauf`)
* wird bewusst NICHT gedruckt — sie ist als Diagramm in der App nützlich,
* als reiner Text auf Papier nicht.
*
* @param TCPDF $pdf PDF-Objekt (Cursor steht direkt hinter der Titelzeile)
* @param string $json result-JSON der Messung
* @return void
*/
function netdiagPdfStressTest($pdf, $json)
{
$data = json_decode((string) $json, true);
if (!is_array($data)) {
$pdf->MultiCell(180, 5, (string) $json, 1, 'L');
return;
}
if (!empty($data['hinweis']) || !empty($data['fehler'])) {
$pdf->MultiCell(180, 5, (string) ($data['hinweis'] ?? $data['fehler']), 1, 'L');
return;
}
$sum = array();
if (isset($data['host'])) {
$sum[] = 'Ziel: '.$data['host'];
}
if (isset($data['dauerSekunden'])) {
$sum[] = 'Dauer: '.round($data['dauerSekunden'] / 60).' min';
}
if (isset($data['intervallSek'])) {
$sum[] = 'Takt: '.$data['intervallSek'].' s';
}
if (isset($data['gesendet']) && isset($data['empfangen'])) {
$sum[] = 'Proben: '.$data['empfangen'].'/'.$data['gesendet'];
}
if (isset($data['verlustProzent'])) {
$sum[] = 'Verlust: '.$data['verlustProzent'].' %';
}
if (isset($data['avgMs']) && $data['avgMs'] !== null) {
$sum[] = 'Ø '.$data['avgMs'].' ms';
}
if (isset($data['minMs']) && isset($data['maxMs']) && $data['minMs'] !== null) {
$sum[] = 'Min/Max '.$data['minMs'].'/'.$data['maxMs'].' ms';
}
if (isset($data['p95Ms']) && $data['p95Ms'] !== null) {
$sum[] = 'p95 '.$data['p95Ms'].' ms';
}
if (isset($data['laengsterAusfallSek']) && $data['laengsterAusfallSek'] !== null) {
$sum[] = 'längster Ausfall '.$data['laengsterAusfallSek'].' s';
}
$pdf->MultiCell(180, 5, implode(' | ', $sum), 1, 'L');
// Ausfalltabelle — je Zeile "HH:MM:SS HH:MM:SS (Xs)", vorformatiert von
// der App (dieselben Zeilen, die auch in der Messungen-Liste in der App
// stehen — eine Quelle der Wahrheit statt zwei Formatierungen).
$ausfaelle = (isset($data['ausfaelle']) && is_array($data['ausfaelle'])) ? $data['ausfaelle'] : array();
if (empty($ausfaelle)) {
$pdf->SetFont('', 'I', 8);
$pdf->Cell(180, 5, 'Kein Ausfall während des Laufs.', 1, 1, 'L');
$pdf->SetFont('', '', 8);
return;
}
$pdf->SetFont('', 'B', 8);
$pdf->Cell(180, 5, 'Ausfälle ('.count($ausfaelle).')', 1, 1, 'L');
$pdf->SetFont('', '', 8);
foreach ($ausfaelle as $line) {
$pdf->Cell(180, 5, dol_trunc(netdiagPdfText($line), 120), 1, 1, 'L');
}
}
/**
* Geräte-Vergleich ins PDF schreiben: drei getrennte Listen.
*
* „Was hat sich seit dem letzten Besuch geändert" ist beim Wiederholungstermin
* die erste Frage des Kunden. Als `|`-Kette in einer Zelle wäre die Antwort
* vorhanden, aber nicht lesbar — und gerade hier zählt, WELCHES Gerät neu ist
* und welches fehlt, nicht nur wie viele.
*
* Die Angabe, worüber ein Gerät wiedererkannt wurde, bleibt bewusst an jeder
* Zeile stehen: „erkannt über IP und Portmuster" ist eine schwächere Aussage
* als „erkannt über Netzwerkname", und der Unterschied gehört ins Dokument
* statt in eine Fußnote.
*
* @param TCPDF $pdf PDF-Objekt (Cursor steht direkt hinter der Titelzeile)
* @param string $json result-JSON der Messung
* @return void
*/
function netdiagPdfGeraeteDiff($pdf, $json)
{
$data = json_decode((string) $json, true);
if (!is_array($data)) {
$pdf->MultiCell(180, 5, netdiagPdfFlattenResult($json), 1, 'L');
return;
}
$sum = array();
if (!empty($data['verglichenMit'])) {
$sum[] = 'Verglichen mit '.$data['verglichenMit'];
}
if (isset($data['geraeteVorher'])) {
$sum[] = 'damals '.$data['geraeteVorher'].' Geräte';
}
if (isset($data['geraeteJetzt'])) {
$sum[] = 'jetzt '.$data['geraeteJetzt'];
}
if (isset($data['unveraendert'])) {
$sum[] = $data['unveraendert'].' unverändert';
}
if (!empty($sum)) {
$pdf->MultiCell(180, 5, netdiagPdfText(implode(' | ', $sum)), 1, 'L');
}
$listen = array(
'neu' => 'Neu hinzugekommen',
'nichtMehrErreichbar' => 'Nicht mehr erreichbar',
'mitNeuerAdresse' => 'Mit neuer IP-Adresse',
'andereOffenePorts' => 'Gleiche Adresse, andere offene Ports',
);
$etwasGefunden = false;
foreach ($listen as $feld => $titel) {
$eintraege = (isset($data[$feld]) && is_array($data[$feld])) ? $data[$feld] : array();
if (empty($eintraege)) {
continue;
}
$etwasGefunden = true;
netdiagPdfEnsureSpace($pdf, 10);
$pdf->SetFont('', 'B', 8);
$pdf->Cell(180, 5, $titel.' ('.count($eintraege).')', 1, 1, 'L');
$pdf->SetFont('', '', 8);
foreach ($eintraege as $line) {
netdiagPdfEnsureSpace($pdf, 5, function ($p) use ($titel) {
$p->SetFont('', 'B', 8);
$p->Cell(180, 5, $titel.' — Fortsetzung', 1, 1, 'L');
$p->SetFont('', '', 8);
});
$pdf->MultiCell(180, 5, netdiagPdfText(netdiagKundentext((string) $line)), 1, 'L');
}
}
if (!$etwasGefunden) {
$pdf->SetFont('', 'I', 8);
$pdf->Cell(180, 5, 'Keine Veränderung gegenüber dem letzten Protokoll.', 1, 1, 'L');
$pdf->SetFont('', '', 8);
}
foreach (array('hinweis', 'verfahren') as $feld) {
if (!empty($data[$feld])) {
netdiagPdfEnsureSpace($pdf, 8);
$pdf->SetFont('', 'I', 7);
$pdf->MultiCell(180, 4, netdiagPdfText(netdiagKundentext((string) $data[$feld])), 1, 'L');
$pdf->SetFont('', '', 8);
}
}
}
/**
* DNS-Prüfung ins PDF schreiben: je Server eine eigene Zeile.
*
* Dieselbe Begründung wie beim Monitor und bei der Internet-Kette: die
* generische Darstellung schiebt alle Serverantworten zu einer `|`-Kette
* zusammen. Bei zwei Servern geht das noch, bei den vier bis fünf Einträgen
* eines Firmennetzes ist die eigentliche Aussage — welcher Server antwortet
* und welcher nicht — nicht mehr auffindbar.
*
* @param TCPDF $pdf PDF-Objekt (Cursor steht direkt hinter der Titelzeile)
* @param string $json result-JSON der Messung
* @return void
*/
function netdiagPdfDns($pdf, $json)
{
$data = json_decode((string) $json, true);
if (!is_array($data)) {
$pdf->MultiCell(180, 5, netdiagPdfFlattenResult($json), 1, 'L');
return;
}
$sum = array();
if (!empty($data['ziel'])) {
$sum[] = 'Abgefragter Name: '.$data['ziel'];
}
if (!empty($data['gefragteServer']) && is_array($data['gefragteServer'])) {
$sum[] = 'Befragte Server: '.count($data['gefragteServer']);
}
if (!empty($sum)) {
$pdf->MultiCell(180, 5, netdiagPdfText(implode(' | ', $sum)), 1, 'L');
}
$antworten = (isset($data['antworten']) && is_array($data['antworten'])) ? $data['antworten'] : array();
if (!empty($antworten)) {
netdiagPdfEnsureSpace($pdf, 10);
$pdf->SetFont('', 'B', 8);
$pdf->Cell(180, 5, 'Antworten der Server ('.count($antworten).')', 1, 1, 'L');
$pdf->SetFont('', '', 8);
foreach ($antworten as $line) {
netdiagPdfEnsureSpace($pdf, 5, function ($p) {
$p->SetFont('', 'B', 8);
$p->Cell(180, 5, 'Antworten der Server — Fortsetzung', 1, 1, 'L');
$p->SetFont('', '', 8);
});
// MultiCell, weil eine Fehlermeldung des Servers lang werden kann
$pdf->MultiCell(180, 5, netdiagPdfText(netdiagKundentext((string) $line)), 1, 'L');
}
}
// Messverfahren und Hinweis zum Schluss — die Grenze der Aussage gehört
// ins Kundendokument (gemessen wurde je ein Paket an je einen Server).
foreach (array('hinweis', 'verfahren') as $feld) {
if (!empty($data[$feld])) {
netdiagPdfEnsureSpace($pdf, 8);
$pdf->SetFont('', 'I', 7);
$pdf->MultiCell(180, 4, netdiagPdfText(netdiagKundentext((string) $data[$feld])), 1, 'L');
$pdf->SetFont('', '', 8);
}
}
}
/**
* Internet-Kette ins PDF schreiben: Fehlerglied zuerst, dann die Stufen.
*
* Ohne eigenen Zweig stünden die vier Stufen als `|`-Kette in einer Zelle und
* das Fehlerglied — die eigentliche Aussage — mittendrin. Beim Kunden wird
* genau diese eine Zeile gelesen: „woran liegt es".
*
* @param TCPDF $pdf PDF-Objekt (Cursor steht direkt hinter der Titelzeile)
* @param string $json result-JSON der Messung
* @return void
*/
function netdiagPdfKette($pdf, $json)
{
$data = json_decode((string) $json, true);
if (!is_array($data)) {
$pdf->MultiCell(180, 5, netdiagPdfFlattenResult($json), 1, 'L');
return;
}
// Fehlerglied als eigene, hervorgehobene Zeile
$glied = isset($data['fehlerglied']) ? trim((string) $data['fehlerglied']) : '';
if ($glied !== '') {
$pdf->SetFont('', 'B', 8);
$pdf->Cell(180, 5, $glied === 'keines'
? 'Kein Glied der Kette hat versagt.'
: 'Fehlerglied: '.$glied, 1, 1, 'L');
$pdf->SetFont('', '', 8);
}
$stufen = (isset($data['stufen']) && is_array($data['stufen'])) ? $data['stufen'] : array();
if (!empty($stufen)) {
netdiagPdfEnsureSpace($pdf, 10);
$pdf->SetFont('', 'B', 8);
$pdf->Cell(180, 5, 'Geprüfte Stufen', 1, 1, 'L');
$pdf->SetFont('', '', 8);
foreach ($stufen as $line) {
netdiagPdfEnsureSpace($pdf, 5, function ($p) {
$p->SetFont('', 'B', 8);
$p->Cell(180, 5, 'Geprüfte Stufen — Fortsetzung', 1, 1, 'L');
$p->SetFont('', '', 8);
});
// MultiCell statt Cell: die Stufenzeilen enthalten Fehlertexte des
// Systems („Unable to resolve host …"), die auch mal lang werden.
// Abschneiden würde ausgerechnet die Begründung kappen.
$pdf->MultiCell(180, 5, netdiagPdfText(netdiagKundentext((string) $line)), 1, 'L');
}
}
if (!empty($data['systemUrteil'])) {
netdiagPdfEnsureSpace($pdf, 8);
$pdf->SetFont('', 'I', 7);
$pdf->MultiCell(180, 4, netdiagPdfText(netdiagKundentext((string) $data['systemUrteil'])), 1, 'L');
$pdf->SetFont('', '', 8);
}
if (!empty($data['hinweis'])) {
netdiagPdfEnsureSpace($pdf, 8);
$pdf->SetFont('', 'I', 7);
$pdf->MultiCell(180, 4, netdiagPdfText(netdiagKundentext((string) $data['hinweis'])), 1, 'L');
$pdf->SetFont('', '', 8);
}
}
/**
* Geräte-Dauerüberwachung strukturiert ins PDF schreiben.
*
* Dritter eigener Zweig neben Dauertest und WLAN-Kanal, aus demselben Grund:
* `netdiagPdfFlattenResult()` schiebt alles zu EINER `|`-getrennten Zeile
* zusammen. Bei zehn überwachten Geräten standen damit die zehn Zeilen aus
* `geraete` UND sämtliche Einzelausfälle in einer einzigen Tabellenzelle —
* vollständig, aber beim Kunden nicht lesbar.
*
* ACHTUNG, Unterschied zu netdiagPdfStressTest(): dort führt ein gesetztes
* `hinweis` zum sofortigen Abbruch. Hier darf es das NICHT — der Monitor legt
* IMMER einen Hinweis an (das Messverfahren gehört zur Aussage dazu, siehe
* `buildMonitorMeasurement()` in app/src/lib/monitor.ts). Ein Frühabbruch
* würde also bei jedem Monitor-Lauf sämtliche Zahlen verschlucken. Der Hinweis
* steht deshalb am Ende, nur `fehler` bricht ab.
*
* @param TCPDF $pdf PDF-Objekt (Cursor steht direkt hinter der Titelzeile)
* @param string $json result-JSON der Messung
* @return void
*/
function netdiagPdfMonitor($pdf, $json)
{
$data = json_decode((string) $json, true);
if (!is_array($data)) {
$pdf->MultiCell(180, 5, netdiagPdfFlattenResult($json), 1, 'L');
return;
}
if (!empty($data['fehler']) || !empty($data['error'])) {
$pdf->MultiCell(180, 5, netdiagPdfText(netdiagKundentext((string) ($data['fehler'] ?? $data['error']))), 1, 'L');
return;
}
// Kennzahlenzeile. Bezeichnungen bewusst wie in netdiagKundenfelder(),
// damit dasselbe Feld im PDF und in der Dolibarr-Ansicht gleich heißt.
$sum = array();
if (isset($data['dauerSekunden'])) {
$sum[] = 'Messdauer: '.netdiagDauerLesbar($data['dauerSekunden']);
}
if (isset($data['intervallSek'])) {
$sum[] = 'Messabstand: '.$data['intervallSek'].' s';
}
if (isset($data['geraeteAnzahl'])) {
$sum[] = 'Überwachte Geräte: '.$data['geraeteAnzahl'];
}
if (isset($data['verfuegbarkeitProzent']) && $data['verfuegbarkeitProzent'] !== null) {
$sum[] = 'Verfügbarkeit (schlechtestes Gerät): '.number_format((float) $data['verfuegbarkeitProzent'], 1, ',', '').' %';
}
if (isset($data['aussetzer'])) {
$sum[] = 'Aussetzer: '.$data['aussetzer'];
}
if (!empty($data['ausfallzeitSek'])) {
$sum[] = 'Ausfallzeit (schlechtestes Gerät): '.netdiagDauerLesbar($data['ausfallzeitSek']);
}
if (!empty($data['laengsterAusfallSek'])) {
$sum[] = 'Längster Ausfall: '.netdiagDauerLesbar($data['laengsterAusfallSek']);
}
if (!empty($sum)) {
$pdf->MultiCell(180, 5, implode(' | ', $sum), 1, 'L');
}
// Je Gerät eine eigene Zeile — das ist der eigentliche Beleg. Die Zeilen
// kommen vorformatiert aus der App (dieselben, die dort in der Messungs-
// liste stehen), wie beim Dauertest: eine Quelle der Wahrheit.
$geraete = (isset($data['geraete']) && is_array($data['geraete'])) ? $data['geraete'] : array();
if (!empty($geraete)) {
netdiagPdfEnsureSpace($pdf, 10);
$pdf->SetFont('', 'B', 8);
$pdf->Cell(180, 5, 'Überwachte Geräte ('.count($geraete).')', 1, 1, 'L');
$pdf->SetFont('', '', 8);
foreach ($geraete as $line) {
netdiagPdfEnsureSpace($pdf, 5, function ($p) use ($geraete) {
$p->SetFont('', 'B', 8);
$p->Cell(180, 5, 'Überwachte Geräte ('.count($geraete).') — Fortsetzung', 1, 1, 'L');
$p->SetFont('', '', 8);
});
$pdf->Cell(180, 5, dol_trunc(netdiagPdfText($line), 120), 1, 1, 'L');
}
}
// Einzelne Ausfälle mit Uhrzeit. Genau das ist der Nachweis bei der
// häufigsten Reklamation („die Kamera fällt ständig aus").
$ausfaelle = (isset($data['ausfaelle']) && is_array($data['ausfaelle'])) ? $data['ausfaelle'] : array();
netdiagPdfEnsureSpace($pdf, 10);
if (empty($ausfaelle)) {
$pdf->SetFont('', 'I', 8);
$pdf->Cell(180, 5, 'Kein Ausfall während der Überwachung.', 1, 1, 'L');
$pdf->SetFont('', '', 8);
} else {
$pdf->SetFont('', 'B', 8);
$pdf->Cell(180, 5, 'Einzelne Ausfälle ('.count($ausfaelle).')', 1, 1, 'L');
$pdf->SetFont('', '', 8);
/*
* Obergrenze gegen den pathologischen Fall: ein im Sekundentakt
* flappendes Gerät erzeugt über einen Arbeitstag vierstellig viele
* Segmente — das wären zweistellig viele Seiten reine Ausfallliste.
* 200 Zeilen sind rund vier Seiten und belegen jede reale Störung
* ausreichend. Gekürzt wird NICHT stillschweigend: die Restzahl steht
* sichtbar im Dokument, sonst läse sich das Protokoll als vollständig.
*/
$max = 200;
$i = 0;
foreach ($ausfaelle as $line) {
if ($i >= $max) {
break;
}
netdiagPdfEnsureSpace($pdf, 5, function ($p) use ($ausfaelle) {
$p->SetFont('', 'B', 8);
$p->Cell(180, 5, 'Einzelne Ausfälle ('.count($ausfaelle).') — Fortsetzung', 1, 1, 'L');
$p->SetFont('', '', 8);
});
$pdf->Cell(180, 5, dol_trunc(netdiagPdfText($line), 120), 1, 1, 'L');
$i++;
}
$rest = count($ausfaelle) - $max;
if ($rest > 0) {
netdiagPdfEnsureSpace($pdf, 5);
$pdf->SetFont('', 'I', 8);
$pdf->Cell(180, 5, '… und '.$rest.' weitere Ausfälle, hier aus Platzgründen nicht einzeln aufgeführt.', 1, 1, 'L');
$pdf->SetFont('', '', 8);
}
}
// Hinweis zum Schluss: Messverfahren und ggf. abgerissener Lauf.
if (!empty($data['hinweis'])) {
netdiagPdfEnsureSpace($pdf, 10);
$pdf->SetFont('', 'I', 7);
$pdf->MultiCell(180, 4, netdiagPdfText(netdiagKundentext((string) $data['hinweis'])), 1, 'L');
$pdf->SetFont('', '', 8);
}
}
/**
* WLAN-Kanal-Momentaufnahme im PDF als Tabelle ausgeben.
*
* Wie netdiagPdfStressTest ein eigener Zweig, weil die generische
* Flach-Darstellung die Netzliste (Array von Objekten) unlesbar macht.
*
* @param TCPDF $pdf PDF-Objekt
* @param string $json JSON-String des Ergebnisses
* @return void
*/
function netdiagPdfWifiKanal($pdf, $json)
{
$data = json_decode($json, true);
if (!is_array($data)) {
$pdf->MultiCell(180, 5, netdiagPdfFlattenResult($json), 1, 'L');
return;
}
// Aus dem Demomodus der App — im PDF besonders wichtig zu kennzeichnen,
// weil das Dokument beim Kunden landet.
if (!empty($data['demodaten'])) {
$pdf->SetFont('', 'B', 8);
$pdf->MultiCell(180, 5, 'DEMODATEN — keine echte Messung', 1, 'L');
$pdf->SetFont('', '', 8);
}
// Kopfzeile: Anzahl, eigenes Netz, Empfehlung
$sum = array();
if (isset($data['anzahlNetze'])) {
$sum[] = $data['anzahlNetze'].' Netze sichtbar';
}
if (!empty($data['eigenesNetz']) && is_array($data['eigenesNetz'])) {
$e = $data['eigenesNetz'];
$teil = 'Eigenes Netz: '.($e['ssid'] ?? '?').' Kanal '.($e['kanal'] ?? '?');
if (isset($e['rssi'])) {
$teil .= ', '.$e['rssi'].' dBm';
}
if (!empty($e['bewertung'])) {
$teil .= ' ('.$e['bewertung'].')';
}
$sum[] = $teil;
}
if (!empty($data['empfehlung24GHz']) && is_array($data['empfehlung24GHz'])) {
$best = reset($data['empfehlung24GHz']);
if (is_array($best) && isset($best['kanal'])) {
$sum[] = 'Störungsärmster 2,4-GHz-Kanal: '.$best['kanal'];
}
}
if (!empty($sum)) {
$pdf->MultiCell(180, 5, implode(' | ', $sum), 1, 'L');
}
if (!empty($data['warnungen']) && is_array($data['warnungen'])) {
foreach ($data['warnungen'] as $w) {
/*
* Ungekürzt. Vorher stand hier dol_trunc(..., 160) — und genau die
* Begründung fiel weg: „…blockiert damit 2 der 3 störungsfreien
* Kanäle. In dicht besiedelter Umgebung meist ein Fehle…". Die
* Warnung ohne ihren Grund ist im Kundendokument wertlos, und
* MultiCell bricht ohnehin sauber um.
*/
netdiagPdfEnsureSpace($pdf, 10);
$pdf->MultiCell(180, 5, '! '.netdiagPdfText(netdiagKundentext((string) $w)), 1, 'L');
}
}
$netze = (isset($data['netze']) && is_array($data['netze'])) ? $data['netze'] : array();
if (empty($netze)) {
return;
}
// Tabellenkopf
$pdf->SetFont('', 'B', 7);
$pdf->Cell(60, 5, 'SSID', 1, 0, 'L');
$pdf->Cell(18, 5, 'Kanal', 1, 0, 'C');
$pdf->Cell(22, 5, 'Band', 1, 0, 'L');
$pdf->Cell(20, 5, 'Breite', 1, 0, 'C');
$pdf->Cell(35, 5, 'Standard', 1, 0, 'L');
$pdf->Cell(25, 5, 'Pegel', 1, 1, 'R');
$pdf->SetFont('', '', 7);
foreach ($netze as $n) {
if (!is_array($n)) {
continue;
}
$pdf->Cell(60, 5, dol_trunc((string) ($n['ssid'] ?? ''), 34), 1, 0, 'L');
$pdf->Cell(18, 5, (string) ($n['kanal'] ?? ''), 1, 0, 'C');
$pdf->Cell(22, 5, (string) ($n['band'] ?? ''), 1, 0, 'L');
$pdf->Cell(20, 5, (isset($n['breiteMhz']) && $n['breiteMhz'] !== null) ? $n['breiteMhz'].' MHz' : '-', 1, 0, 'C');
$pdf->Cell(35, 5, dol_trunc((string) ($n['standard'] ?? '-'), 20), 1, 0, 'L');
$pdf->Cell(25, 5, (string) ($n['rssi'] ?? '').' dBm', 1, 1, 'R');
}
}
/**
* SIP-Erreichbarkeit ins PDF schreiben.
*
* Eigener Zweig, weil die entscheidende Aussage sonst untergeht: „401
* Unauthorized" sieht in einer flachen Schlüssel/Wert-Liste wie ein Fehler
* aus, ist aber der Beweis für einen erreichbaren Anschluss. Deshalb steht
* hier der Klartext-Befund zuerst und in eigener Zeile — er ist das, was der
* Kunde und der nächste Techniker lesen.
*
* @param TCPDF $pdf PDF-Objekt (Cursor steht direkt hinter der Titelzeile)
* @param string $json result-JSON der Messung
* @return void
*/
function netdiagPdfSip($pdf, $json)
{
$data = json_decode((string) $json, true);
if (!is_array($data)) {
$pdf->MultiCell(180, 5, netdiagPdfFlattenResult($json), 1, 'L');
return;
}
// Kopfzeile: Ziel, Port, Antwort
$sum = array();
if (!empty($data['ziel'])) {
$teil = 'Ziel: '.$data['ziel'];
if (!empty($data['zielIp']) && $data['zielIp'] !== $data['ziel']) {
$teil .= ' ('.$data['zielIp'].')';
}
$sum[] = $teil;
}
if (!empty($data['zielPort'])) {
$sum[] = 'Port '.$data['zielPort'];
}
if (!empty($data['statuszeile'])) {
$sum[] = 'Antwort: '.$data['statuszeile'];
} else {
$sum[] = 'Antwort: keine';
}
if (!empty($data['antwortWeg'])) {
$sum[] = 'über '.strtoupper((string) $data['antwortWeg']);
}
if (!empty($sum)) {
$pdf->MultiCell(180, 5, netdiagPdfText(implode(' | ', $sum)), 1, 'L');
}
// Der Befund im Klartext — die eigentliche Aussage der Messung
if (!empty($data['befund'])) {
netdiagPdfEnsureSpace($pdf, 10);
$pdf->MultiCell(180, 5, netdiagPdfText(netdiagKundentext((string) $data['befund'])), 1, 'L');
}
// Je Transportweg eine Zeile: erst damit ist belegbar, ob UDP still blieb
// und nur TCP antwortete (Firewall) oder ob gar nichts kam.
$versuche = (isset($data['versuche']) && is_array($data['versuche'])) ? $data['versuche'] : array();
if (!empty($versuche)) {
netdiagPdfEnsureSpace($pdf, 10);
$pdf->SetFont('', 'B', 8);
$pdf->Cell(180, 5, 'Versuche je Transportweg', 1, 1, 'L');
$pdf->SetFont('', '', 8);
foreach ($versuche as $v) {
if (!is_array($v)) {
continue;
}
$zeile = (string) ($v['transport'] ?? '?');
if (!empty($v['antwort'])) {
$zeile .= ': '.$v['antwort'];
} elseif (!empty($v['hinweis'])) {
$zeile .= ': '.$v['hinweis'];
} else {
$zeile .= ': keine Antwort';
}
if (isset($v['laufzeitMs']) && $v['laufzeitMs'] !== null && $v['laufzeitMs'] !== '') {
$zeile .= ' ('.$v['laufzeitMs'].' ms)';
}
netdiagPdfEnsureSpace($pdf, 5, function ($p) {
$p->SetFont('', 'B', 8);
$p->Cell(180, 5, 'Versuche je Transportweg — Fortsetzung', 1, 1, 'L');
$p->SetFont('', '', 8);
});
$pdf->MultiCell(180, 5, netdiagPdfText(netdiagKundentext($zeile)), 1, 'L');
}
}
// Gegenstelle und beherrschte Methoden — Beleg dafür, WAS dort antwortet
$rest = array();
if (!empty($data['gegenstelle'])) {
$rest[] = 'Gegenstelle: '.$data['gegenstelle'];
}
if (!empty($data['unterstuetzteMethoden'])) {
$rest[] = 'Unterstützte Methoden: '.$data['unterstuetzteMethoden'];
}
if (!empty($data['zielHerkunft'])) {
$rest[] = 'Ziel: '.$data['zielHerkunft'];
}
if (!empty($rest)) {
netdiagPdfEnsureSpace($pdf, 8);
$pdf->MultiCell(180, 5, netdiagPdfText(netdiagKundentext(implode(' | ', $rest))), 1, 'L');
}
// Grenze der Aussage zum Schluss — im Kundendokument unverzichtbar:
// gemessen ist die Erreichbarkeit, NICHT die Sprachqualität.
foreach (array('hinweis', 'verfahren') as $feld) {
if (!empty($data[$feld])) {
netdiagPdfEnsureSpace($pdf, 8);
$pdf->SetFont('', 'I', 7);
$pdf->MultiCell(180, 4, netdiagPdfText(netdiagKundentext((string) $data[$feld])), 1, 'L');
$pdf->SetFont('', '', 8);
}
}
}