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>
8.1 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Kundenverwaltung für ein Ingenieurbüro: Kunden, Termine, Projekte, Angebote, Lieferscheine, Rechnungen, Zeiterfassung. Gewachsene PHP-Anwendung (seit ~2011) mit Smarty-Templates und MariaDB/MySQL. Domänenbegriffe, Bezeichner und UI-Texte sind durchgehend deutsch — das bitte beibehalten.
Tooling
Es gibt kein Composer, npm, Build, Linter oder Testsuite. Abhängigkeiten liegen als Kopie in incs/ und werden dort direkt gepflegt. Ein Update einer dieser Bibliotheken ist immer ein manueller Eingriff, kein Paketmanager-Aufruf.
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). 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. Der wählt aber nur noch die Konfigurationsdatei aus. Der eigentliche Instanzname steht in der Config als $instanz und wird zur Konstante INSTANZ:
| 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.
Konsequenz für alles, was per CLI/Cron läuft: Das Arbeitsverzeichnis muss das Installationsverzeichnis sein, sonst wird die falsche (oder keine) Konfiguration geladen.
cd /home/intranet/kunden && php db-backup.php
constants.php kompensiert dabei nur die Aufrufe aus ajax/ und ajax/auto/, indem es ein bzw. drei Ebenen hochgeht.
2. Zugangsdaten liegen außerhalb des Repos
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.
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).
Request-Ablauf
index.php ist der einzige Einstieg für die Oberfläche:
- Konstanten, Session, Konfiguration, DB-Verbindung
- Schema-Migration (siehe unten)
head.tpl- bei gültiger Session:
checkRightsNInclude()ausincs/funcs.php— einswitchüber$_GET["action"], das Rechte prüft und den Pfad insites/zurückgibt, der dann inkludiert wird foot.tpl
Eine neue Seite anzulegen heißt: Datei in sites/ plus ein case in checkRightsNInclude(). Ohne den case ist die Seite nicht erreichbar.
Die Endpunkte in ajax/ gehen nicht über index.php. Sie bootstrappen selbst über incs/connectmysql.php (Konstanten, Session, Config, DB, Escape-Wrapper) und rendern eigene Fragmente nach ajax/templates_c/.
Rechtesystem
$_SESSION["rights"] ist ein String, keine Zahl. Die Zeichenposition benennt den Bereich, der Zeichenwert ist eine Bitmaske:
- Position:
RIGHTS_KUNDEN=0,RIGHTS_TERMINE=1,RIGHTS_ANGEBOTE=2,RIGHTS_LIEFER=3,RIGHTS_RECHNUNGEN=4,RIGHTS_SUPERADMIN=5 - Wert:
RIGHTS_LOOK=1,RIGHTS_CHANGE=2,RIGHTS_ADD=4
Geprüft wird immer nach diesem Muster:
if(substr($_SESSION["rights"], RIGHTS_KUNDEN, 1) & RIGHTS_LOOK)
Schema-Migrationen laufen bei jedem Seitenaufruf
Es gibt keine SQL-Migrationsdateien. Der Stand steht in update/thisversion/currentversion.txt (dreistellig, aktuell 010), die Zielversion ist der erste Eintrag in $versions in update/versions.php. index.php ruft bei jedem Request checknewversion() und manageupdate() aus incs/update.php; liegt die Zielversion höher, arbeitet update_tables() in update/database-update.php sich in einem switch von Version zu Version hoch und schreibt die neue Nummer zurück.
Eine Schemaänderung besteht also aus: neuer Eintrag vorn in update/versions.php und passender case in update_tables(). update/thisversion/ muss für den Webserver schreibbar sein.
Datenbankzugriff
Prozedurales mysqli. $dbconn wird zusätzlich in $_SESSION["dbconn"] abgelegt, damit incs/mysql_ecape_wrapper.php eine globale Funktion mysql_escape_string() als Shim über diese Verbindung bereitstellen kann — ein Überbleibsel der mysql_*-Ära, das in altem Code noch aufgerufen wird.
SQL wird durchgehend per String-Verkettung gebaut. Escaping ist Handarbeit über mysqli_real_escape_string($dbconn, …) und muss bei jeder Änderung mitgedacht werden.
Fallstricke
smarty/enthält Smarty 4.5.6 — der schlichte Verzeichnisname sagt nichts über die Version.- PHPMailer liegt in zwei Versionen parallel und beide sind aktiv:
index.phplädtincs/PHPMailer_v6für den Rechnungsversand,sites/projectadd.phpdagegenincs/PHPMailer_v5.1. Keine der beiden ist entfernbar, ohne den jeweiligen Aufrufer umzustellen. incs/font/gehört zu pChart, nicht zu TCPDF. Die TTFs dort werden vonstats.phpfür die Diagramme geladen; TCPDF bringt seine Schriften inincs/tcpdf/fonts/selbst mit.index.phpsetzterror_reporting(E_ERROR | E_PARSE). Warnungen und Notices sind unterdrückt, und der Code verlässt sich darauf — nicht ohne Not hochdrehen, sonst überflutet es die Ausgabe.templates_c/,ajax/templates_c/undtemp/müssen für den Webserver schreibbar sein. Sie sind git-ignoriert und nur über.gitkeepim Repo vorhanden.uploads_*/{notes,signs}enthalten personenbezogene Kundendaten (Rechnungen, Scans, Unterschriften) und sind bewusst git-ignoriert. Nie committen.- Zeilenenden werden über
.gitattributesauf LF normalisiert.
Authentifizierung
Klassischer Login plus WebAuthn/FIDO2. Server-Seite: authServer.php mit incs/WebAuthn (Bibliothek von Lukas Buchs) und incs/authHelper.php; Client-Seite js/auth.js. Schlüsselverwaltung über addKey.php und sites/userkeys.php.
Historie
Am 2026-08-28 wurden tote Pfade entfernt: smarty3/ (ungenutztes Smarty 3.1.6), incs/tcpdf_old/, incs/fpdf.php samt FPDF-Schriftmetriken und incs/font/makefont/, sites/makepdf_OLD.php, incs/users_old.php und oldindex.php. Auf keines davon verwies noch Code.
Das Repository wurde am 2026-08-28 neu initialisiert. Die mitgelieferte Historie von 2014 war irreparabel beschädigt und wurde verworfen — der erste Commit ist der Ausgangsstand, nicht der Projektbeginn.