dolibarr.idsconnect/callback.php
data 5f5a389809 IDS Connect v2.2 - Menü-Integration, ADL-Hooks, Admin-Erweiterung
- Menü unter Einkauf > Lieferantenbestellungen statt eigenes Top-Menü
- ADL-Buttons auf Produkt-Lieferantenpreisen per Hook (pricesuppliercard)
- Admin-Seite: Großhändler-Schnellübersicht mit Version-Check
- Dashboard: Shop-öffnen-Button (LI-Action)
- Neue Datei: class/actions_idsconnect.class.php

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 17:45:15 +01:00

474 lines
18 KiB
PHP
Executable file

<?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.
*/
/**
* \file idsconnect/callback.php
* \ingroup idsconnect
* \brief HOOKURL-Callback-Handler - empfängt Warenkörbe vom Großhandels-Shop
*/
// Version zur Überprüfung ob der richtige Code deployed ist
define('IDSCONNECT_CALLBACK_VERSION', '2.0');
// Dolibarr laden (ohne Login-Erfordernis für Callback)
define('NOLOGIN', 1);
define('NOCSRFCHECK', 1);
define('NOREQUIREMENU', 1);
define('NOREQUIREHTML', 1);
define('NOREQUIREAJAX', 1);
$res = 0;
if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) {
$res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php";
}
$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))."/main.inc.php")) {
$res = @include substr($tmp, 0, ($i + 1))."/main.inc.php";
}
if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) {
$res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php";
}
if (!$res && file_exists("../main.inc.php")) {
$res = @include "../main.inc.php";
}
if (!$res && file_exists("../../main.inc.php")) {
$res = @include "../../main.inc.php";
}
if (!$res) {
http_response_code(500);
die("Server configuration error");
}
require_once DOL_DOCUMENT_ROOT.'/core/lib/security.lib.php';
dol_include_once('/idsconnect/class/idsconnect.class.php');
dol_include_once('/idsconnect/class/idslog.class.php');
dol_include_once('/idsconnect/class/idssupplier.class.php');
/**
* @var DoliDB $db
*/
// Callback-Eingang loggen (alles erfassen für Debugging)
$callback_meta = array(
'callback_version' => IDSCONNECT_CALLBACK_VERSION,
'method' => $_SERVER['REQUEST_METHOD'],
'remote_ip' => getUserRemoteIP(),
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unbekannt',
'content_type' => $_SERVER['CONTENT_TYPE'] ?? 'unbekannt',
'content_length' => $_SERVER['CONTENT_LENGTH'] ?? 0,
'query_string' => $_SERVER['QUERY_STRING'] ?? '',
'post_keys' => array_keys($_POST),
'files_keys' => array_keys($_FILES),
'timestamp' => date('Y-m-d H:i:s'),
);
dol_syslog("IDS Connect Callback v".IDSCONNECT_CALLBACK_VERSION.": Eingang von ".$callback_meta['remote_ip']." Method=".$callback_meta['method']." POST-Keys=".implode(',', $callback_meta['post_keys'])." FILES-Keys=".implode(',', $callback_meta['files_keys']), LOG_INFO);
// Nur POST und GET erlauben
if (!in_array($_SERVER['REQUEST_METHOD'], array('POST', 'GET'))) {
dol_syslog("IDS Connect Callback: Method Not Allowed: ".$_SERVER['REQUEST_METHOD'], LOG_WARNING);
http_response_code(405);
die("Method not allowed");
}
// Token aus GET oder POST
$token = GETPOST('token', 'alphanohtml');
if (empty($token)) {
dol_syslog("IDS Connect Callback: Kein Token - POST-Daten: ".json_encode(array_keys($_POST)), LOG_WARNING);
http_response_code(400);
die("Missing token - Callback v".IDSCONNECT_CALLBACK_VERSION);
}
// Token verifizieren
$idsconnect = new IdsConnect($db);
$log = $idsconnect->verifyCallbackToken($token);
if ($log === false) {
dol_syslog("IDS Connect Callback: Ungültiger oder abgelaufener Token: ".$token." von IP: ".getUserRemoteIP(), LOG_WARNING);
http_response_code(403);
die("Invalid or expired token - Callback v".IDSCONNECT_CALLBACK_VERSION);
}
dol_syslog("IDS Connect Callback: Token gültig - LogID=".$log->id." Supplier=".$log->fk_supplier." Action=".$log->action_type, LOG_INFO);
// ============================================================
// Warenkorb-XML aus verschiedenen Quellen lesen
// ============================================================
$cart_xml = '';
$cart_source = '';
// 1. Reguläres POST-Feld 'warenkorb' (IDS Connect Standard)
if (!empty($_POST['warenkorb'])) {
$cart_xml = $_POST['warenkorb'];
$cart_source = 'POST[warenkorb]';
}
// 2. POST-Feld 'cart' (alternativer Name)
elseif (!empty($_POST['cart'])) {
$cart_xml = $_POST['cart'];
$cart_source = 'POST[cart]';
}
// 3. Datei-Upload 'warenkorb' (bei multipart/form-data möglich)
elseif (!empty($_FILES['warenkorb']['tmp_name']) && is_uploaded_file($_FILES['warenkorb']['tmp_name'])) {
$cart_xml = file_get_contents($_FILES['warenkorb']['tmp_name']);
$cart_source = 'FILES[warenkorb] ('.$_FILES['warenkorb']['name'].' '.$_FILES['warenkorb']['size'].' bytes)';
}
// 4. Datei-Upload 'cart'
elseif (!empty($_FILES['cart']['tmp_name']) && is_uploaded_file($_FILES['cart']['tmp_name'])) {
$cart_xml = file_get_contents($_FILES['cart']['tmp_name']);
$cart_source = 'FILES[cart] ('.$_FILES['cart']['name'].' '.$_FILES['cart']['size'].' bytes)';
}
// 5. Erster verfügbarer Datei-Upload
elseif (!empty($_FILES)) {
$first_file = reset($_FILES);
$first_key = key($_FILES);
if (!empty($first_file['tmp_name']) && is_uploaded_file($first_file['tmp_name'])) {
$cart_xml = file_get_contents($first_file['tmp_name']);
$cart_source = 'FILES['.$first_key.'] ('.$first_file['name'].' '.$first_file['size'].' bytes)';
}
}
// 6. Erster POST-Wert der XML enthält
if (empty($cart_xml)) {
foreach ($_POST as $key => $value) {
if (is_string($value) && (strpos($value, '<?xml') !== false || strpos($value, '<Warenkorb') !== false || strpos($value, '<Order') !== false)) {
$cart_xml = $value;
$cart_source = 'POST['.$key.'] (XML-Suche)';
break;
}
}
}
// 7. Raw POST body als Fallback
if (empty($cart_xml)) {
$raw = file_get_contents('php://input');
if (!empty($raw)) {
if (strpos($raw, '<?xml') !== false || strpos($raw, '<Warenkorb') !== false) {
$cart_xml = $raw;
$cart_source = 'php://input ('.strlen($raw).' bytes)';
}
}
}
$callback_meta['cart_source'] = $cart_source;
dol_syslog("IDS Connect Callback: Cart-Source=".$cart_source." Cart-Länge=".strlen($cart_xml)." POST-Felder=".implode(',', array_keys($_POST))." FILES-Felder=".implode(',', array_keys($_FILES)), LOG_INFO);
// ============================================================
// Debug-Daten IMMER in DB speichern (egal was passiert)
// ============================================================
$debug_data = array(
'callback_version' => IDSCONNECT_CALLBACK_VERSION,
'callback_meta' => $callback_meta,
'cart_source' => $cart_source,
'cart_xml_length' => strlen($cart_xml),
'post_keys' => array_keys($_POST),
'post_values_preview' => array(),
'files_info' => array(),
);
// POST-Werte gekürzt speichern
foreach ($_POST as $k => $v) {
if ($k === 'pw_kunde') {
$debug_data['post_values_preview'][$k] = '***';
} elseif (is_string($v)) {
$debug_data['post_values_preview'][$k] = substr($v, 0, 500).(strlen($v) > 500 ? '...['.strlen($v).' bytes]' : '');
} else {
$debug_data['post_values_preview'][$k] = gettype($v);
}
}
// FILES-Info speichern
foreach ($_FILES as $k => $f) {
$debug_data['files_info'][$k] = array(
'name' => $f['name'] ?? '',
'type' => $f['type'] ?? '',
'size' => $f['size'] ?? 0,
'error' => $f['error'] ?? -1,
);
}
// Roh-XML immer speichern wenn vorhanden
if (!empty($cart_xml)) {
$log->updateCart($cart_xml);
}
// ============================================================
// OCI-Format prüfen (NEW_ITEM-* POST-Felder)
// ============================================================
$oci_items = array();
$has_oci = false;
foreach ($_POST as $key => $value) {
if (strpos($key, 'NEW_ITEM-') === 0) {
$has_oci = true;
break;
}
}
if ($has_oci) {
dol_syslog("IDS Connect Callback: OCI-Format erkannt (NEW_ITEM-* Felder)", LOG_INFO);
$cart_source = 'OCI (POST NEW_ITEM-* Felder)';
$callback_meta['cart_source'] = $cart_source;
$debug_data['cart_source'] = $cart_source;
$debug_data['format'] = 'OCI';
// OCI-Felder parsen: NEW_ITEM-FELDNAME[index] = wert
$oci_raw = array();
foreach ($_POST as $key => $value) {
if (preg_match('/^NEW_ITEM-([A-Z_]+)\[?(\d*)\]?$/', $key, $m)) {
$field = $m[1];
$idx = $m[2] !== '' ? (int) $m[2] : 1;
$oci_raw[$idx][$field] = $value;
}
// Alternatives Format: NEW_ITEM-FELDNAME ohne Index
elseif (preg_match('/^NEW_ITEM-([A-Z_]+)$/', $key, $m) && is_array($value)) {
foreach ($value as $idx => $val) {
$oci_raw[$idx][$m[1]] = $val;
}
}
}
foreach ($oci_raw as $idx => $fields) {
$item = array(
'artikelnr' => $fields['VENDORMAT'] ?? $fields['MATNR'] ?? $fields['EXT_PRODUCT_ID'] ?? '',
'bezeichnung' => $fields['DESCRIPTION'] ?? $fields['LONGTEXT'] ?? '',
'langtext' => $fields['LONGTEXT'] ?? '',
'menge' => (float) ($fields['QUANTITY'] ?? 0),
'einheit' => $fields['UNIT'] ?? 'STK',
'einzelpreis' => (float) ($fields['PRICE'] ?? 0),
'angebotspreis' => 0,
'gesamtpreis' => 0,
'ean' => $fields['EAN'] ?? '',
'hersteller' => $fields['MANUFACTMAT'] ?? $fields['VENDOR'] ?? '',
'herstellernr' => $fields['MANUFACTMAT'] ?? '',
'hinweis' => '',
);
if ($item['menge'] > 0 && $item['einzelpreis'] > 0) {
$item['gesamtpreis'] = $item['menge'] * $item['einzelpreis'];
}
if (!empty($item['artikelnr']) || !empty($item['bezeichnung'])) {
$oci_items[] = $item;
}
}
}
// ============================================================
// Verarbeitung
// ============================================================
// OCI-Artikel haben Vorrang wenn gefunden
if (!empty($oci_items)) {
$items = $oci_items;
// Als "XML" für die DB speichern (OCI-Rohdaten als JSON)
$oci_log = json_encode($_POST, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
$log->updateCart($oci_log);
$debug_data['result'] = 'success';
$debug_data['format'] = 'OCI';
$debug_data['item_count'] = count($items);
$debug_data['items'] = $items;
$log->updateStatus('success', json_encode($debug_data));
dol_syslog("IDS Connect Callback: OCI ".count($items)." Artikel empfangen für Supplier ".$log->fk_supplier, LOG_INFO);
$_SESSION['idsconnect_callback'] = array(
'log_id' => $log->id,
'supplier_id' => $log->fk_supplier,
'items' => $items,
'timestamp' => dol_now(),
);
idsconnectCallbackPage('Warenkorb empfangen (OCI): '.count($items).' Artikel', 'success', $log, $debug_data);
exit;
}
if (empty($cart_xml)) {
$debug_data['result'] = 'no_cart';
$log->updateStatus('cancelled', json_encode($debug_data), 'Kein Warenkorb empfangen');
dol_syslog("IDS Connect Callback: Kein Warenkorb empfangen für Token ".$token, LOG_INFO);
// Ergebnis direkt anzeigen (NOLOGIN-Seite, kein Redirect nötig)
idsconnectCallbackPage('Kein Warenkorb', 'warning', $log, $debug_data);
exit;
}
// Warenkorb parsen (IDS XML-Format)
$items = $idsconnect->parseCartXml($cart_xml);
if ($items === false) {
$debug_data['result'] = 'parse_error';
$debug_data['parse_error'] = $idsconnect->error;
$debug_data['cart_xml_preview'] = substr($cart_xml, 0, 3000);
$log->updateStatus('error', json_encode($debug_data), 'XML-Parse-Fehler: '.$idsconnect->error);
dol_syslog("IDS Connect Callback: XML-Parse-Fehler: ".$idsconnect->error." Quelle=".$cart_source." XML-Anfang: ".substr($cart_xml, 0, 300), LOG_ERR);
idsconnectCallbackPage('XML-Fehler: '.$idsconnect->error, 'error', $log, $debug_data, $cart_xml);
exit;
}
// Erfolg!
$debug_data['result'] = 'success';
$debug_data['item_count'] = count($items);
$debug_data['items'] = $items;
$log->updateStatus('success', json_encode($debug_data));
dol_syslog("IDS Connect Callback: ".count($items)." Artikel empfangen für Supplier ".$log->fk_supplier." XML-Länge=".strlen($cart_xml), LOG_INFO);
// Session-Daten für die Weiterverarbeitung
$_SESSION['idsconnect_callback'] = array(
'log_id' => $log->id,
'supplier_id' => $log->fk_supplier,
'items' => $items,
'timestamp' => dol_now(),
);
idsconnectCallbackPage('Warenkorb empfangen: '.count($items).' Artikel', 'success', $log, $debug_data);
exit;
// ============================================================
// HTML-Ausgabe-Funktion (kein Redirect, NOLOGIN-Seite)
// ============================================================
/**
* Zeigt das Callback-Ergebnis direkt an
*
* @param string $message Nachricht
* @param string $type success, error, warning
* @param IdsLog $log Log-Eintrag
* @param array $debug Debug-Daten
* @param string $raw_xml Roh-XML für Anzeige
*/
function idsconnectCallbackPage($message, $type, $log, $debug = array(), $raw_xml = '')
{
$colors = array(
'success' => array('bg' => '#d4edda', 'border' => '#28a745', 'text' => '#155724'),
'error' => array('bg' => '#f8d7da', 'border' => '#dc3545', 'text' => '#721c24'),
'warning' => array('bg' => '#fff3cd', 'border' => '#ffc107', 'text' => '#856404'),
);
$c = $colors[$type] ?? $colors['warning'];
// Dolibarr-Link: Die URL verwenden über die der User beim Launch eingeloggt war
// Priorität: 1. user_base_url aus Log, 2. IDSCONNECT_PUBLIC_URL, 3. $dolibarr_main_url_root
global $dolibarr_main_url_root;
$internal_base = '';
// Aus dem Log-Eintrag die Herkunfts-URL des Users lesen
if ($log && !empty($log->request_data)) {
$req = json_decode($log->request_data, true);
if (!empty($req['user_base_url'])) {
$internal_base = rtrim($req['user_base_url'], '/');
}
}
// Fallback auf IDSCONNECT_PUBLIC_URL oder $dolibarr_main_url_root
if (empty($internal_base)) {
$public_url = getDolGlobalString('IDSCONNECT_PUBLIC_URL');
if (!empty($public_url)) {
$internal_base = rtrim($public_url, '/');
} else {
$internal_base = $dolibarr_main_url_root;
if (!empty($internal_base) && strpos($internal_base, '://') === false) {
$internal_base = 'http://'.$internal_base;
}
}
}
$dolibarr_link = $internal_base.'/custom/idsconnect/idsconnectindex.php';
header('Content-Type: text/html; charset=UTF-8');
echo '<!DOCTYPE html><html><head><meta charset="UTF-8">';
echo '<title>IDS Connect Callback v'.IDSCONNECT_CALLBACK_VERSION.'</title>';
echo '<style>body{font-family:Arial,sans-serif;max-width:800px;margin:20px auto;padding:0 20px;background:#f5f5f5}';
echo '.box{padding:15px;border-radius:5px;margin:10px 0;border-left:4px solid}';
echo 'pre{background:#f8f9fa;padding:10px;overflow:auto;max-height:400px;font-size:12px;border:1px solid #ddd;border-radius:3px}';
echo 'details{margin:10px 0}summary{cursor:pointer;font-weight:bold;padding:5px}</style>';
echo '</head><body>';
echo '<h2>IDS Connect Callback <small style="color:#999">v'.IDSCONNECT_CALLBACK_VERSION.'</small></h2>';
// Hauptnachricht
echo '<div class="box" style="background:'.$c['bg'].';border-color:'.$c['border'].';color:'.$c['text'].'">';
echo '<strong>'.htmlspecialchars($message).'</strong>';
echo '<br>Log-Eintrag: #'.$log->id;
echo '</div>';
// Bei Erfolg: Artikelübersicht
if ($type === 'success' && !empty($debug['items'])) {
echo '<h3>Empfangene Artikel</h3>';
echo '<table style="width:100%;border-collapse:collapse;background:white">';
echo '<tr style="background:#2c3e50;color:white"><th style="padding:8px">Art.-Nr.</th><th style="padding:8px">Bezeichnung</th><th style="padding:8px;text-align:right">Menge</th><th style="padding:8px">Einheit</th><th style="padding:8px;text-align:right">Preis</th></tr>';
foreach ($debug['items'] as $item) {
echo '<tr style="border-bottom:1px solid #eee">';
echo '<td style="padding:6px"><code>'.htmlspecialchars($item['artikelnr'] ?? '').'</code></td>';
echo '<td style="padding:6px">'.htmlspecialchars($item['bezeichnung'] ?? '').'</td>';
echo '<td style="padding:6px;text-align:right">'.($item['menge'] ?? 0).'</td>';
echo '<td style="padding:6px">'.htmlspecialchars($item['einheit'] ?? 'STK').'</td>';
echo '<td style="padding:6px;text-align:right">'.number_format($item['einzelpreis'] ?? 0, 2, ',', '.').'</td>';
echo '</tr>';
}
echo '</table>';
}
// Debug-Infos
echo '<details><summary>Debug-Informationen</summary>';
echo '<table style="width:100%;background:white;border-collapse:collapse">';
$show_fields = array(
'Callback-Version' => $debug['callback_version'] ?? '-',
'Ergebnis' => $debug['result'] ?? '-',
'Cart-Quelle' => $debug['cart_source'] ?? 'keine',
'Cart-XML-Länge' => ($debug['cart_xml_length'] ?? 0).' Bytes',
'POST-Felder' => implode(', ', $debug['post_keys'] ?? array()) ?: 'keine',
'FILES-Felder' => implode(', ', array_keys($debug['files_info'] ?? array())) ?: 'keine',
'Content-Type' => $debug['callback_meta']['content_type'] ?? '-',
'IP-Adresse' => $debug['callback_meta']['remote_ip'] ?? '-',
'Zeitstempel' => $debug['callback_meta']['timestamp'] ?? '-',
);
foreach ($show_fields as $label => $value) {
echo '<tr style="border-bottom:1px solid #eee"><td style="padding:5px;font-weight:bold">'.htmlspecialchars($label).'</td>';
echo '<td style="padding:5px"><code>'.htmlspecialchars($value).'</code></td></tr>';
}
echo '</table>';
// POST-Werte
if (!empty($debug['post_values_preview'])) {
echo '<details><summary>POST-Werte (gekürzt)</summary><pre>';
echo htmlspecialchars(json_encode($debug['post_values_preview'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
echo '</pre></details>';
}
// FILES-Info
if (!empty($debug['files_info'])) {
echo '<details><summary>FILES-Info</summary><pre>';
echo htmlspecialchars(json_encode($debug['files_info'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
echo '</pre></details>';
}
echo '</details>';
// Roh-XML bei Fehler anzeigen
if (!empty($raw_xml)) {
echo '<details><summary>Roh-XML ('.strlen($raw_xml).' Bytes)</summary>';
echo '<pre>'.htmlspecialchars(substr($raw_xml, 0, 5000)).'</pre>';
if (strlen($raw_xml) > 5000) {
echo '<p style="color:#999">... gekürzt, vollständiges XML im Log #'.$log->id.'</p>';
}
echo '</details>';
}
// Links
echo '<div style="margin-top:20px;padding:15px;background:white;border-radius:5px">';
echo '<p><strong>Weiter:</strong></p>';
echo '<a href="'.htmlspecialchars($dolibarr_link).'" style="display:inline-block;padding:8px 20px;background:#27ae60;color:white;text-decoration:none;border-radius:5px;margin-right:10px">Zur IDS Connect Übersicht</a>';
if ($type === 'success') {
$review_link = $internal_base.'/custom/idsconnect/cart_review.php?log_id='.$log->id;
echo '<a href="'.htmlspecialchars($review_link).'" style="display:inline-block;padding:8px 20px;background:#3498db;color:white;text-decoration:none;border-radius:5px">Warenkorb prüfen</a>';
}
echo '</div>';
echo '</body></html>';
}