Instanzname in die Config, Setup-Luecke geschlossen

Instanz:
- $instanz in der externen Config bestimmt INSTANZ, UPLOADS_PATH und
  SESSION_NAME. Der Verzeichnisname waehlt nur noch die Config-Datei aus
  und ist Fallback, solange $instanz fehlt.
- savePDF() schreibt nicht mehr fest nach DOCUMENT_ROOT."kunden", sondern
  in das Verzeichnis, das es auch aufraeumt. .gitkeep wird nicht mehr
  mitgeloescht.

Sicherheit:
- index.php?action=setup war ohne Login auch bei gueltiger Config
  erreichbar. rebuild-settings.php schrieb $_POST ungeprueft als PHP-Code
  in die Config (Codeausfuehrung, sofern die Datei schreibbar war).
- Setup nur noch ohne gueltige Config, firststart.php und
  rebuild-settings.php pruefen das zusaetzlich selbst.
- Config-Werte werden per var_export() maskiert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-17 08:44:33 +02:00
co-authored by Claude Opus 5
parent 70c8600efe
commit 73e64b72ac
6 changed files with 89 additions and 30 deletions
+11 -8
View File
@@ -14,19 +14,22 @@ Syntaxprüfung der eigenen Dateien:
for f in $(git ls-files "*.php" | grep -vE "^(incs/(tcpdf|pChart|PHPMailer|WebAuthn|drawer|font)|smarty|spellcheck|js)/"); do php -l "$f"; done for f in $(git ls-files "*.php" | grep -vE "^(incs/(tcpdf|pChart|PHPMailer|WebAuthn|drawer|font)|smarty|spellcheck|js)/"); do php -l "$f"; done
``` ```
Zum Ausführen braucht es einen Webserver mit PHP, eine MariaDB/MySQL-Instanz und die externe Konfigurationsdatei (siehe unten). Ohne die Konfiguration springt `index.php` in den Setup-Modus (`update/firststart.php`). Zum Ausführen braucht es einen Webserver mit PHP, eine MariaDB/MySQL-Instanz und die externe Konfigurationsdatei (siehe unten). Ohne die Konfiguration springt `index.php` in den Setup-Modus (`update/firststart.php`). **Nur** dann: Das Setup läuft ohne Login, schreibt die Config und kann die Datenbank leeren. Deshalb wird `action=setup` bei gültiger Config verworfen, und `firststart.php` und `rebuild-settings.php` brechen selbst ab. Werte, die in die Config geschrieben werden, immer über `var_export()` maskieren die Config ist ausführbarer PHP-Code.
## Die zwei zentralen Mechanismen ## Die zwei zentralen Mechanismen
### 1. Das Verzeichnis bestimmt die Instanz ### 1. Das Verzeichnis bestimmt die Instanz
`incs/constants.php` leitet aus `getcwd()` den Namen des Installationsverzeichnisses ab und baut daraus: `incs/constants.php` leitet aus `getcwd()` den Namen des Installationsverzeichnisses ab. Der wählt aber nur noch die Konfigurationsdatei aus. Der eigentliche Instanzname steht in der Config als `$instanz` und wird zur Konstante `INSTANZ`:
| Konstante | Wert bei Installation in `…/kunden` | | Konstante | abgeleitet aus | Wert bei Installation in `…/kunden` |
|---|---| |---|---|---|
| `CONFIG_FILE` | `kunden_settings.conf` | | `CONFIG_FILE` | Verzeichnisname | `kunden_settings.conf` |
| `UPLOADS_PATH` | `uploads_kunden/` | | `INSTANZ` | `$instanz` in der Config | `kunden` |
| `SESSION_NAME` | `KUNDENSESS` | | `UPLOADS_PATH` | `INSTANZ` | `uploads_kunden/` |
| `SESSION_NAME` | `INSTANZ` | `KUNDENSESS` |
Fehlt `$instanz` in der Config (ältere Configs, Ersteinrichtung), fällt `INSTANZ` auf den Verzeichnisnamen zurück. Erlaubt sind nur Buchstaben und Ziffern, sonst bricht der Request ab. Die Config wird dafür schon in `constants.php` einmal eingelesen, also **vor** `session_start()`. Code in der Config darf deshalb nichts ausgeben und keine Verbindung voraussetzen.
Aus **einer** Codebasis laufen dadurch mehrere getrennte Instanzen — die `uploads_dev`, `uploads_ingwa`, `uploads_kunden`, `uploads_lsg` im Baum sind die Spuren davon. Jede Instanz hat eigene Config, eigene Session, eigene Uploads. Aus **einer** Codebasis laufen dadurch mehrere getrennte Instanzen — die `uploads_dev`, `uploads_ingwa`, `uploads_kunden`, `uploads_lsg` im Baum sind die Spuren davon. Jede Instanz hat eigene Config, eigene Session, eigene Uploads.
@@ -42,7 +45,7 @@ cd /home/intranet/kunden && php db-backup.php
`CONFIG_PATH` zeigt auf `/volume1/homes/wagner/kundenconfig/` (Linux) bzw. `c:/kundenverwaltung/config/` (Windows). Geschrieben wird die Datei von `update/rebuild-settings.php` beim Ersteinrichten. `CONFIG_PATH` zeigt auf `/volume1/homes/wagner/kundenconfig/` (Linux) bzw. `c:/kundenverwaltung/config/` (Windows). Geschrieben wird die Datei von `update/rebuild-settings.php` beim Ersteinrichten.
Die Datei deklariert `$mysql_user`, `$mysql_pw`, `$mysql_server`, `$mysql_db`, `$firma` und weitere ausdrücklich als `global` und weist sie erst danach zu. Nur deshalb funktioniert `readsettings()` in `incs/settings.php`: Das `include` passiert im Funktionsrumpf, ohne die `global`-Zeilen blieben die Variablen dort lokal. Wer die Config-Struktur ändert, muss diese Eigenheit erhalten. Neben den Zugangsdaten steht dort auch `$instanz` (siehe oben). Die Datei deklariert `$mysql_user`, `$mysql_pw`, `$mysql_server`, `$mysql_db`, `$firma` und weitere ausdrücklich als `global` und weist sie erst danach zu. Nur deshalb funktioniert `readsettings()` in `incs/settings.php`: Das `include` passiert im Funktionsrumpf, ohne die `global`-Zeilen blieben die Variablen dort lokal. Wer die Config-Struktur ändert, muss diese Eigenheit erhalten.
Zugangsdaten gehören **nie** in den Code — `db-backup.php` zeigt das Muster für Skripte außerhalb des Request-Zyklus (Config einlesen, Passwort über eine temporäre Optionsdatei an `mysqldump`, nicht per Kommandozeile). Zugangsdaten gehören **nie** in den Code — `db-backup.php` zeigt das Muster für Skripte außerhalb des Request-Zyklus (Config einlesen, Passwort über eine temporäre Optionsdatei an `mysqldump`, nicht per Kommandozeile).
+28 -2
View File
@@ -12,8 +12,34 @@ if(DIRECTORY_SEPARATOR == "/")
else else
define("CONFIG_PATH", "c:/kundenverwaltung/config/"); define("CONFIG_PATH", "c:/kundenverwaltung/config/");
define("CONFIG_FILE", $dir."_settings.conf"); define("CONFIG_FILE", $dir."_settings.conf");
define("UPLOADS_PATH", "uploads_".$dir."/");
define("SESSION_NAME",strtoupper($dir)."SESS"); /*
* Der Verzeichnisname wählt nur noch aus, welche Konfigurationsdatei
* geladen wird. Der Name der Instanz steht dort als $instanz und bestimmt
* Uploads-Verzeichnis und Session-Name. Fehlt $instanz (ältere Configs,
* Ersteinrichtung), gilt wie bisher der Verzeichnisname.
*/
function instanzAusKonfiguration($datei, $fallback)
{
if(!is_file($datei))
return $fallback;
// Leerzeilen hinter dem schließenden PHP-Tag der Config würden sonst
// ausgegeben und den Session-Cookie verhindern, weil das hier vor
// session_start() läuft.
ob_start();
include($datei);
ob_end_clean();
if(!isset($instanz) || $instanz === "")
return $fallback;
// Landet in Dateipfaden und im Session-Namen (dort nur alphanumerisch zulässig).
if(!is_string($instanz) || !preg_match('/^[A-Za-z0-9]+$/', $instanz))
die("Ungültiger Instanzname in ".$datei.": nur Buchstaben und Ziffern erlaubt.");
return $instanz;
}
define("INSTANZ", instanzAusKonfiguration(CONFIG_PATH.CONFIG_FILE, $dir));
define("UPLOADS_PATH", "uploads_".INSTANZ."/");
define("SESSION_NAME",strtoupper(INSTANZ)."SESS");
define("RIGHTS_KUNDEN", 0); define("RIGHTS_KUNDEN", 0);
define("RIGHTS_TERMINE", 1); define("RIGHTS_TERMINE", 1);
define("RIGHTS_ANGEBOTE", 2); define("RIGHTS_ANGEBOTE", 2);
+12 -8
View File
@@ -1107,13 +1107,19 @@ function writeZahlziel($ktonr,$zahlziel,$gespreis,$wahrung,$rechdate,$kundeid)
function savePDF($pdf,$PDFpath) function savePDF($pdf,$PDFpath)
{ {
$PDFpathpart = dirname($PDFpath)."/"; // TCPDF stellt beim Schreiben file:// voran und braucht deshalb einen
$PDFfilepart = basename($PDFpath,".pdf"); // absoluten Pfad. Aufgelöst wird relativ zum Arbeitsverzeichnis, also in
// der Installation, aus der der Aufruf kommt - früher stand hier fest
// DOCUMENT_ROOT."kunden".
$PDFpathpart = realpath(dirname($PDFpath));
if($PDFpathpart === false || !is_dir($PDFpathpart))
die("PDF-Verzeichnis nicht gefunden: ".dirname($PDFpath));
$PDFpathpart = str_replace("\\", "/", $PDFpathpart)."/";
$handle=opendir($PDFpathpart); $handle=opendir($PDFpathpart);
while ($file = readdir($handle)) while ($file = readdir($handle))
{ {
if (preg_match("=^\.{1,2}$=", $file)) if (preg_match("=^\.=", $file))
continue; # . und .. Dateien ignorieren continue; # ., .. und versteckte Dateien (.gitkeep) ignorieren
elseif(is_file($PDFpathpart.$file)) elseif(is_file($PDFpathpart.$file))
{ #Dateien prüfen, und löschen, wenn älter als $delFile - Minuten { #Dateien prüfen, und löschen, wenn älter als $delFile - Minuten
$lastTime = ceil((time() - filemtime($PDFpathpart.$file))); $lastTime = ceil((time() - filemtime($PDFpathpart.$file)));
@@ -1121,11 +1127,9 @@ function savePDF($pdf,$PDFpath)
unlink($PDFpathpart.$file); unlink($PDFpathpart.$file);
} }
} }
}
while(str_starts_with($PDFpath, ".")){
$PDFpath = substr($PDFpath,1);
} }
$pdf->Output($_SERVER['DOCUMENT_ROOT']."kunden".$PDFpath,"F"); closedir($handle);
$pdf->Output($PDFpathpart.basename($PDFpath),"F");
} }
function outputPDF($pdf) function outputPDF($pdf)
+5
View File
@@ -46,8 +46,13 @@ if($version == -1)
$settings=readsettings(); $settings=readsettings();
// Die Ersteinrichtung schreibt die Config neu und kann die Datenbank leeren.
// Sie läuft ohne Login und darf deshalb nur erreichbar sein, solange es
// noch keine gültige Config gibt.
if($settings != "valid") if($settings != "valid")
$action = "setup"; $action = "setup";
else if($action == "setup")
$action = "";
if($settings == "valid" && $action != "setup") if($settings == "valid" && $action != "setup")
{ {
+5 -1
View File
@@ -1,4 +1,8 @@
<?php <?php
// Nur als Teil von index.php?action=setup gültig und nur, solange es noch
// keine gültige Config gibt (siehe index.php).
if(!defined("CONFIG_PATH") || readsettings() == "valid")
exit;
$err = array(); $err = array();
require_once("update/database-update.php"); require_once("update/database-update.php");
+28 -11
View File
@@ -1,8 +1,26 @@
<?php <?php
// Nur als Teil von index.php?action=setup gültig, nie direkt aufrufbar.
if(!defined("CONFIG_PATH") || readsettings() == "valid")
exit;
require_once("incs/import.php"); require_once("incs/import.php");
set_time_limit(60000); set_time_limit(60000);
$err = 0; $err = 0;
if(!file_put_contents (CONFIG_PATH.CONFIG_FILE,
/*
* Formularwert als PHP-Literal für die Config. var_export() erzeugt einen
* korrekt maskierten String in einfachen Anführungszeichen - früher wurde
* $_POST ungeprüft zwischen doppelte Anführungszeichen gesetzt, womit sich
* beliebiger PHP-Code in die Config schreiben ließ.
*/
function configWert($name, $html = false)
{
$wert = isset($_POST[$name]) && is_string($_POST[$name]) ? $_POST[$name] : "";
if($html)
$wert = htmlspecialchars($wert);
return var_export($wert, true);
}
if(!file_put_contents (CONFIG_PATH.CONFIG_FILE,
'<?php '<?php
global $mysql_user; global $mysql_user;
global $mysql_db; global $mysql_db;
@@ -13,16 +31,15 @@ if(!file_put_contents (CONFIG_PATH.CONFIG_FILE,
global $dbconn; global $dbconn;
global $geodbconn; global $geodbconn;
$instanz = '.var_export(INSTANZ, true).';
$dbconn = 0; $dbconn = 0;
$geodbconn = 0; $geodbconn = 0;
$mysql_user = "'.$_POST["mysqluser"].'"; $mysql_user = '.configWert("mysqluser").';
$mysql_pw = "'.$_POST["mysqlpw"].'"; $mysql_pw = '.configWert("mysqlpw").';
$mysql_db = "'.mysql_escape_string($_POST["mysqldb"]).'"; $mysql_db = '.configWert("mysqldb").';
$mysql_server = "'.$_POST["mysqlserver"].'"; $mysql_server = '.configWert("mysqlserver").';
$mysql_opengeodb = "'.mysql_escape_string($_POST["opengeodb"]).'"; $mysql_opengeodb = '.configWert("opengeodb").';
$firma = "'.htmlspecialchars($_POST["firma"]).'"; $firma = '.configWert("firma", true).';
$mysql_pw = "'.$_POST["mysqlpw"].'";
$mysql_pw = "'.$_POST["mysqlpw"].'";
?>')) ?>'))
{ {
$err= error("Konfiguration konnte nicht geschrieben werden!"); $err= error("Konfiguration konnte nicht geschrieben werden!");
@@ -73,7 +90,7 @@ else
$version = $status[1]; $version = $status[1];
} }
if(!$err) if(!$err)
ok("<font size='+1'><b>Installation komplett!!</b></font><br /> Bitte entfernen Sie nun die Schreibrechte für die Datei /kundenverwaltung/config/settings.conf (chmod 555)"); ok("<font size='+1'><b>Installation komplett!!</b></font><br /> Bitte entfernen Sie nun die Schreibrechte für die Datei ".htmlspecialchars(CONFIG_PATH.CONFIG_FILE)." (chmod 444)");
} }
else else
$err = error("Konfigurationsdatei fehlerhaft!"); $err = error("Konfigurationsdatei fehlerhaft!");