- incs/rechnung.php: alle Abfragen mit Platzhaltern. saveRechnung() setzte $_POST ungeprueft in SQL (Injection; ein Apostroph im Text brach das Speichern ab). Speichern, Kopieren und Loeschen laufen in Transaktionen, bei Fehlern bleibt nichts Halbes zurueck. Formularwerte ueber formularText/-Zahl/-Ganzzahl (strikter Modus). - copyRech() trug die Rechnungsnummer ohne typ_id ein (Fehler im strikten Modus) und zaehlte typuebergreifend; jetzt wie beim Anlegen pro Typ (neueRechnungsnummer()). Artikel und Eigenschaften per INSERT ... SELECT. - Adressblock-Formatierung aus makeadressblock() und makeadressblockUmschlag() in formatiereAdressblock() zusammengefuehrt. - ajax/autosave.php und ajax/preview.php teilen sich saveTmpRechnung(); ueberzaehlige Entwuerfe werden samt Artikeln entfernt (vorher FK-Fehler). - sites/rechnungadd.php: Storno ohne SQL-Injection ueber $_GET["id"], Auswahllisten ueber db_zuordnung(), tote Kommissions-Abfrage entfernt. - Rechnungssuche (createRechQry/createKdnRechQry) liefert SQL und Parameter getrennt; Suchbegriffe waren ungeprueft im SQL. - saveLog() mit Platzhaltern, Protokolltext ueber db_sql_text(). - db.php: db_zuordnung(), db_sql_text(); Helfer liefern Strings wie mysqli_query(), damit alter Code sich gleich verhaelt. Getestet: alter und neuer Code lesen dieselbe migrierte Datenbank, 150 von 155 Pruefungen (10.936 Werte) identisch, die uebrigen 5 sind der Injection-Suchbegriff (alt: SQL-Fehler). Schreibtests fuer Entwurf, Anlegen, Nummernkreise, Aendern, Rollback, Kopieren, Adresse, Storno und Loeschen im strikten Modus. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
131 lines
4.1 KiB
PHP
131 lines
4.1 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;
|
|
}
|
|
|
|
/* 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];
|
|
}
|