Setzt die Rechte aus dem vorigen Commit in den Oberflaechen um und
ergaenzt die fehlenden Verwaltungsseiten.
Neu:
- /admin/users - Konten anlegen, Rolle und Sperre setzen, Passwort
zuruecksetzen, Gruppenzuordnung je Konto
- /admin/groups - Gruppen anlegen und umbenennen, Mitglieder samt
Rolle pflegen, zugeordnete Domains einsehen
- /admin/profile - eigenes Konto: Gruppenzugehoerigkeit ansehen und
das Passwort aendern. Das fehlte bisher vollstaendig, auch fuer
den einen Administrator.
Domains sind jetzt Administratorsache, weil dort die Gruppe zugewiesen
wird. Kurzlinks, Statistik und QR-Codes richten sich nach der Gruppe:
ein fremder Kurzlink antwortet mit 404 (seine Existenz geht den
Aufrufer nichts an), ein sichtbarer ohne Schreibrecht mit 403.
Geschlossene Luecke: /admin/uploads/{file} pruefte bisher nur, ob
ueberhaupt jemand angemeldet ist. Da alle Logos in einem gemeinsamen
Verzeichnis liegen, war der Dateiname die einzige Huerde. Ausgeliefert
wird jetzt nur noch, was laut Datenbank zu einem Kurzlink aus einer
Gruppe des Aufrufers gehoert.
Die Navigation blendet aus, wofuer die Rechte fehlen - das ist
Bedienkomfort, kein Zugriffsschutz: die Routen pruefen unabhaengig
davon noch einmal.
Der Setup-Assistent legt das erste Konto als Administrator an und
traegt es gleich als Editor der Gruppe "Standard" ein. Ohne das
koennte es zwar verwalten, aber selbst keine Kurzlinks anlegen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Loest das eine fest verdrahtete Admin-Konto durch beliebig viele
Konten ab, die in Gruppen organisiert sind.
Die Zugriffskette lautet Benutzer -> Gruppe -> Domain -> Kurzlink:
eine Domain gehoert zu genau einer Gruppe, ein Kurzlink haengt an
seiner Domain. Wer Mitglied einer Gruppe ist, sieht damit alle
Kurzlinks aller Domains dieser Gruppe. Ein Kurzlink hat bewusst
keinen persoenlichen Eigentuemer - sonst wuerde das Loeschen eines
Kontos gedruckte QR-Codes unbrauchbar machen.
Zwei Rollenebenen: global "admin" (verwaltet Benutzer, Gruppen und
Domains, sieht alles) bzw. "user", innerhalb einer Gruppe "editor"
(anlegen und aendern) bzw. "viewer" (nur lesen).
AccessPolicy beantwortet je Request, welche Domains sichtbar bzw.
beschreibbar sind; die Repositories nehmen diese Liste als Filter.
Dabei ist die Unterscheidung wesentlich: null heisst "keine
Einschraenkung" (Administrator), ein leeres Array heisst "nichts" -
und darf gerade nicht zu einer Abfrage ohne Bedingung werden.
Der Zugang aus der .env (ADMIN_USER / ADMIN_PASSWORD_HASH) bleibt
gueltig, steht aber ab jetzt als Notfallzugang neben den regulaeren
Konten, statt sie zu ueberstimmen. Er hat Administratorrechte und
existiert nur fuer den Fall, dass das letzte Administratorkonto
gesperrt wurde oder sein Passwort verloren ging.
Migration 003 uebernimmt das Bestandskonto aus der settings-Tabelle
als Benutzer Nr. 1 mit unveraendertem Hash, legt die Gruppe
"Standard" an, ordnet ihr alle vorhandenen Domains zu und raeumt
die beiden alten settings-Schluessel auf.
Nebenbei geschlossen: das Rate-Limiting am Login zaehlte nur nach
IP-Adresse. Es zaehlt jetzt auch je Benutzername, sonst reicht ein
Wechsel der Adresse, um alle Konten der Reihe nach durchzuprobieren.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Wer bereits gedruckte QR-Codes aus einem anderen System uebernimmt,
kann deren Adressen nicht mehr aendern. Sehen die aus wie
"/g/1AEJ0X90ZmF", waren sie hier bisher nicht abbildbar: der
Normalisierer machte daraus "g-1aej0x90zmf" - Schraegstrich zu
Bindestrich, alles klein. Der Kurzlink zeigte damit ins Leere.
Ein Slug darf jetzt mehrere durch "/" getrennte Abschnitte haben
(hoechstens acht, je 64 Zeichen) und behaelt die eingegebene
Schreibweise. Erlaubt sind Buchstaben, Ziffern, Bindestrich,
Unterstrich und Punkt; jeder Abschnitt muss mit einem Buchstaben oder
einer Ziffer beginnen und enden. Umlaute und Leerzeichen werden
weiterhin umgeschrieben. Wird versehentlich eine komplette URL
eingefuegt, bleibt davon nur der Pfad uebrig.
Damit die Verwaltung nicht verdeckt werden kann, greift die Liste
reservierter Namen jetzt auf den ERSTEN Abschnitt: "admin/geheim" wird
ebenso abgelehnt wie "admin". Die Route der Weiterleitung steht
weiterhin ganz am Ende, alle Verwaltungsrouten gewinnen davor.
Bei der Eindeutigkeit gilt bewusst das Gegenteil: sie wird OHNE
Ruecksicht auf Gross- und Kleinschreibung geprueft. "Messe" und "messe"
koennen auf derselben Domain nicht nebeneinander bestehen - an einer
gedruckten Adresse waere sonst nicht mehr erkennbar, welcher Link
gemeint ist. Ausserdem wuerde MySQL sie je nach Kollation ohnehin als
gleich ansehen, SQLite nicht; so verhalten sich beide Treiber wieder
gleich.
Beim Aufruf wird zuerst exakt gesucht und nur bei einem Fehlschlag ohne
Ruecksicht auf die Schreibweise - abgetippte Codes stimmen nicht immer.
Weil die Eindeutigkeit oben schon ohne Ruecksicht geprueft wurde, kann
dieser zweite Versuch nie mehrdeutig sein.
Geprueft in beiden Betriebsarten: im Container und in der
Shared-Hosting-Variante, bei der die Root-.htaccess nach public/
umleitet - dort sind mehrteilige Pfade der wahrscheinlichste
Stolperstein.
Auf einem Hetzner-Webhosting mit PHP 8.5 brach die Anwendung beim ersten
Aufruf ab. Ausloeser war session.sid_length: seit PHP 8.4 als veraltet
gemarkt. Die Meldung wurde ausgegeben, damit waren Header raus - und
danach liess sich weder eine ini-Einstellung aendern noch eine Session
starten. Aus einer Randnotiz wurde so ein Totalausfall.
Behoben auf drei Ebenen:
1. Die veralteten Aufrufe sind weg. session.sid_length und
session.sid_bits_per_character werden nicht mehr gesetzt; die
Voreinstellungen liefern mit 128 Bit ohnehin genug Entropie.
Ebenso entfallen curl_close(), imagedestroy() und finfo_close() -
seit PHP 8.0 bzw. 8.5 wirkungslos und als veraltet gemeldet.
2. public/index.php puffert die Ausgabe von der ersten Zeile an. Viele
Hoster erzwingen display_errors und lassen sich per ini_set() nicht
davon abbringen. Gepuffert laesst sich Fremdausgabe vor dem Senden
verwerfen und ins Fehlerprotokoll umleiten, statt die Antwort zu
zerstoeren. Das schuetzt auch Binaerdownloads: eine einzelne
Meldung haette bisher jedes PNG beschaedigt. Session::start()
meldet ausserdem klar, wenn doch schon Ausgaben raus sind, statt
eine Kaskade aus Warnungen zu erzeugen.
3. endroid/qr-code von 5.1 auf 6.0 angehoben. Version 5 markiert
Parameter implizit als nullable, was PHP 8.4 bemaengelt - bei jedem
erzeugten QR-Code. Version 6.1 waere ebenfalls verfuegbar, verlangt
aber PHP 8.4+ und wuerde Hoster mit 8.2 oder 8.3 ausschliessen;
6.0 laeuft ab 8.1.
Getestet wird jetzt gegen PHP 8.3, 8.4 und 8.5, jeweils mit
display_errors=On und error_reporting=E_ALL, damit Deprecations
sichtbar werden statt still im Log zu landen. Alle 232 Tests laufen auf
allen drei Versionen ohne eine einzige Meldung durch. Zusaetzlich ist
das Release-Paket in einer PHP-8.5-Umgebung mit fremdem Webroot
geprueft: Ersteinrichtung, Kurzlinks, Weiterleitung, QR-Codes und
Statistik, ohne Streuausgabe in Seiten oder Downloads.
Auf Shared Hosting gibt es keine Kommandozeile - ohne Anzeige laesst
sich dort nicht feststellen, ob etwa gd fehlt, die PHP-Version zu alt
ist oder data/ nicht beschreibbar ist. Der Setup-Assistent prueft das
jetzt und listet es auf; im Dashboard meldet sich die Pruefung spaeter
nur noch, wenn etwas nicht stimmt. Fehlende Pflichtbausteine sind
Fehler, fehlende Kuer (intl, imagick, cURL) nur Hinweise mit Angabe,
was dadurch entfaellt.
build-release.sh erzeugt das Upload-Paket: Abhaengigkeiten ohne
Entwicklungspakete, ohne Tests und Docker-Dateien. Dabei faellt eine
16 MB grosse Schriftart aus endroid/qr-code weg, die ausschliesslich
fuer Beschriftungen unter dem QR-Code gebraucht wird - die erzeugt
diese Anwendung nicht. Das Paket schrumpft damit von 18 MB auf 1 MB.
Sollte spaeter doch eine Beschriftung dazukommen, erkennt das Skript
das und behaelt die Datei.
Die Variante ohne eigenen Webroot (alles liegt im Domainverzeichnis,
die .htaccess im Stammverzeichnis leitet nach public/ um) war bisher
gebaut, aber nie erprobt. Sie ist jetzt gegen eine Hoster-Simulation
getestet: Ersteinrichtung, Kurzlinks, Weiterleitung, QR-Codes,
Statistik und die Sperren fuer src/, vendor/, data/ und .env.
Setzt CSP, X-Frame-Options, Referrer-Policy, COOP/CORP und
Permissions-Policy; ueber HTTPS zusaetzlich HSTS und
upgrade-insecure-requests. Letzteres bewusst nur bei HTTPS, weil es
eine reine HTTP-Installation sonst unbrauchbar macht.
X-Forwarded-For und X-Forwarded-Proto werden nur hinter einem in
TRUSTED_PROXIES eingetragenen Proxy ausgewertet - sonst liesse sich die
Login-Sperre mit einem erfundenen Header umgehen. Ebenso fliesst der
Host-Header nur geprueft in erzeugte URLs ein.
Eine reine DNS-Pruefung sagt nichts darueber aus, ob der Webserver die
Domain auch dem richtigen Verzeichnis zuordnet - genau das ist der
haeufigste Fehler. Die Pruefung ruft die Domain deshalb von aussen auf
einen signierten Endpunkt auf. Kommt der Aufruf mit gueltigem Token an,
ist die ganze Kette bewiesen, denn nur diese Installation kann ein
solches Token erzeugen.
Erkannt und benannt werden: fehlender DNS-Eintrag, Domain-Weiterleitung
des Registrars, falsches Verzeichnis, ungueltiges Zertifikat.
Link-lokale Adressen werden nicht abgerufen - dort liegen bei Cloud-
Anbietern die Metadaten-Dienste.
short.example.de/messe und qr.example.de/messe koennen auf
unterschiedliche Ziele zeigen. Die Domain ergibt sich aus dem
Host-Header; Slugs sind nur noch innerhalb ihrer Domain eindeutig.
Bestandslinks tragen nach der Migration den Platzhalter 0 und werden
beim ersten Zugriff der aus APP_BASE_URL erzeugten Standarddomain
zugeordnet. Ein Sentinel statt NULL, weil SQLite NULL-Werte in einem
UNIQUE-Index als jeweils verschieden behandelt - die Eindeutigkeit
waere sonst nicht durchgesetzt.
Unbekannte Hosts landen auf der Standarddomain, damit bestehende Links
auch beim Aufruf ueber die IP oder hinter einem Proxy funktionieren.
Erzeugt PNG und SVG ueber endroid/qr-code. Die eingestellte Groesse gilt
fuer das fertige Bild inklusive Quiet Zone; der Rand wird in Modulen
angegeben und ueber die Matrix in Pixel umgerechnet.
Kontrastwarnung nach WCAG, zusaetzlich Hinweis auf invertierte Codes -
die erkennen viele Scanner nicht. Mit Logo wird die Fehlerkorrektur
zwingend auf Level H gesetzt.
Uploads werden gegen GD geprueft (getimagesize allein akzeptiert auch
beschaedigte Dateien), in der Groesse und den Abmessungen begrenzt und
ausserhalb des Webroots abgelegt. SVGs werden vor dem Speichern von
Skripten, Event-Handlern, Entities und externen Referenzen befreit.
Slugs aus 34 Zeichen (0 und 1 fehlen wegen Verwechslung mit o und l),
kollisionssicher mit wachsender Laenge. Wunsch-Slugs werden normalisiert
und gegen eine Liste reservierter Pfade geprueft.
Die URL-Validierung kommt bewusst ohne FILTER_VALIDATE_URL aus: dieses
verwirft jedes Nicht-ASCII-Zeichen und lehnt damit Umlaut-Domains und
Umlaute im Pfad ab. Erlaubt sind nur http und https, darin aber auch
IDN, IPv6 und interne Hostnamen ohne Punkt. Gespeichert wird die
lesbare Schreibweise; Punycode entsteht erst im Location-Header, weil
HTTP-Header laut RFC 7230 ASCII sein muessen.
Die Statistik speichert keine IP-Adressen (optional nur einen taeglich
rotierenden Hash) und laeuft nach dem Ausliefern der Weiterleitung.
Session-Login mit genau einem Admin-Konto, Passwort als bcrypt-Hash.
Sessions mit HttpOnly, SameSite=Lax, strict mode, ID-Wechsel beim Login,
Inaktivitaets- und absoluter Zeitgrenze; ueber HTTPS wird das Cookie
automatisch auf secure gesetzt. Ein Passwortwechsel entwertet
bestehende Sitzungen.
Das Rate-Limiting zaehlt Fehlversuche pro IP-Hash - die IP selbst wird
nie gespeichert.
SetupToken schliesst das Zeitfenster zwischen Upload und erstem Aufruf:
ohne das Einmal-Token aus dem Datenverzeichnis laesst sich kein Konto
anlegen.
Der Router kennt Platzhalter fuer ein Pfadsegment und liefert 405, wenn
nur die Methode nicht passt. Response kann Arbeit fuer die Zeit NACH dem
Ausliefern vormerken (afterSend) - so verzoegert das Protokollieren
eines Klicks die Weiterleitung nicht.
BasePath deckt drei Installationsarten ab: Webroot auf public/,
Unterverzeichnis und Shared Hosting ohne eigenen Webroot.
SQLite als Standard (keine Einrichtung noetig, WAL aktiviert), optional
MySQL/MariaDB. Beide Treiber nutzen dieselben vorbereiteten Statements.
Die Migrationen laufen automatisch beim ersten Request; jede Migration
liefert das SQL je Treiber. Damit entfaellt ein manueller Schritt beim
Deployment ueber FTP.
data/ enthaelt eine eigene .htaccess als zweite Absicherung, falls das
Verzeichnis beim Hoster doch einmal im Webroot landet.