From 73e64b72ac84ae767a25bf70bd1c802f2d38e4bf Mon Sep 17 00:00:00 2001 From: Moirtz Wagner Date: Thu, 17 Sep 2026 08:44:33 +0200 Subject: [PATCH] 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 --- CLAUDE.md | 19 ++++++++++-------- incs/constants.php | 30 ++++++++++++++++++++++++++-- incs/makepdf.php | 20 +++++++++++-------- index.php | 5 +++++ update/firststart.php | 6 +++++- update/rebuild-settings.php | 39 ++++++++++++++++++++++++++----------- 6 files changed, 89 insertions(+), 30 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d9253a3..c9eb02d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 ``` -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 ### 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` | -|---|---| -| `CONFIG_FILE` | `kunden_settings.conf` | -| `UPLOADS_PATH` | `uploads_kunden/` | -| `SESSION_NAME` | `KUNDENSESS` | +| Konstante | abgeleitet aus | Wert bei Installation in `…/kunden` | +|---|---|---| +| `CONFIG_FILE` | Verzeichnisname | `kunden_settings.conf` | +| `INSTANZ` | `$instanz` in der Config | `kunden` | +| `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. @@ -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. -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). diff --git a/incs/constants.php b/incs/constants.php index 9191de3..41f92ca 100644 --- a/incs/constants.php +++ b/incs/constants.php @@ -12,8 +12,34 @@ if(DIRECTORY_SEPARATOR == "/") else define("CONFIG_PATH", "c:/kundenverwaltung/config/"); 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_TERMINE", 1); define("RIGHTS_ANGEBOTE", 2); diff --git a/incs/makepdf.php b/incs/makepdf.php index bcf2299..47793e0 100644 --- a/incs/makepdf.php +++ b/incs/makepdf.php @@ -1107,13 +1107,19 @@ function writeZahlziel($ktonr,$zahlziel,$gespreis,$wahrung,$rechdate,$kundeid) function savePDF($pdf,$PDFpath) { - $PDFpathpart = dirname($PDFpath)."/"; - $PDFfilepart = basename($PDFpath,".pdf"); + // TCPDF stellt beim Schreiben file:// voran und braucht deshalb einen + // 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); while ($file = readdir($handle)) { - if (preg_match("=^\.{1,2}$=", $file)) - continue; # . und .. Dateien ignorieren + if (preg_match("=^\.=", $file)) + continue; # ., .. und versteckte Dateien (.gitkeep) ignorieren elseif(is_file($PDFpathpart.$file)) { #Dateien prüfen, und löschen, wenn älter als $delFile - Minuten $lastTime = ceil((time() - filemtime($PDFpathpart.$file))); @@ -1121,11 +1127,9 @@ function savePDF($pdf,$PDFpath) 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) diff --git a/index.php b/index.php index 897b02b..d6c78fd 100644 --- a/index.php +++ b/index.php @@ -46,8 +46,13 @@ if($version == -1) $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") $action = "setup"; +else if($action == "setup") + $action = ""; if($settings == "valid" && $action != "setup") { diff --git a/update/firststart.php b/update/firststart.php index 51e1fd8..a19d298 100644 --- a/update/firststart.php +++ b/update/firststart.php @@ -1,4 +1,8 @@ -')) { $err= error("Konfiguration konnte nicht geschrieben werden!"); @@ -73,7 +90,7 @@ else $version = $status[1]; } if(!$err) - ok("Installation komplett!!
Bitte entfernen Sie nun die Schreibrechte für die Datei /kundenverwaltung/config/settings.conf (chmod 555)"); + ok("Installation komplett!!
Bitte entfernen Sie nun die Schreibrechte für die Datei ".htmlspecialchars(CONFIG_PATH.CONFIG_FILE)." (chmod 444)"); } else $err = error("Konfigurationsdatei fehlerhaft!");