* * 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 . */ /** * \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, ));