Version 3.3: Sicherheit, Error-Handling und Berechtigungen

- XSS Fixes: $_SERVER['PHP_SELF'] und EAN-Ausgabe escaped
- Error-Handling fuer rename()/copy() Dateioperationen
- DB-Transaktion bei Force Reimport (Race Condition)
- db->query() Rueckgabewerte bei Extrafields geprueft
- Berechtigungspruefung fuer Index-Seite und Loeschen
- Helper-Funktionen fuer Lieferantenpreis-Erstellung (DRY)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-02-17 05:30:06 +01:00
parent 06acc0b2f9
commit 1b2357a2aa
3 changed files with 140 additions and 11 deletions

View file

@ -1,5 +1,17 @@
# CHANGELOG MODULE IMPORTZUGFERD FOR [DOLIBARR ERP CRM](https://www.dolibarr.org)
## 3.3
### Sicherheit und Code-Qualitaet
- XSS Fix: $_SERVER['PHP_SELF'] in JavaScript escaped (dol_escape_js)
- XSS Fix: EAN-Ausgabe in HTML escaped (dol_escape_htmltag)
- Error-Handling: rename()/copy() Dateioperationen mit Fehlerbehandlung
- Race Condition: DB-Transaktion bei Force Reimport hinzugefuegt
- Error-Handling: db->query() Rueckgabewerte bei Extrafields-Insert geprueft
- Berechtigungspruefung: Index-Seite prueft jetzt import:read Recht
- Berechtigungspruefung: Loeschen prueft jetzt import:delete Recht
- Helper-Funktionen fuer Lieferantenpreis-Erstellung (DRY)
## 3.2
### Neue Funktionen

View file

