Vorab: drei Punkte der Roadmap-Liste waren Fehlannahmen. Eine Analyse mit anschließender Gegenprüfung (jeder Befund musste einen Widerlegungsversuch überstehen) hat sie ausgeräumt, bevor Code geändert wurde: - "ab Seite 2 alles nach rechts verschoben" existiert nicht. Nachgemessen am Prod-PDF ND2026-0015 mit pdftotext -bbox: Seite 1 und Seite 2 beginnen beide bei 16,0 mm. Die echten Umbruchfehler waren andere. - measure_status validieren war seit Phase 1 erledigt. - Werkzeug-IDs / Teilnetz-Gruppierung / TCPDF-Fußzeile: verworfen, die vorgeschlagenen Änderungen hätten das PDF verschlechtert. PDF (alle Punkte am mehrseitigen Dokument nachgeprüft): - Tabellenkopf der Geräteliste wird auf Folgeseiten wiederholt. Vorher standen ab Seite 2 unbeschriftete Spalten — bei leeren MAC/Hostname-Feldern vier namenlose Spalten. - Messungs-Titelzeile und Ergebnis werden zusammengehalten. Vorher blieb die Überschrift samt Ampel am Seitenende allein zurück, darunter ein leerer, unten offener Rahmen; in einem Testlauf über 61 Umbruchlagen 5-mal (~8 %). - Spalte "Gerätetyp" hatte 15 mm, ließ aber 10 Zeichen zu — "Chromecast/TV" lief bis 199,0 mm bei 195 mm Tabellenkante über den Rahmen in den Druckrand. - Deutsche Bezeichnungen mit Einheiten statt roher JSON-Schlüssel: aus "VerlustProzent: 0 | MinMs: 4.4 | UptimeSek: 8123456" wird "Paketverlust: 0 % | Kürzeste Antwortzeit: 4.4 ms | Betriebszeit: 94 Tage 1 Std". Als Whitelist (netdiagKundenfelder(), gemeinsam für Karte und PDF) — interne Felder wie arpAvailable, mdnsOk, probed, answered fallen damit automatisch heraus. - "ARP-Tabelle nicht lesbar (/proc/net/arp) — braucht Root" wird beim Drucken zu einem kundentauglichen Satz. Altdaten stehen so in der DB, deshalb Ersetzung beim Drucken statt nur in der App. Gerätemerkmale (der eigentliche Roadmap-Punkt): Der Techniker sah in der App "Drucker HP, Port 9100", im Kundenprotokoll stand nur die IP. Die Felder fehlten dabei nicht in der Übertragung, sondern durchgängig — ein Fix allein in der API wäre folgenlos geblieben, weil Dolibarrs setSaveQuery() nur deklarierte $fields schreibt. Ergänzt über die ganze Kette: sql/llx_netdiag_device.sql + neue Migration llx_netdiag_device_v2.sql (ADD COLUMN IF NOT EXISTS, wiederholbar, läuft bei jedem Modul-Update), NetDiagDevice::$fields + Properties, api/protocols.php POST und GET, Kartenansicht und PDF. Neu: netbios_name, mdns_name, mdns_services, custom_name, open_ports, found_via, last_seen. Im PDF steht jetzt statt "192.168.178.20" die Zeile "Brother HL-L2350DW · Brother · Drucker · 80,443,9100" — der Name kommt aus mDNS, obwohl der Hostname leer ist. Sprachschlüssel Vendor -> NetDiagVendor: Die Gegenprüfung hielt den Punkt für falsch (Translate::load() ist first-wins, im CLI-Test kam "Hersteller"), im Browser stand in der Kartenansicht aber "Lieferant" — im HTTP-Kontext lädt Dolibarr vorher andere Sprachdateien als im CLI. Statt der Ursache nachzugehen jetzt ein eigener, kollisionsfreier Schlüssel; im Browser gegengeprüft. Nebenbei: doppeltes "OK OK" beim Status 0 im PDF. Gegen das Test-Dolibarr geprüft: Sync über die echte API (Login, POST, GET), Felder in der DB kontrolliert, Kartenansicht im Browser, mehrseitiges PDF gerendert und angesehen, Migration zweimal ausgeführt (idempotent).
492 lines
15 KiB
PHP
492 lines
15 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 — 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);
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* 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);
|
|
}
|
|
|
|
$user = null;
|
|
|
|
// 1. Zentrales Auth-Modul
|
|
if (netdiag_awlauth_available()) {
|
|
$user = awlauth_verify_token($token);
|
|
}
|
|
|
|
// 2. Übergang: altes netdiag-JWT
|
|
if ($user === null) {
|
|
$payload = netdiag_jwt_decode($token);
|
|
if ($payload !== null && !empty($payload['sub'])) {
|
|
require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php';
|
|
$legacy = new User($db);
|
|
if ($legacy->fetch((int) $payload['sub']) > 0 && !empty($legacy->id)) {
|
|
$legacy->loadRights();
|
|
$user = $legacy;
|
|
}
|
|
}
|
|
}
|
|
|
|
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<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();
|
|
}
|
|
|
|
/**
|
|
* 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<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;
|
|
}
|
|
|
|
/**
|
|
* 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<int,string>
|
|
*/
|
|
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 !== '';
|
|
}));
|
|
}
|