Files
Kundenverwaltung/incs/db.php
T
adminandClaude Opus 5 3dfbf1c9c7 Kunden und Adressen auf Prepared Statements umgebaut
incs/saveNchange.php neu geschrieben: Adressen, Telefonnummern, Kontakte,
Notizen und Konten laufen ueber db_abfrage() mit Platzhaltern, mehrschrittiges
Speichern in einer Transaktion. Weil sich die Speicherfunktionen gegenseitig
aufrufen, klammern beginneSpeichern()/beendeSpeichern() die Verschachtelung;
incs/rechnung.php nutzt sie ebenfalls.

Kunden- und Lieferantenseiten waren zwei fast gleiche Kopien und teilen sich
jetzt partnerAddSeite(), partnerChangeSeite() und partnerShowDaten().

Dabei behoben:
- SQL-Injektion ueber die Suchbegriffe der Kunden-, Lieferanten-, Adress-,
  Kommissions- und Projektsuche sowie ueber die Artikel-Autovervollstaendigung
- gespeichertes XSS: Notizen (nl2br escapt nicht), Bankname, Dokumenttyp,
  Namen und Adressen in allen Trefferlisten
- sites/liefershow.php war unter PHP 8 ein Fatal ($kdnres.id)
- "Telefonnummer loeschen" im Kundenbereich lief in einen Fatal, die Nummern
  einer geloeschten Adresse blieben stehen (Spalte hiess adress_id statt
  adresse_id, mysql_fetch_assoc gibt es nicht mehr)
- Geburtstag ohne Jahr wurde im Formular verschluckt, die IBAN kam beim
  Bearbeiten aus $_GET statt aus dem Konto
- die Versionskette eines Kunden brach nach einem Schritt ab
- Kommissionsadressen speicherten die Ortsangabe der Adresse statt der des
  Telefonformulars und einen Adresstyp als Telefontyp
- Blaettern in den Trefferlisten: ohne ORDER BY konnte dieselbe Zeile zweimal
  oder gar nicht erscheinen; ajax_searchadress() bekam die Seite nicht
- Hauptadresse laesst sich nicht mehr loeschen, Kundendaten nicht mehr ueber
  die Lieferantenseite (und umgekehrt)
