- incs/db.php: db_verbinden() setzt utf8mb4, strikten sql_mode und schaltet die mysqli-Exceptions von PHP 8.1+ ab, damit die bestehenden "or error(mysqli_error())"-Pruefungen weiter greifen. - db_abfrage/db_zeilen/db_zeile/db_wert mit ?-Platzhaltern (mysqli_execute_query, PHP 8.2). - Alle neun kopierten mysqli_connect/select_db-Stellen nutzen jetzt db_verbinden(). Nur zusammen mit der auf utf8mb4 migrierten Datenbank einsetzbar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
8.9 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 DB-Zugangsdaten stehen dort auch $instanz (siehe oben), die SMTP-Zugänge je Firma ($smtp_konten, Schlüssel "*" als Rückfall, genutzt von mail_attachment()), der Zugang zur Statistik-Datenbank für stats.php ($stats_mysql_*) und die Wetter-API-Schlüssel ($openweathermap_appid, $aeris_client_*). 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
Umbau läuft (Branch db-umbau): Verbindungen entstehen nur noch über db_verbinden() aus incs/db.php. Die Funktion setzt utf8mb4, den strikten sql_mode und schaltet die mysqli-Exceptions ab. Neue und umgebaute Abfragen nutzen db_abfrage(), db_zeilen(), db_zeile() und db_wert() mit ?-Platzhaltern statt String-Verkettung. Ziel-Schema ist das der Instanz kunden in utf8mb4; die Altdaten sind teils doppelt kodiert und werden per Migration repariert. Dieser Stand darf nur zusammen mit der migrierten Datenbank live gehen.
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.