* * 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/netdiag_api.lib.php * \ingroup netdiag * \brief Gemeinsame Funktionen der JSON-API: Bootstrap, JWT, Antworten. * * Wird von jedem API-Endpunkt eingebunden. Lädt die Dolibarr-Umgebung * ohne Web-Session und authentifiziert die mobile App per JWT. */ // Konstanten setzen BEVOR Dolibarr geladen wird (kein Menü, kein HTML, kein Login) if (!defined('NOLOGIN')) { define('NOLOGIN', '1'); } if (!defined('NOCSRFCHECK')) { define('NOCSRFCHECK', '1'); } if (!defined('NOTOKENRENEWAL')) { define('NOTOKENRENEWAL', '1'); } if (!defined('NOREQUIREMENU')) { define('NOREQUIREMENU', '1'); } if (!defined('NOREQUIREHTML')) { define('NOREQUIREHTML', '1'); } if (!defined('NOREQUIREAJAX')) { define('NOREQUIREAJAX', '1'); } if (!defined('NOREQUIRESOC')) { define('NOREQUIRESOC', '1'); } // =================================================================== // Dolibarr-Umgebung laden — MUSS im globalen Scope passieren. // master.inc.php innerhalb einer Funktion zu includen würde $conf, // $db, $langs, $user ... in den Funktions-Scope legen; nach return // wären sie weg und jeder DB-Zugriff liefe gegen null -> HTTP 500. // Dieser Block läuft, sobald ein Endpunkt die Lib per require_once // einbindet, also im File-Scope des Endpunkts = global. // =================================================================== $res = 0; $tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME']; $tmp2 = realpath(__FILE__); $i = strlen($tmp) - 1; $j = strlen($tmp2) - 1; while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) { $i--; $j--; } if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/master.inc.php")) { $res = @include substr($tmp, 0, ($i + 1))."/master.inc.php"; } if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/master.inc.php")) { $res = @include dirname(substr($tmp, 0, ($i + 1)))."/master.inc.php"; } if (!$res && file_exists("../../../master.inc.php")) { $res = @include "../../../master.inc.php"; } if (!$res && file_exists("../../../../master.inc.php")) { $res = @include "../../../../master.inc.php"; } if (!$res) { header('Content-Type: application/json; charset=utf-8'); http_response_code(500); echo json_encode(array('error' => 'Dolibarr environment not found')); exit; } unset($res, $tmp, $tmp2, $i, $j); /** * Herkünfte, die die API im Browser-Sinne aufrufen dürfen. * * Die App läuft im Capacitor-WebView auf einem eigenen Origin — je nach * `androidScheme` in capacitor.config.ts ist das `https://localhost` (Release) * oder `http://localhost` (lokaler Debug-Build gegen ein Test-Dolibarr). * Dazu der Vite-Dev-Server für die Entwicklung im Browser. * * @return string[] Erlaubte Origins */ function netdiag_api_allowed_origins() { return array( 'https://localhost', 'http://localhost', 'capacitor://localhost', 'ionic://localhost', 'http://localhost:5173', ); } /** * CORS-Header setzen und Preflight (OPTIONS) sofort beantworten. * * Dolibarr selbst ist zu diesem Zeitpunkt bereits geladen (siehe Block * oben, der beim require_once dieser Lib im globalen Scope läuft). * * Kein Wildcard-Origin mehr: die API liefert Kundendaten aus und wird per * Bearer-Token authentifiziert. Ein `*` erlaubt jeder beliebigen Webseite, * die Antwort auszulesen, sobald sie an ein Token kommt. Fehlt der * Origin-Header ganz (native HTTP-Clients, curl, der APK-Downloader im * Kotlin-Plugin), wird kein CORS-Header gesetzt — dort greift die Same-Origin- * Policy des Browsers ohnehin nicht. * * @return void */ function netdiag_api_bootstrap() { $origin = isset($_SERVER['HTTP_ORIGIN']) ? (string) $_SERVER['HTTP_ORIGIN'] : ''; // Antwort hängt vom Origin ab -> Caches/Proxys müssen das wissen header('Vary: Origin'); if ($origin !== '' && in_array($origin, netdiag_api_allowed_origins(), true)) { header('Access-Control-Allow-Origin: '.$origin); header('Access-Control-Allow-Methods: GET, POST, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type, Authorization'); header('Access-Control-Max-Age: 86400'); } // Preflight sofort beantworten if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; } } /** * Zentrales Auth-Modul einbinden, sofern vorhanden und aktiv. * * NetDiag läuft auch ohne awlauth weiter (Fallback auf das modul-eigene JWT) — * so, wie es die awlauth-Doku für Fremdmodule vorschreibt. Damit lässt sich das * Modul auch in einer Instanz betreiben, in der awlauth nicht installiert ist. * * @return bool True wenn awlauth benutzbar ist */ function netdiag_awlauth_available() { static $ok = null; if ($ok !== null) { return $ok; } $ok = false; if (function_exists('dol_include_once')) { dol_include_once('/awlauth/lib/awlauth.lib.php'); } if (function_exists('awlauth_is_enabled') && function_exists('awlauth_verify_token')) { $ok = awlauth_is_enabled(); } return $ok; } /** * JSON-Antwort senden und Skript beenden. * * @param mixed $data Antwortdaten * @param int $httpstatus HTTP-Statuscode * @return void */ function netdiag_api_respond($data, $httpstatus = 200) { header('Content-Type: application/json; charset=utf-8'); http_response_code($httpstatus); echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); exit; } /** * Fehler-Antwort senden und Skript beenden. * * @param string $message Fehlermeldung * @param int $httpstatus HTTP-Statuscode * @return void */ function netdiag_api_error($message, $httpstatus = 400) { netdiag_api_respond(array('error' => $message), $httpstatus); } /** * Token aus Request lesen (Authorization-Header oder ?jwt=). * * @return string Token oder leerer String */ function netdiag_api_read_token() { $auth = ''; if (!empty($_SERVER['HTTP_AUTHORIZATION'])) { $auth = $_SERVER['HTTP_AUTHORIZATION']; } elseif (!empty($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) { $auth = $_SERVER['REDIRECT_HTTP_AUTHORIZATION']; } elseif (function_exists('apache_request_headers')) { $headers = apache_request_headers(); if (!empty($headers['Authorization'])) { $auth = $headers['Authorization']; } } if (stripos($auth, 'Bearer ') === 0) { return trim(substr($auth, 7)); } if (!empty($_GET['jwt'])) { return (string) $_GET['jwt']; } return ''; } /** * Aktuellen Request authentifizieren. Bricht mit 401 ab, wenn ungültig. * * Reihenfolge: zuerst das zentrale awlauth-Token (Signatur + serverseitig * widerrufbare Sitzung), danach als Übergangslösung das alte modul-eigene JWT. * Der Fallback hält bereits ausgestellte App-Tokens bis zu ihrem Ablauf gültig, * damit die Umstellung niemanden mitten im Einsatz aussperrt. Er entfällt, * sobald alle Geräte einmal neu angemeldet sind (siehe ROADMAP_UMSETZUNG L5). * * @param DoliDB $db Datenbank-Handler * @return User Geladenes Benutzer-Objekt */ function netdiag_api_authenticate($db) { $token = netdiag_api_read_token(); if (empty($token)) { netdiag_api_error('Kein Token übermittelt', 401); } /* * Ausschließlich über das zentrale Auth-Modul. Der Übergangspfad mit dem * eigenen netdiag-JWT ist am 17.08.2026 entfallen, nachdem Eddy den * APK-Rollout als abgeschlossen bestätigt hat. * * Warum das ein Gewinn ist und nicht nur Aufräumen: Der alte Pfad prüfte * die Signatur eines selbst ausgestellten Tokens und holte damit einen * Benutzer aus der Datenbank — an der Sitzungsliste von awlauth vorbei. * „Gerät abmelden" dort hatte auf ein solches Token keine Wirkung; ein * verlorenes Handy blieb bis zum Ablauf der TTL angemeldet. Jetzt gibt es * genau eine Stelle, an der Sitzungen enden. * * Ohne aktives awlauth verweigert die API bewusst den Dienst, statt auf * einen zweiten Weg auszuweichen. */ if (!netdiag_awlauth_available()) { netdiag_api_error('Anmeldung nicht möglich: das Modul AWL-Auth ist nicht aktiv', 503); } $user = awlauth_verify_token($token); if ($user === null || empty($user->id)) { netdiag_api_error('Token ungültig oder abgelaufen', 401); } if (isset($user->statut) && $user->statut == 0) { netdiag_api_error('Benutzer deaktiviert', 403); } if (!$user->hasRight('netdiag', 'protocol', 'read')) { netdiag_api_error('Keine Berechtigung für NetDiag', 403); } return $user; } /** * JSON-Body eines POST-Requests einlesen. * * @return array Dekodierte Daten (leer bei Fehler) */ function netdiag_api_read_body() { $raw = file_get_contents('php://input'); if (empty($raw)) { return array(); } $data = json_decode($raw, true); return is_array($data) ? $data : array(); } /** * Zeitstempel der App in einen Unix-Zeitstempel (Sekunden) umrechnen. * * Die App (JavaScript) liefert Zeitstempel in Millisekunden (Date.now()). * Dolibarr/`idate()` erwartet Sekunden — sonst: "Bad value ... for date". * * @param mixed $value Zeitstempel aus dem Request (ms, s oder leer) * @return int Unix-Zeitstempel in Sekunden */ function netdiag_api_timestamp($value) { $v = (int) $value; if ($v <= 0) { return dol_now(); } // 13-stellig (> ~Jahr 5138 in Sekunden) = Millisekunden -> auf Sekunden if ($v > 100000000000) { $v = (int) ($v / 1000); } return $v; } /** * Liste von Diagnose-Protokollen als Array zurückgeben (für API-Antworten). * * @param DoliDB $db Datenbank-Handler * @param string $filtersql Zusätzlicher SQL-Filter, beginnend mit ' AND ...' * @return array> Liste der Protokolle */ function netdiag_api_protocol_list($db, $filtersql = '') { $prefix = $db->prefix(); $sql = "SELECT p.rowid, p.ref, p.label, p.client_uuid, p.fk_soc, p.fk_commande,"; $sql .= " p.date_diag, p.standort, p.subnet, p.status,"; $sql .= " (SELECT COUNT(*) FROM ".$prefix."netdiag_device d WHERE d.fk_protocol = p.rowid) as devcount,"; $sql .= " (SELECT COUNT(*) FROM ".$prefix."netdiag_measurement m WHERE m.fk_protocol = p.rowid) as meascount"; $sql .= " FROM ".$prefix."netdiag_protocol as p"; $sql .= " WHERE p.entity IN (".getEntity('netdiagprotocol').")"; $sql .= $filtersql; $sql .= " ORDER BY p.date_diag DESC, p.rowid DESC"; $list = array(); $resql = $db->query($sql); if ($resql) { while ($obj = $db->fetch_object($resql)) { $list[] = array( 'id' => (int) $obj->rowid, 'ref' => $obj->ref, 'label' => $obj->label, 'clientUuid' => $obj->client_uuid, 'socId' => $obj->fk_soc ? (int) $obj->fk_soc : null, 'orderId' => $obj->fk_commande ? (int) $obj->fk_commande : null, 'dateDiag' => $db->jdate($obj->date_diag), 'location' => $obj->standort, 'subnet' => $obj->subnet, 'status' => (int) $obj->status, 'deviceCount' => (int) $obj->devcount, 'measureCount' => (int) $obj->meascount, ); } } return $list; } /** * Liste (Array aus der App) für die Datenbank zu einer Komma-Zeichenkette * zusammenfassen. * * Ports und mDNS-Dienste sind in der App Arrays; in der Tabelle liegen sie als * Textspalte. Bewusst mit Längenbegrenzung: ein Gerät mit sehr vielen Diensten * darf den INSERT nicht scheitern lassen (MariaDB kürzt im strict mode nicht, * sondern lehnt ab). * * @param mixed $val Array oder Skalar aus dem JSON-Payload * @param int $maxLen Maximale Länge der Zielspalte * @return string Komma-Liste, ggf. gekürzt */ function netdiag_join_list($val, $maxLen) { if ($val === null || $val === '') { return ''; } if (!is_array($val)) { $val = array($val); } $teile = array(); foreach ($val as $v) { if (is_array($v)) { continue; // verschachtelte Strukturen gehören nicht in eine Textspalte } $s = trim((string) $v); if ($s !== '') { $teile[] = str_replace(',', ' ', $s); } } $out = implode(',', $teile); return strlen($out) > $maxLen ? substr($out, 0, $maxLen) : $out; } /** * Komma-Zeichenkette aus der Datenbank wieder als Liste liefern. * * @param string|null $val Spaltenwert * @return array */ function netdiag_split_list($val) { if ($val === null || trim((string) $val) === '') { return array(); } $teile = array_map('trim', explode(',', (string) $val)); return array_values(array_filter($teile, static function ($t) { return $t !== ''; })); }