* * 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); /** * 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). * * @return void */ function netdiag_api_bootstrap() { // CORS: Bearer-Token-Auth, daher Wildcard-Origin erlaubt (keine Cookies) header('Access-Control-Allow-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; } } /** * 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); } /** * Base64-URL-kodieren (JWT-konform, ohne Padding). * * @param string $data Rohdaten * @return string Kodierter String */ function netdiag_base64url_encode($data) { return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); } /** * Base64-URL-dekodieren. * * @param string $data Kodierter String * @return string Rohdaten */ function netdiag_base64url_decode($data) { return base64_decode(strtr($data, '-_', '+/')); } /** * Geheimen JWT-Schlüssel des Moduls holen. * * @return string Schlüssel */ function netdiag_jwt_secret() { $secret = getDolGlobalString('NETDIAG_API_JWT_SECRET'); if (empty($secret)) { // Fallback: Instanz-eindeutiger Wert (sollte nach Modulaktivierung nicht eintreten) $secret = md5(DOL_DOCUMENT_ROOT.getDolGlobalString('MAIN_INFO_SOCIETE_NOM')); } return $secret; } /** * JWT (HS256) erzeugen. * * @param array $payload Nutzdaten (sub, name, exp werden ergänzt) * @param int $ttl Gültigkeit in Sekunden * @return string Signiertes Token */ function netdiag_jwt_encode($payload, $ttl) { $header = array('alg' => 'HS256', 'typ' => 'JWT'); $now = dol_now(); $payload['iat'] = $now; $payload['exp'] = $now + $ttl; $seg = array(); $seg[] = netdiag_base64url_encode(json_encode($header)); $seg[] = netdiag_base64url_encode(json_encode($payload)); $signinginput = implode('.', $seg); $signature = hash_hmac('sha256', $signinginput, netdiag_jwt_secret(), true); $seg[] = netdiag_base64url_encode($signature); return implode('.', $seg); } /** * JWT prüfen und Nutzdaten zurückgeben. * * @param string $token JWT * @return array|null Nutzdaten oder null bei ungültig/abgelaufen */ function netdiag_jwt_decode($token) { $parts = explode('.', (string) $token); if (count($parts) !== 3) { return null; } list($h, $p, $s) = $parts; $expected = hash_hmac('sha256', $h.'.'.$p, netdiag_jwt_secret(), true); $given = netdiag_base64url_decode($s); if (!hash_equals($expected, $given)) { return null; } $payload = json_decode(netdiag_base64url_decode($p), true); if (!is_array($payload)) { return null; } if (empty($payload['exp']) || $payload['exp'] < dol_now()) { return null; } return $payload; } /** * 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. * * @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); } $payload = netdiag_jwt_decode($token); if ($payload === null || empty($payload['sub'])) { netdiag_api_error('Token ungültig oder abgelaufen', 401); } require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php'; $user = new User($db); if ($user->fetch((int) $payload['sub']) <= 0 || empty($user->id)) { netdiag_api_error('Benutzer nicht gefunden', 401); } if (!empty($user->statut) && $user->statut == 0) { netdiag_api_error('Benutzer deaktiviert', 403); } $user->loadRights(); 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; }