Alle vier durch eine systematische Gegenpruefung des echten Codes gefunden,
nicht durch Symptome — sie waren bereits ausgeliefert und still.
1. Whitelist-Schluessel 'typ' existiert nicht. Die App liefert das
LinkInfo-Objekt durch, dort heisst das Feld 'type' (app/src/lib/types.ts).
Folge: im Kundendokument fehlte ausgerechnet beim IP-Test an der Dose die
Angabe, ob per LAN oder WLAN gemessen wurde - bei einem Abnahmebeleg die
halbe Aussage. Zusaetzlich 'rssi' aufgenommen: bei einer WLAN-Dose IST der
Empfangspegel der Messwert, er wurde bisher verworfen.
Die Rohwerte sind englisch; netdiagKundentext() bildet sie jetzt ab
('ethernet' -> 'LAN (Kabel)'), sonst stuende "Anschlussart: ethernet" im
Abnahmeprotokoll eines deutschen Handwerksbetriebs.
2. Leeres Array druckte "Offene Ports: " ohne Wert. Die Leerpruefung ist ein
elseif hinter dem Array-Zweig und wurde nie erreicht. Betraf ausgerechnet
die GUTEN Ergebnisse: Portscan ohne offenen Port, IP-Konflikt ohne
Konflikt, IP-Scan ohne Veraenderung. Jetzt "keine" - Weglassen waere
schlechter, weil der Kunde sonst nicht unterscheiden kann, ob nichts
gefunden oder nichts geprueft wurde.
3. 'error' fehlte in der Whitelist. Der IP-Konflikt gibt im Abbruchfall
{ error: ... } mit roter Ampel zurueck - im Kundendokument stand eine rote
Ampel ohne einen Buchstaben Erklaerung. Die App vereinheitlicht kuenftig
auf 'fehler', aber Altdaten lassen sich nicht aendern.
4. tool/category/label wurden beim Sync nicht auf die Spaltenlaenge gekuerzt
(varchar 64/32/255). Bei striktem SQL-Modus kippt EIN zu langes Label den
gesamten Sync per rollback() - der Techniker steht beim Kunden mit einem
Protokoll da, das sich nicht abschliessen laesst, weil eine Beschriftung zu
lang war. Die App setzt Labels durch Verketten zusammen, 255 Zeichen sind
erreichbar. found_via wurde 20 Zeilen darueber laengst gekappt.
Ausserdem:
- GET-Zweig der API prueft jetzt eine Berechtigung. Bisher konnte jeder
angemeldete Benutzer mit einer geratenen ID ein fremdes Protokoll samt
Geraeteliste, IP-Adressen und offenen Ports abrufen. 'write' wird bewusst
mit akzeptiert: die Rechte sind in Dolibarr einzeln vergebbar, und ein
Techniker mit Schreib- ohne ausdruecklichem Leserecht duerfte nicht
ausgesperrt werden - das waere erst beim Kunden aufgefallen.
- netdiagDauerLesbar: "1 Tage 0 Std" -> "1 Tag". floor() liefert einen Float,
1.0 === 1 ist in PHP false.
Geprueft: php -l, und netdiagFeldFuerKunde() mit 13 echten Faellen gegen die
lokale Instanz durchgerechnet (LAN/WLAN/Mobilfunk, leere und gefuellte Arrays,
error/fehler, tote Schluessel, Interna).
275 lines
12 KiB
PHP
275 lines
12 KiB
PHP
<?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/api/protocols.php
|
|
* \ingroup netdiag
|
|
* \brief API-Endpunkt: Protokolle lesen (GET ?id=) und synchronisieren
|
|
* (POST action=sync). Der Sync ist idempotent über client_uuid.
|
|
*/
|
|
|
|
require_once __DIR__.'/netdiag_api.lib.php';
|
|
|
|
netdiag_api_bootstrap();
|
|
|
|
/**
|
|
* @var Conf $conf
|
|
* @var DoliDB $db
|
|
* @var Translate $langs
|
|
*/
|
|
|
|
$user = netdiag_api_authenticate($db);
|
|
|
|
require_once __DIR__.'/../class/netdiagprotocol.class.php';
|
|
require_once __DIR__.'/../class/netdiagdevice.class.php';
|
|
require_once __DIR__.'/../class/netdiagmeasurement.class.php';
|
|
|
|
// =========================================================================
|
|
// GET: einzelnes Protokoll mit Geräten und Messungen
|
|
// =========================================================================
|
|
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
|
// Leserecht prüfen. Der POST-Zweig weiter unten verlangt seit jeher
|
|
// 'protocol write', der GET-Zweig prüfte bis 16.08.2026 gar nichts: jeder
|
|
// angemeldete Benutzer konnte mit einer geratenen ID ein fremdes Protokoll
|
|
// samt Geräteliste, IP-Adressen und offenen Ports abrufen. Das ist beim
|
|
// Kunden erhobene Netzstruktur — sie gehört hinter ein Recht.
|
|
//
|
|
// 'write' wird bewusst mit akzeptiert: In Dolibarr sind die Rechte einzeln
|
|
// vergebbar, ein Techniker-Benutzer mit Schreib- aber ohne ausdrückliches
|
|
// Leserecht ist möglich. Der würde sonst genau die Protokolle nicht mehr
|
|
// abrufen können, die er selbst hochgeladen hat — die App wäre nach dem
|
|
// Update kaputt, und zwar erst beim Kunden. Wer schreiben darf, darf lesen.
|
|
if (!$user->hasRight('netdiag', 'protocol', 'read')
|
|
&& !$user->hasRight('netdiag', 'protocol', 'write')) {
|
|
netdiag_api_error('Keine Leseberechtigung', 403);
|
|
}
|
|
$id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
|
|
if ($id <= 0) {
|
|
netdiag_api_error('Parameter id fehlt', 400);
|
|
}
|
|
$protocol = new NetDiagProtocol($db);
|
|
if ($protocol->fetch($id) <= 0) {
|
|
netdiag_api_error('Protokoll nicht gefunden', 404);
|
|
}
|
|
|
|
$devObj = new NetDiagDevice($db);
|
|
$devices = array();
|
|
foreach ($devObj->fetchAllByProtocol($protocol->id) as $d) {
|
|
$devices[] = array(
|
|
'id' => (int) $d->id,
|
|
'ip' => $d->ip,
|
|
'mac' => $d->mac,
|
|
'hostname' => $d->hostname,
|
|
'netbiosName' => $d->netbios_name,
|
|
'mdnsName' => $d->mdns_name,
|
|
'mdnsServices' => netdiag_split_list($d->mdns_services),
|
|
'customName' => $d->custom_name,
|
|
'vendor' => $d->vendor,
|
|
'deviceType' => $d->devicetype,
|
|
'openPorts' => array_map('intval', netdiag_split_list($d->open_ports)),
|
|
'foundVia' => $d->found_via,
|
|
'lastSeen' => !empty($d->last_seen) ? (int) $db->jdate($d->last_seen) * 1000 : null,
|
|
'note' => $d->note,
|
|
);
|
|
}
|
|
$measObj = new NetDiagMeasurement($db);
|
|
$measurements = array();
|
|
foreach ($measObj->fetchAllByProtocol($protocol->id) as $m) {
|
|
$measurements[] = array(
|
|
'id' => (int) $m->id,
|
|
'deviceId' => $m->fk_device ? (int) $m->fk_device : null,
|
|
'tool' => $m->tool,
|
|
'category' => $m->category,
|
|
'label' => $m->label,
|
|
'params' => $m->params ? json_decode($m->params, true) : null,
|
|
'result' => $m->result ? json_decode($m->result, true) : null,
|
|
'measureStatus' => (int) $m->measure_status,
|
|
'dateMeasure' => $db->jdate($m->date_measure),
|
|
);
|
|
}
|
|
|
|
netdiag_api_respond(array(
|
|
'protocol' => array(
|
|
'id' => (int) $protocol->id,
|
|
'ref' => $protocol->ref,
|
|
'label' => $protocol->label,
|
|
'clientUuid' => $protocol->client_uuid,
|
|
'socId' => $protocol->fk_soc ? (int) $protocol->fk_soc : null,
|
|
'orderId' => $protocol->fk_commande ? (int) $protocol->fk_commande : null,
|
|
'dateDiag' => $db->jdate($protocol->date_diag),
|
|
'location' => $protocol->standort,
|
|
'subnet' => $protocol->subnet,
|
|
'status' => (int) $protocol->status,
|
|
'note' => $protocol->note,
|
|
),
|
|
'devices' => $devices,
|
|
'measurements' => $measurements,
|
|
));
|
|
}
|
|
|
|
// =========================================================================
|
|
// POST: Protokoll synchronisieren (anlegen oder aktualisieren)
|
|
// =========================================================================
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
netdiag_api_error('Methode nicht erlaubt', 405);
|
|
}
|
|
if (!$user->hasRight('netdiag', 'protocol', 'write')) {
|
|
netdiag_api_error('Keine Schreibberechtigung', 403);
|
|
}
|
|
|
|
$body = netdiag_api_read_body();
|
|
if (($body['action'] ?? '') !== 'sync' || empty($body['protocol']) || !is_array($body['protocol'])) {
|
|
netdiag_api_error('Erwartet: { action: "sync", protocol: {...} }', 400);
|
|
}
|
|
$p = $body['protocol'];
|
|
|
|
$uuid = isset($p['clientUuid']) ? trim((string) $p['clientUuid']) : '';
|
|
if ($uuid === '') {
|
|
netdiag_api_error('protocol.clientUuid erforderlich', 400);
|
|
}
|
|
|
|
$db->begin();
|
|
|
|
// Vorhandenes Protokoll über UUID suchen (idempotent)
|
|
$protocol = new NetDiagProtocol($db);
|
|
$exists = $protocol->fetchByClientUuid($uuid);
|
|
if ($exists < 0) {
|
|
$db->rollback();
|
|
netdiag_api_error('Datenbankfehler beim Laden', 500);
|
|
}
|
|
|
|
$protocol->client_uuid = $uuid;
|
|
$protocol->label = isset($p['label']) ? (string) $p['label'] : '';
|
|
$protocol->fk_soc = !empty($p['socId']) ? (int) $p['socId'] : null;
|
|
$protocol->fk_commande = !empty($p['orderId']) ? (int) $p['orderId'] : null;
|
|
$protocol->date_diag = netdiag_api_timestamp($p['dateDiag'] ?? 0);
|
|
$protocol->fk_user_techniker = (int) $user->id;
|
|
$protocol->standort = isset($p['location']) ? (string) $p['location'] : '';
|
|
// Standort leer? Dann aus der Kundenadresse vorbelegen. Der Techniker tippt
|
|
// vor Ort selten etwas ein, im Protokoll stand der Ort deshalb fast immer
|
|
// leer — obwohl er in Dolibarr am Kunden hinterlegt ist. Eine vom Techniker
|
|
// eingetragene Angabe wird NIE überschrieben.
|
|
if (trim($protocol->standort) === '' && !empty($protocol->fk_soc)) {
|
|
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
|
|
$socTmp = new Societe($db);
|
|
if ($socTmp->fetch((int) $protocol->fk_soc) > 0) {
|
|
$teile = array_filter(array(trim($socTmp->address), trim($socTmp->zip.' '.$socTmp->town)));
|
|
$protocol->standort = substr(implode(', ', $teile), 0, 255);
|
|
}
|
|
}
|
|
$protocol->subnet = isset($p['subnet']) ? (string) $p['subnet'] : '';
|
|
$protocol->status = isset($p['status']) ? (int) $p['status'] : NetDiagProtocol::STATUS_DRAFT;
|
|
$protocol->note = isset($p['note']) ? (string) $p['note'] : '';
|
|
// tms explizit setzen — die Spalte ist NOT NULL ohne brauchbaren NULL-Default
|
|
// (explicit_defaults_for_timestamp=1), createCommon würde sonst NULL einfügen.
|
|
$protocol->tms = dol_now();
|
|
|
|
if ($exists > 0) {
|
|
$result = $protocol->update($user, 1);
|
|
} else {
|
|
$result = $protocol->create($user, 1);
|
|
}
|
|
if ($result <= 0) {
|
|
$db->rollback();
|
|
netdiag_api_error('Protokoll speichern fehlgeschlagen: '.$protocol->errorsToString(), 500);
|
|
}
|
|
$protocolId = (int) $protocol->id;
|
|
|
|
// Alte Geräte und Messungen entfernen (Sync ersetzt komplett)
|
|
$db->query("DELETE FROM ".$db->prefix()."netdiag_measurement WHERE fk_protocol = ".$protocolId);
|
|
$db->query("DELETE FROM ".$db->prefix()."netdiag_device WHERE fk_protocol = ".$protocolId);
|
|
|
|
// Geräte einfügen, dabei clientId -> serverRowid merken
|
|
$deviceIdMap = array();
|
|
$devicesIn = (!empty($p['devices']) && is_array($p['devices'])) ? $p['devices'] : array();
|
|
foreach ($devicesIn as $d) {
|
|
$dev = new NetDiagDevice($db);
|
|
$dev->fk_protocol = $protocolId;
|
|
$dev->ip = isset($d['ip']) ? (string) $d['ip'] : '';
|
|
$dev->mac = isset($d['mac']) ? (string) $d['mac'] : '';
|
|
$dev->hostname = isset($d['hostname']) ? (string) $d['hostname'] : '';
|
|
$dev->vendor = isset($d['vendor']) ? (string) $d['vendor'] : '';
|
|
$dev->devicetype = isset($d['deviceType']) ? (string) $d['deviceType'] : '';
|
|
$dev->note = isset($d['note']) ? (string) $d['note'] : '';
|
|
// Gerätemerkmale aus dem IP-Scan. Ohne sie steht im Kundenprotokoll nur
|
|
// die IP, während der Techniker in der App „Drucker HP, Port 9100" sieht.
|
|
$dev->netbios_name = isset($d['netbiosName']) ? (string) $d['netbiosName'] : '';
|
|
$dev->mdns_name = isset($d['mdnsName']) ? (string) $d['mdnsName'] : '';
|
|
$dev->mdns_services = netdiag_join_list($d['mdnsServices'] ?? null, 512);
|
|
$dev->custom_name = isset($d['customName']) ? (string) $d['customName'] : '';
|
|
$dev->open_ports = netdiag_join_list($d['openPorts'] ?? null, 255);
|
|
$dev->found_via = isset($d['foundVia']) ? substr((string) $d['foundVia'], 0, 16) : '';
|
|
// lastSeen kommt als Unix-Zeit in Millisekunden aus der App
|
|
$dev->last_seen = !empty($d['lastSeen']) ? $db->idate((int) round(((float) $d['lastSeen']) / 1000)) : null;
|
|
$dev->tms = dol_now();
|
|
if ($dev->create($user, 1) <= 0) {
|
|
$db->rollback();
|
|
netdiag_api_error('Gerät speichern fehlgeschlagen: '.$dev->errorsToString(), 500);
|
|
}
|
|
if (isset($d['clientId'])) {
|
|
$deviceIdMap[(string) $d['clientId']] = (int) $dev->id;
|
|
}
|
|
}
|
|
|
|
// Messungen einfügen, deviceClientId auf serverRowid abbilden
|
|
$measIn = (!empty($p['measurements']) && is_array($p['measurements'])) ? $p['measurements'] : array();
|
|
foreach ($measIn as $m) {
|
|
$meas = new NetDiagMeasurement($db);
|
|
$meas->fk_protocol = $protocolId;
|
|
$dcid = isset($m['deviceClientId']) ? (string) $m['deviceClientId'] : '';
|
|
$meas->fk_device = ($dcid !== '' && isset($deviceIdMap[$dcid])) ? $deviceIdMap[$dcid] : null;
|
|
// Auf die Spaltenlängen kürzen (varchar 64/32/255, siehe
|
|
// sql/llx_netdiag_measurement.sql). Ohne das kippt bei striktem SQL-Modus
|
|
// EIN zu langes Label den gesamten Sync: create() schlägt fehl, es folgt
|
|
// rollback() — und der Techniker steht beim Kunden mit einem Protokoll da,
|
|
// das sich nicht abschließen lässt, obwohl nur eine Beschriftung zu lang
|
|
// war. Die App setzt Labels durch Verketten zusammen (Raumname + SSID +
|
|
// Geschwindigkeit …), 255 Zeichen sind dabei erreichbar. Bei den Geräten
|
|
// wird längst so verfahren (found_via, netdiag_join_list weiter oben).
|
|
$meas->tool = substr(isset($m['tool']) ? (string) $m['tool'] : 'unknown', 0, 64);
|
|
$meas->category = substr(isset($m['category']) ? (string) $m['category'] : '', 0, 32);
|
|
$meas->label = substr(isset($m['label']) ? (string) $m['label'] : '', 0, 255);
|
|
$meas->params = isset($m['params']) ? json_encode($m['params'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : null;
|
|
$meas->result = isset($m['result']) ? json_encode($m['result'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : null;
|
|
// Nur bekannte Bewertungen uebernehmen. Ein unbekannter Wert landete sonst
|
|
// als Array-Index in Karte und PDF und lief dort ins Leere.
|
|
$st = isset($m['measureStatus']) ? (int) $m['measureStatus'] : 0;
|
|
$meas->measure_status = ($st >= 0 && $st <= 3) ? $st : 1;
|
|
$meas->date_measure = netdiag_api_timestamp($m['dateMeasure'] ?? 0);
|
|
$meas->tms = dol_now();
|
|
if ($meas->create($user, 1) <= 0) {
|
|
$db->rollback();
|
|
netdiag_api_error('Messung speichern fehlgeschlagen: '.$meas->errorsToString(), 500);
|
|
}
|
|
}
|
|
|
|
$db->commit();
|
|
|
|
// PDF neu erzeugen, wenn das Protokoll abgeschlossen ist
|
|
$pdfgenerated = false;
|
|
if ($protocol->status == NetDiagProtocol::STATUS_DONE) {
|
|
require_once __DIR__.'/../lib/netdiag_pdf.lib.php';
|
|
$pdfgenerated = (netdiagGeneratePdf($db, $protocol, $langs) > 0);
|
|
}
|
|
|
|
netdiag_api_respond(array(
|
|
'ok' => true,
|
|
'protocolId' => $protocolId,
|
|
'ref' => $protocol->ref,
|
|
'created' => ($exists == 0),
|
|
'pdfGenerated' => $pdfgenerated,
|
|
));
|