@ -89,6 +89,107 @@ $notification = new ImportNotification($db);
$error = 0;
$message = '';
/*
* Helper-Funktionen (DRY)
*/
/**
* Extrafields fuer Lieferantenpreis aus Datanorm-Daten zusammenstellen
*
* @param Datanorm $datanorm Datanorm-Objekt
* @param ImportLine|null $lineObj Import-Zeile (optional, fuer ZUGFeRD-Daten)
* @return array Extrafields-Array
*/
function datanormBuildSupplierPriceExtrafields($datanorm, $lineObj = null)
{
$extrafields = array();
// Kupferzuschlag
if (!empty($datanorm->metal_surcharge) && $datanorm->metal_surcharge > 0) {
$extrafields['options_kupferzuschlag'] = $datanorm->metal_surcharge;
} elseif ($lineObj && !empty($lineObj->copper_surcharge) && $lineObj->copper_surcharge > 0) {
$extrafields['options_kupferzuschlag'] = $lineObj->copper_surcharge;
}
// Preiseinheit
if (!empty($datanorm->price_unit) && $datanorm->price_unit > 1) {
$extrafields['options_preiseinheit'] = $datanorm->price_unit;
} elseif ($lineObj && !empty($lineObj->basis_quantity) && $lineObj->basis_quantity > 1) {
$extrafields['options_preiseinheit'] = $lineObj->basis_quantity;
}
// Warengruppe
if (!empty($datanorm->product_group)) {
$extrafields['options_warengruppe'] = $datanorm->product_group;
}
return $extrafields;
}
/**
* Lieferantenpreis aus Datanorm hinzufuegen
*
* @param DoliDB $db Datenbank
* @param int $productId Produkt-ID
* @param Datanorm $datanorm Datanorm-Objekt
* @param Societe $supplier Lieferant-Objekt
* @param User $user Benutzer
* @param float $purchasePrice Einkaufspreis
* @param float $taxPercent MwSt-Satz
* @param array $extrafields Extrafields
* @return int >0 bei Erfolg, <0 bei Fehler
*/
function datanormAddSupplierPrice($db, $productId, $datanorm, $supplier, $user, $purchasePrice, $taxPercent = 19, $extrafields = array())
{
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
$prodfourn = new ProductFournisseur($db);
$prodfourn->id = $productId;
$supplierEan = !empty($datanorm->ean) ? $datanorm->ean : '';
$supplierEanType = !empty($datanorm->ean) ? 2 : 0;
$description = trim($datanorm->short_text1 . ($datanorm->short_text2 ? ' ' . $datanorm->short_text2 : ''));
return $prodfourn->update_buyprice(
1, $purchasePrice, $user, 'HT', $supplier, 0,
$datanorm->article_number, $taxPercent,
0, 0, 0, 0, 0, 0, array(), '',
0, 'HT', 1, '',
$description, $supplierEan, $supplierEanType,
$extrafields
);
}
/**
* Extrafields in product_fournisseur_price_extrafields einfuegen
*
* @param DoliDB $db Datenbank
* @param int $priceId ID des Lieferantenpreises
* @param array $extrafields Extrafields-Array
*/
function datanormInsertPriceExtrafields($db, $priceId, $extrafields)
{
if (empty($priceId) || empty($extrafields)) {
return;
}
$kupferzuschlag = !empty($extrafields['options_kupferzuschlag']) ? (float)$extrafields['options_kupferzuschlag'] : 'NULL';
$preiseinheit = !empty($extrafields['options_preiseinheit']) ? (int)$extrafields['options_preiseinheit'] : 1;
$warengruppe = !empty($extrafields['options_warengruppe']) ? "'".$db->escape($extrafields['options_warengruppe'])."'" : 'NULL';
// Pruefen ob bereits vorhanden
$sqlCheck = "SELECT rowid FROM ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields WHERE fk_object = ".(int)$priceId;
$resCheck = $db->query($sqlCheck);
if ($resCheck && $db->num_rows($resCheck) > 0) {
return; // Bereits vorhanden
}
$sql = "INSERT INTO ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields";
$sql .= " (fk_object, kupferzuschlag, preiseinheit, warengruppe) VALUES (";
$sql .= (int)$priceId.", ";
$sql .= ($kupferzuschlag === 'NULL' ? "NULL" : $kupferzuschlag).", ";
$sql .= $preiseinheit.", ";
$sql .= $warengruppe.")";
if (!$db->query($sql)) {
dol_syslog('ImportZugferd: Fehler beim Einfuegen der Extrafields: '.$db->lasterror(), LOG_ERR);
}
}
/*
* Actions
*/
@ -189,16 +290,16 @@ if ($action == 'upload') {
$oldImport = new ZugferdImport($db);
$oldImport->fetch(0, null, $file_hash);
if ($oldImport->id > 0) {
// Delete old lines
$db->begin();
// Alten Import-Datensatz komplett loeschen (Transaktion)
$oldLines = new ImportLine($db);
$oldLines->deleteAllByImport($oldImport->id);
// Delete old files
$old_dir = $conf->importzugferd->dir_output.'/imports/'.$oldImport->id;
if (is_dir($old_dir)) {
dol_delete_dir_recursive($old_dir);
}
// Delete old import record
$oldImport->delete($user);
$db->commit();
}
}
// Parse the file
@ -281,7 +382,14 @@ if ($action == 'upload') {
if (!is_dir($final_dir)) {
dol_mkdir($final_dir);
}
rename($destfile, $final_dir.'/'.$filename);
if (!@rename($destfile, $final_dir.'/'.$filename)) {
// Fallback: copy + delete (z.B. bei verschiedenen Dateisystemen)
if (@copy($destfile, $final_dir.'/'.$filename)) {
@unlink($destfile);
} else {
dol_syslog('ImportZugferd: Fehler beim Verschieben der PDF nach '.$final_dir, LOG_ERR);
}
}
// Send notification if manual intervention required
if ($import->status == ZugferdImport::STATUS_PENDING) {
@ -737,7 +845,9 @@ if ($action == 'createfromdatanorm' && $line_id > 0) {
$sqlInsertExtra .= ($kupferzuschlag === 'NULL' ? "NULL" : $kupferzuschlag).", ";
$sqlInsertExtra .= $preiseinheit.", ";
$sqlInsertExtra .= $warengruppe.")";
$db->query($sqlInsertExtra);
if (!$db->query($sqlInsertExtra)) {
dol_syslog('ImportZugferd: Fehler beim Einfuegen der Extrafields: '.$db->lasterror(), LOG_ERR);
}
}
}
@ -1149,7 +1259,9 @@ if ($action == 'createallfromdatanorm' && $id > 0) {
$sqlInsertExtra .= ($kupferzuschlag === 'NULL' ? "NULL" : $kupferzuschlag).", ";
$sqlInsertExtra .= $preiseinheit.", ";
$sqlInsertExtra .= $warengruppe.")";
$db->query($sqlInsertExtra);
if (!$db->query($sqlInsertExtra)) {
dol_syslog('ImportZugferd: Fehler beim Einfuegen der Extrafields: '.$db->lasterror(), LOG_ERR);
}
}
}
@ -1507,7 +1619,9 @@ if ($action == 'createinvoice' && $id > 0) {
if (!is_dir($dest_dir)) {
dol_mkdir($dest_dir);
}
copy($source_pdf, $dest_dir.'/'.$import->pdf_filename);
if (!@copy($source_pdf, $dest_dir.'/'.$import->pdf_filename)) {
dol_syslog('ImportZugferd: Fehler beim Kopieren der PDF nach '.$dest_dir, LOG_ERR);
}
}
// Update import record
@ -1593,7 +1707,7 @@ if ($action == 'finishimport' && $id > 0) {
}
// Delete import record
if ($action == 'confirm_delete' && $confirm == 'yes' && $id > 0) {
if ($action == 'confirm_delete' && $confirm == 'yes' && $id > 0 && $user->hasRight('importzugferd', 'import', 'delete')) {
$import->fetch($id);
// Delete lines first
@ -1865,7 +1979,7 @@ if ($action == 'edit' && $import->id > 0) {
print '<td>';
print dol_escape_htmltag($line->product_name);
if (!empty($line->ean) && !$hasProduct) {
print '<br><span style="color: #666;">EAN: '.$line->ean.'</span>';
print '<br><span style="color: #666;">EAN: '.dol_escape_htmltag($line->ean).'</span>';
}
print '</td>';
print '<td class="right">'.price2num($line->quantity, 'MS').' '.zugferdGetUnitLabel($line->unit_code).'</td>';
@ -1940,7 +2054,7 @@ if ($action == 'edit' && $import->id > 0) {
print '<br><span class="opacitymedium">'.$langs->trans('MatchMethod').': '.$line->match_method.'</span>';
}
if (!empty($line->ean)) {
print '<br><span class="opacitymedium"><i class="fas fa-barcode"></i> '.$line->ean.'</span>';
print '<br><span class="opacitymedium"><i class="fas fa-barcode"></i> '.dol_escape_htmltag($line->ean).'</span>';
}
print ' <i class="fas fa-check-circle" style="color: green;"></i>';
} else {
@ -2568,7 +2682,7 @@ function showRawDatanorm(articleNumber, fkSoc) {
// AJAX request
var xhr = new XMLHttpRequest();
xhr.open("GET", "'.$_SERVER['PHP_SELF'].'?action=get_raw_lines&article_number=" + encodeURIComponent(articleNumber) + "&fk_soc=" + fkSoc + "&token='.newToken().'", true);
xhr.open("GET", "'.dol_escape_js($_SERVER['PHP_SELF']).'?action=get_raw_lines&article_number=" + encodeURIComponent(articleNumber) + "&fk_soc=" + fkSoc + "&token='.newToken().'", true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {

View file

@ -54,6 +54,9 @@ $langs->loadLangs(array("importzugferd@importzugferd"));
if (!isModEnabled('importzugferd')) {
accessforbidden('Module not enabled');
}
if (!$user->hasRight('importzugferd', 'import', 'read')) {
accessforbidden();
}
/*
* View