dolibarr.netdiag/api/netdiag_api.lib.php
Eduard Wisch c576726a26
Some checks are pending
Deploy netdiag / deploy (push) Waiting to run
Initiales Commit — Dolibarr-Modul NetDiag [deploy]
Netzwerk-Diagnose-Modul mit JSON-API für die NetDiag-App:
- 3 Tabellen (protocol/device/measurement), generisches JSON-result
- JSON-API: auth, customers, orders, protocols (idempotenter Sync), pdf
- JWT-Auth (HS256), CORS für die Capacitor-App
- Tabs an Thirdparty + Auftrag, Protokoll-Card, PDF-Generator
- QR-Code zum App-Download in der Modul-Konfiguration
- de_DE + en_US, Rechtesystem netdiag->protocol read/write/delete

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 12:12:11 +02:00

330 lines
9.5 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/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 (master.inc.php) und CORS-Header setzen.
*
* @return void
*/
function netdiag_api_bootstrap()
{
// master.inc.php aus dem Webroot finden
$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;
}
// 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<string,mixed> $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<string,mixed>|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<string,mixed> 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();
}
/**
* 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<int,array<string,mixed>> 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;
}