Beim Test der Oberflaeche im Browser gefunden:
- addrechnung.tpl fragte die gespeicherte Anrede als $rechnung.anr1 ab
(die Daten heissen anrede1) und gab sie ohne Klammern aus. Beim
Bearbeiten von Angeboten/Briefen erschien deshalb immer die
Standard-Anrede, eine geaenderte ging beim erneuten Speichern verloren.
- |escape innerhalb von {if}-Bedingungen wieder entfernt (dort ohne
Wirkung, gehoert nicht dorthin).
- UTF-8-BOM aus sechs Dateien entfernt; makepdf.php gab es auf jeder
Rechnungsseite vor dem HTML aus.
- makeImageFromPDF() loeschte bei der Aufraeumrunde auch .gitkeep;
Pfade fuer convert per escapeshellarg().
- constants.php: Instanzname auch unter Windows (getcwd() mit
Backslashes) korrekt; KV_CONFIG_PATH als Config-Pfad fuer lokale Tests.
Im Browser getestet (lokaler PHP-Webserver, migrierte Testdatenbank):
Login mit Sonderzeichen-Passwort, Entwurf automatisch speichern und
laden, Rechnung mit Umlauten/Anfuehrungszeichen/HTML im Artikelnamen und
Komma-Betraegen speichern, Anzeige als PDF (Original und Kopie),
Kundensuche nach Dokumenten mit Umlaut, Artikelnamen escaped, Storno,
Kopie auf andere Adresse, Umschlag, Angebot mit eigener Anrede anlegen und
wieder bearbeiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
11 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.
Lokal testen (auch unter Windows): Die Umgebungsvariable KV_CONFIG_PATH ersetzt CONFIG_PATH. Die Config-Datei heißt nach dem Verzeichnisnamen des Checkouts, also z.B. Kundenverwaltung_settings.conf; $lokale_netze muss 127.0.0.1/::1 enthalten, sonst gibt es kein Passwortfeld. Start im Repo-Verzeichnis mit php -S 127.0.0.1:8765. Die PDF-Vorschau als Bild (makeImageFromPDF()) braucht ImageMagick unter /opt/bin/convert und funktioniert lokal nicht.
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.
Regeln für den Umbau:
- Ergebnisse sind Strings wie früher bei
mysqli_query():db_zeilen(),db_zeile(),db_wert()unddb_zuordnung()wandeln die nativen int/float-Werte der Prepared Statements um. Werdb_abfrage()selbst ausliest, bekommt native Typen. - Formularwerte über
formularText(),formularZahl()(Komma als Dezimaltrenner, leer wird 0) undformularGanzzahl()ausincs/rechnung.phpholen – der strikte Modus lehnt''in Zahlenspalten ab. - Änderungsprotokoll:
saveLog()bekommt den Text ausdb_sql_text(). Die Tabellelogist reine Dokumentation, nichts daraus wird ausgeführt. - Mehrschrittiges Speichern in eine Transaktion (
mysqli_begin_transaction/commit/rollback), damit bei Fehlern keine halben Datensätze bleiben. - Tabellen- und Spaltennamen lassen sich nicht binden; nur feste Werte aus dem Code einsetzen,
LIMITals(int).
Umgebaut: Login/Passkeys, Benutzereinstellungen, Änderungsprotokoll, Rechnungsmodul (incs/rechnung.php, sites/rechnungadd.php, sites/umschlagshow.php, ajax/autosave.php, ajax/preview.php, Rechnungssuche). Offen: Kunden/Adressen (incs/saveNchange.php), Termine, Projekte, Zeiten, Benutzerverwaltung, übrige Suchen, incs/makepdf.php, update/.
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.
- Passwort-Login nur aus dem lokalen Netz, von außen nur per Passkey. „Lokal“ entscheidet
isLocal()inincs/authHelper.phpanhand von$lokale_netze(CIDR-Liste) aus der externen Config. Anfragen mit Proxy-Headern (X-Forwarded-For,Forwarded…) gelten nie als lokal. Webserver und MariaDB laufen in Docker auf dem NAS: Das Docker-Netz gehört nicht in die Liste, sonst gilt jede über den Docker- oder Reverse-Proxy eingehende Anfrage als lokal. - Passkeys anlegen geht nur über eine Einmal-Freigabe: Ein Superadmin erzeugt in
addKey.phpeinenaddKey,sites/userkeys.phplöst ihn ein und legt$_SESSION["passkey_freigabe"](Benutzer-id, 5 Minuten) ab.authServer.phpakzeptiertgetCreateArgs/processCreatenur mit dieser Freigabe und nimmt die Benutzer-id von dort, nie aus der URL.
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.