- ajax/auto/* baute JSON von Hand zusammen, ein Anfuehrungszeichen im Ort
  machte die Liste unbrauchbar

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 19:47:01 +02:00

140 lines
4.4 KiB
PHP

<?php
/*
* Datenbankzugriff: Verbindungsaufbau und Prepared Statements.
*
* Neue und umgebaute Abfragen laufen über db_abfrage() und Co. Werte stehen
* im SQL nur als ? und kommen getrennt als Array - sie werden nie in den
* SQL-String eingesetzt, Escaping entfällt. Tabellen- und Spaltennamen
* (z.B. für ORDER BY) lassen sich nicht binden und müssen gegen eine feste
* Liste erlaubter Werte geprüft werden.
*
* Fehler verhalten sich wie bei mysqli_query(): Rückgabe false, Details über
* mysqli_error($dbconn). Das bestehende Muster
* db_abfrage(...) or $err = error(...mysqli_error($dbconn));
* funktioniert also unverändert.
*/
/*
* Baut die Verbindung auf und stellt sie einheitlich ein. Gibt die
* Verbindung zurück oder false; der Grund steht dann in
* mysqli_connect_error().
*/
function db_verbinden($server, $user, $pw, $db)
{
// Seit PHP 8.1 wirft mysqli standardmäßig Exceptions. Der Code prüft
// Rückgabewerte, also das alte Verhalten wiederherstellen.
mysqli_report(MYSQLI_REPORT_OFF);
$dbconn = @mysqli_connect($server, $user, $pw, $db);
if(!$dbconn)
return false;
// Ohne das behandelt der Server die Verbindung je nach Einstellung als
// latin1, und UTF-8 aus dem Browser wird doppelt kodiert gespeichert.
mysqli_set_charset($dbconn, "utf8mb4");
// Unabhängig von der Server-Konfiguration immer strikt: ungültige Werte
// und fehlende Pflichtfelder sind Fehler, statt still zu '' oder 0 zu
// werden.
mysqli_query($dbconn, "SET SESSION sql_mode = 'STRICT_ALL_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'");
return $dbconn;
}
/*
* Führt eine Abfrage mit ?-Platzhaltern aus. Gibt bei SELECT ein
* mysqli_result zurück, sonst true, bei Fehlern false.
*
* Achtung: Prepared Statements liefern Zahlen als int/float, mysqli_query()
* lieferte Strings. db_zeilen(), db_zeile(), db_wert() und db_zuordnung()
* wandeln deshalb in Strings um, damit sich alter Code (=== "0", "1" etc.)
* gleich verhält. Wer das Ergebnis von db_abfrage() selbst ausliest, bekommt
* die nativen Typen.
*/
function db_abfrage($dbconn, $sql, $params = array())
{
return mysqli_execute_query($dbconn, $sql, array_values($params));
}
/* Werte einer Zeile als Strings wie bei mysqli_query(), NULL bleibt NULL */
function db_als_text($zeile)
{
if(!is_array($zeile))
return $zeile;
foreach($zeile as $spalte => $wert)
if($wert !== null && !is_string($wert))
$zeile[$spalte] = (string)$wert;
return $zeile;
}
/* Alle Zeilen als Liste assoziativer Arrays, bei Fehlern false. */
function db_zeilen($dbconn, $sql, $params = array())
{
$res = db_abfrage($dbconn, $sql, $params);
if(!$res)
return false;
return array_map("db_als_text", mysqli_fetch_all($res, MYSQLI_ASSOC));
}
/* Erste Zeile als assoziatives Array, null ohne Treffer, bei Fehlern false. */
function db_zeile($dbconn, $sql, $params = array())
{
$res = db_abfrage($dbconn, $sql, $params);
if(!$res)
return false;
return db_als_text(mysqli_fetch_assoc($res));
}
/*
* Zwei Spalten als Zuordnung erste => zweite, etwa für Auswahllisten
* (SELECT id, anrede FROM anrede). Bei Fehlern false.
*/
function db_zuordnung($dbconn, $sql, $params = array())
{
$res = db_abfrage($dbconn, $sql, $params);
if(!$res)
return false;
$zuordnung = array();
while($zeile = mysqli_fetch_row($res))
$zuordnung[$zeile[0]] = $zeile[1] === null ? null : (string)$zeile[1];
return $zuordnung;
}
/*
* Die Abfrage mit eingesetzten Werten als lesbarer Text - ausschließlich für
* das Änderungsprotokoll (Tabelle log). Nie ausführen.
*/
function db_sql_text($dbconn, $sql, $params = array())
{
$teile = explode("?", $sql);
$text = array_shift($teile);
foreach(array_values($params) as $i => $wert)
{
if($wert === null)
$text .= "NULL";
else
$text .= "'".mysqli_real_escape_string($dbconn, (string)$wert)."'";
$text .= isset($teile[$i]) ? $teile[$i] : "";
}
return $text;
}
/*
* Text als Praefix-Suchmuster fuer LIKE. % und _ waeren darin sonst
* Platzhalter; das Fluchtzeichen ist der Backslash.
*/
function db_like_praefix($text)
{
return str_replace(array("\\", "%", "_"), array("\\\\", "\%", "\_"), (string)$text)."%";
}
/* Erste Spalte der ersten Zeile, null ohne Treffer, bei Fehlern false. */
function db_wert($dbconn, $sql, $params = array())
{
$res = db_abfrage($dbconn, $sql, $params);
if(!$res)
return false;
$zeile = mysqli_fetch_row($res);
return $zeile === null || $zeile[0] === null ? null : (string)$zeile[0];
}