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.
765 lines
36 KiB
Markdown
765 lines
36 KiB
Markdown
# Kurzlinks – URL-Shortener mit QR-Code-Generator
|
||
|
||
Ein schlanker, selbst gehosteter URL-Shortener mit eingebautem QR-Code-Generator.
|
||
Gedacht als leichtgewichtige Alternative zu Shlink: kein Framework, kein Build-Schritt,
|
||
keine Node-Toolchain – nur PHP, eine Datenbank und ein Webserver.
|
||
|
||
**Alles liegt hinter einem Login.** Öffentlich erreichbar sind ausschließlich die
|
||
Weiterleitungen `/{slug}` – Übersicht, Verwaltung, Statistiken und QR-Codes verlangen
|
||
Benutzername und Passwort.
|
||
|
||
---
|
||
|
||
## Inhalt
|
||
|
||
- [Funktionsumfang](#funktionsumfang)
|
||
- [Systemvoraussetzungen](#systemvoraussetzungen)
|
||
- [Deployment A: Shared Webhosting per FTP](#deployment-a-shared-webhosting-per-ftp)
|
||
- [Deployment B: Docker](#deployment-b-docker)
|
||
- [Konfigurationsreferenz](#konfigurationsreferenz)
|
||
- [Bedienung](#bedienung)
|
||
- [Projektstruktur](#projektstruktur)
|
||
- [Sicherheit und Datenschutz](#sicherheit-und-datenschutz)
|
||
- [Tests](#tests)
|
||
- [Fehlerbehebung](#fehlerbehebung)
|
||
|
||
---
|
||
|
||
## Funktionsumfang
|
||
|
||
**Kurzlinks**
|
||
- **Mehrere Domains** auf einer Installation: `short.example.de/messe` und `qr.example.de/messe`
|
||
können auf verschiedene Ziele zeigen. Slugs sind je Domain eindeutig. Ein eingebauter
|
||
**Domain-Check** prüft auf Knopfdruck, ob DNS, Webserver-Zuordnung und Zertifikat stimmen.
|
||
- Lange URL eintragen → Kurzlink erzeugen
|
||
- Zufälliger Slug (kollisionssicher, 7 Zeichen, wächst bei Kollisionen automatisch)
|
||
oder eigener Wunsch-Slug wie `deine-domain.de/messe2026`
|
||
- Mehrteilige Slugs mit Schrägstrich (`g/1AEJ0X90ZmF`) und erhaltener Groß-/Kleinschreibung –
|
||
damit lassen sich bereits gedruckte QR-Codes aus einem anderen System übernehmen
|
||
- URL-Validierung: ausschließlich `http` und `https` – innerhalb davon aber großzügig
|
||
(Umlaut-Domains, Umlaute in Pfad/Query, IPv6, interne Hostnamen ohne Punkt)
|
||
- Weiterleitung per HTTP 301/302/307/308 (konfigurierbar)
|
||
- Freundliche 404-Seite für unbekannte oder deaktivierte Slugs
|
||
- Liste mit Suche und Sortierung; bearbeiten, aktivieren/deaktivieren, löschen
|
||
|
||
**Statistiken**
|
||
- Jeder Aufruf wird protokolliert: Zeitpunkt, Referrer, Gerätetyp (Desktop/Mobil/Tablet/Bot),
|
||
Browser und Plattform
|
||
- **Keine IP-Adressen** – standardmäßig wird gar nichts gespeichert, optional nur ein
|
||
tageweise rotierender, gesalzener Hash
|
||
- Detailseite je Link: Gesamtklicks, Liniendiagramm über 7/30/90/365 Tage,
|
||
Top-Referrer, Aufteilung nach Gerät, Browser und Plattform, letzte Aufrufe
|
||
- Die Protokollierung läuft **nach** dem Ausliefern der Weiterleitung
|
||
(`fastcgi_finish_request`) und verzögert den Redirect daher nicht spürbar
|
||
|
||
**QR-Codes** (erzeugt mit [`endroid/qr-code`](https://github.com/endroid/qr-code))
|
||
- Vorder- und Hintergrundfarbe per Farbwähler oder Hex-Eingabe, mit Farbvoreinstellungen
|
||
- Automatische Kontrastwarnung (WCAG-Verhältnis < 4:1 sowie invertierte Codes)
|
||
- Logo in der Mitte (PNG, JPG, SVG) – Fehlerkorrektur wird dabei automatisch auf **H** gesetzt
|
||
- Größe 256/512/1024/2048 px, Randbreite (Quiet Zone) 0–10 Module
|
||
- Download als **PNG** (Web) und **SVG** (vektorbasiert für den Druck)
|
||
- Live-Vorschau im Browser beim Anpassen
|
||
|
||
**Sicherheit**
|
||
- Setup-Assistent beim ersten Start oder feste Zugangsdaten in der `.env`
|
||
- Session-Login mit bcrypt-Hash, CSRF-Token für alle Formulare, Rate-Limiting am Login
|
||
- Vorbereitete Statements überall, Output-Escaping, Security-Header inkl. CSP
|
||
|
||
---
|
||
|
||
## Systemvoraussetzungen
|
||
|
||
| | |
|
||
|---|---|
|
||
| PHP | 8.2 oder neuer – getestet mit **8.3, 8.4 und 8.5** |
|
||
| Erweiterungen | `pdo` + `pdo_sqlite` (Standard) bzw. `pdo_mysql`, `gd`, `json`, `mbstring`, `fileinfo` |
|
||
| Optional | `imagick` – nur nötig, wenn SVG-Logos auch im **PNG**-Download erscheinen sollen |
|
||
| Optional | `intl` – für Umlaut-Domains nach UTS-46; fehlt sie, greift ein eingebauter Punycode-Fallback |
|
||
| Datenbank | SQLite (eingebaut) oder MySQL/MariaDB 5.7+ |
|
||
| Webserver | Apache mit `mod_rewrite` (`.htaccess` liegt bei) oder Nginx |
|
||
|
||
---
|
||
|
||
## Deployment A: Shared Webhosting per FTP
|
||
|
||
Es wird **keine Shell** benötigt. Der `vendor/`-Ordner ist im Release-Paket enthalten.
|
||
|
||
### Upload-Paket erzeugen
|
||
|
||
```bash
|
||
sh build-release.sh
|
||
```
|
||
|
||
Ohne lokales PHP über das Anwendungs-Image (der schlanke `composer:2` hat kein `gd`
|
||
und bricht an der Plattformprüfung ab):
|
||
|
||
```bash
|
||
docker compose build
|
||
docker run --rm -v "$PWD:/app" -w /app urlshort-qr-gen:latest sh build-release.sh
|
||
```
|
||
|
||
Ergebnis ist `build/urlshort-qr-gen-release/` – rund **1 MB, 217 Dateien**. Das Skript
|
||
installiert die Abhängigkeiten ohne Entwicklungspakete und entfernt eine 16 MB große
|
||
Schriftart aus `endroid/qr-code`, die nur für Beschriftungen *unter* dem QR-Code
|
||
gebraucht wird (die diese Anwendung nicht erzeugt). Sollte sich das einmal ändern,
|
||
merkt das Skript es und behält die Datei.
|
||
|
||
### Was hochgeladen wird
|
||
|
||
| | |
|
||
|---|---|
|
||
| `public/` | Webroot: `index.php`, `.htaccess`, `assets/` |
|
||
| `src/` | Anwendungscode |
|
||
| `templates/` | Seiten |
|
||
| `migrations/` | Datenbankschema |
|
||
| `vendor/` | Abhängigkeiten (ohne Entwicklungspakete) |
|
||
| `data/` | leer, muss **beschreibbar** sein – hier entstehen Datenbank und Logos |
|
||
| `.htaccess` | nur für Variante A2 nötig (siehe unten) |
|
||
| `.env` | selbst anlegen, Vorlage ist `.env.example` |
|
||
|
||
**Nicht** hochgeladen werden `tests/`, `docker/`, `Dockerfile`, `docker-compose.yml`,
|
||
`phpunit.xml`, `build-release.sh` und `.git/` – das Release-Paket enthält sie erst gar nicht.
|
||
|
||
### Variante A1 – Webroot kann auf `public/` gezeigt werden (empfohlen)
|
||
|
||
Viele Hoster erlauben es, das Dokumentenstammverzeichnis einer (Sub-)Domain frei zu wählen.
|
||
|
||
1. Kompletten Projektordner per FTP/SFTP hochladen, z. B. nach `/kurzlinks`.
|
||
2. Im Hosting-Panel den Webroot der Domain auf `/kurzlinks/public` setzen.
|
||
3. Schreibrechte für den Ordner `data/` setzen (meist `755`, bei manchen Hostern `775`).
|
||
4. `.env.example` nach `.env` kopieren und mindestens `APP_BASE_URL` eintragen:
|
||
|
||
```dotenv
|
||
APP_BASE_URL=https://kurz.deine-domain.de
|
||
```
|
||
|
||
5. Domain im Browser aufrufen → der **Setup-Assistent** legt das Admin-Konto an.
|
||
|
||
Bei dieser Variante liegen `src/`, `templates/`, `data/` und `vendor/` außerhalb
|
||
des Webroots und sind damit grundsätzlich nicht abrufbar.
|
||
|
||
### Variante A2 – nur ein fester Webroot (z. B. `httpdocs`)
|
||
|
||
Wenn der Webroot nicht verschoben werden kann:
|
||
|
||
1. **Den gesamten Projektinhalt** (inklusive `public/`, `src/`, `vendor/`, `data/`,
|
||
der beiliegenden `.htaccess` im Stammverzeichnis und der `.env`) direkt in den
|
||
Webroot hochladen.
|
||
2. Schreibrechte für `data/` setzen.
|
||
3. `.env` anlegen wie oben.
|
||
4. Domain aufrufen → Setup-Assistent.
|
||
|
||
Die mitgelieferte `.htaccess` im Stammverzeichnis leitet alle Anfragen intern nach
|
||
`public/` weiter und sperrt gleichzeitig `src/`, `templates/`, `migrations/`,
|
||
`vendor/`, `data/` sowie `.env`, `composer.json` und Markdown-Dateien.
|
||
Zusätzlich liegt in `data/` eine eigene `.htaccess` als zweite Absicherung.
|
||
|
||
> **Bitte prüfen:** Rufe nach dem Upload einmal `https://deine-domain.de/.env` und
|
||
> `https://deine-domain.de/data/database.sqlite` auf. Beides muss `403` oder `404`
|
||
> liefern. Falls nicht, unterstützt der Hoster `AllowOverride` nicht ausreichend –
|
||
> dann bitte Variante A1 verwenden.
|
||
|
||
### Beispiel: Hetzner Webhosting (konsoleH)
|
||
|
||
Stellvertretend für Anbieter mit eigenem Panel – bei IONOS, Strato, All-Inkl oder Netcup
|
||
heißen die Menüpunkte anders, die Schritte sind dieselben.
|
||
|
||
1. **PHP-Version prüfen/setzen** – in konsoleH unter *Einstellungen → PHP-Version*
|
||
mindestens **PHP 8.2** wählen. Steht dort noch 7.x oder 8.0, läuft die Anwendung nicht.
|
||
2. **Verzeichnis wählen.** Der Webspace hat pro Domain ein Verzeichnis unterhalb von
|
||
`/public_html`. Lässt sich das Zielverzeichnis der Domain frei setzen, nimm
|
||
**Variante A1** und zeige damit auf `…/kurzlinks/public`. Geht das nicht, nimm
|
||
**Variante A2** und lade alles direkt in das Domainverzeichnis.
|
||
3. **Hochladen** per SFTP (Zugangsdaten stehen in konsoleH unter *Benutzer*). Den
|
||
**Inhalt** von `build/urlshort-qr-gen-release/` übertragen, nicht den Ordner selbst.
|
||
4. **Schreibrechte** für `data/` setzen – im FTP-Programm Rechte `755` (bei manchen
|
||
Tarifen `775`). Ohne das legt die Anwendung keine Datenbank an.
|
||
5. **`.env` anlegen:** `.env.example` in `.env` umbenennen und mindestens eintragen:
|
||
|
||
```dotenv
|
||
APP_BASE_URL=https://kurz.deine-domain.de
|
||
SESSION_SECURE=true
|
||
```
|
||
|
||
6. **HTTPS aktivieren** – Hetzner stellt kostenlose Let’s-Encrypt-Zertifikate aus
|
||
(*Einstellungen → SSL*). Vorher `APP_BASE_URL` auf `http://` lassen, sonst zeigen
|
||
Kurzlinks auf eine Adresse, die der Browser nicht öffnet.
|
||
7. **Domain aufrufen.** Der Setup-Assistent prüft zuerst die Systemvoraussetzungen und
|
||
zeigt an, falls etwas fehlt. Er verlangt außerdem das Einmal-Token aus
|
||
`data/setup-token.txt` – diese Datei per FTP öffnen, den 16-stelligen Wert eintragen.
|
||
Danach wird sie automatisch gelöscht.
|
||
|
||
**Datenbank:** SQLite genügt und ist voreingestellt – die bei Hetzner enthaltenen
|
||
MySQL-Datenbanken werden nicht gebraucht. Wer sie trotzdem nutzen will, trägt in der
|
||
`.env` `DB_DRIVER=mysql` samt Zugangsdaten aus konsoleH ein.
|
||
|
||
**Was bei Hetzner erfahrungsgemäß passt:** `mod_rewrite`, `gd`, `mbstring`, `fileinfo`
|
||
und `pdo_sqlite` sind vorhanden. `imagick` ist es meist nicht – dann erscheinen
|
||
SVG-Logos nur im SVG-Download, PNG-Logos funktionieren normal. Der Setup-Assistent
|
||
zeigt den tatsächlichen Stand deines Tarifs an; verlass dich darauf statt auf diese
|
||
Aufzählung.
|
||
|
||
### Wichtige Hinweise zu Shared Hosting
|
||
|
||
- **`mod_rewrite` muss aktiv sein.** Ohne Rewrites funktionieren die Kurzlinks nicht.
|
||
- Läuft die Anwendung in einem **Unterverzeichnis** (`https://domain.de/kurz/`),
|
||
wird das automatisch erkannt; `APP_BASE_URL` muss dann `https://domain.de/kurz`
|
||
lauten.
|
||
- Die SQLite-Datei liegt in `data/database.sqlite` und wird beim ersten Aufruf
|
||
automatisch samt Schema angelegt.
|
||
- **Backup:** Es genügt, den Ordner `data/` zu sichern (Datenbank + Logos).
|
||
|
||
---
|
||
|
||
## Deployment B: Docker
|
||
|
||
### Schnellstart (SQLite, nichts weiter nötig)
|
||
|
||
```bash
|
||
cp .env.example .env # optional – Defaults funktionieren auch ohne
|
||
docker compose up -d --build
|
||
```
|
||
|
||
Danach: <http://localhost:8080> → der Setup-Assistent fragt Benutzername und Passwort ab.
|
||
|
||
Der Container basiert auf `php:8.3-apache` und verwendet dieselbe `.htaccess` wie das
|
||
Shared Hosting – Docker und FTP-Deployment verhalten sich dadurch identisch.
|
||
Datenbank und Logos liegen im benannten Volume `app-data` und überleben ein `--build`.
|
||
|
||
Anderer Port:
|
||
|
||
```bash
|
||
APP_PORT=9000 APP_BASE_URL=http://localhost:9000 docker compose up -d --build
|
||
```
|
||
|
||
### Mit MariaDB statt SQLite
|
||
|
||
```bash
|
||
DB_DRIVER=mysql docker compose --profile mysql up -d --build
|
||
```
|
||
|
||
Die App wartet beim Start, bis die Datenbank erreichbar ist, und legt das Schema
|
||
selbst an.
|
||
|
||
### Konfiguration im Container
|
||
|
||
Die Werte lassen sich auf zwei Wegen setzen:
|
||
|
||
- **Umgebungsvariablen** in `docker-compose.yml` bzw. einer `.env` neben der
|
||
Compose-Datei (Compose setzt sie als Container-Umgebung), **oder**
|
||
- eine in den Container gemountete `.env`-Datei.
|
||
|
||
> Achtung: Eine im Projekt vorhandene `.env` hat **Vorrang** vor
|
||
> Container-Umgebungsvariablen. Für Docker also entweder das eine oder das andere
|
||
> verwenden – nicht beides gemischt.
|
||
|
||
### Nützliche Befehle
|
||
|
||
```bash
|
||
docker compose logs -f app # Logs ansehen
|
||
docker compose exec app sh # Shell im Container
|
||
docker compose down # stoppen (Daten bleiben im Volume)
|
||
docker compose down -v # stoppen und Daten löschen
|
||
```
|
||
|
||
---
|
||
|
||
## Konfigurationsreferenz
|
||
|
||
Alle Werte stehen in der `.env` (Vorlage: `.env.example`) oder als Umgebungsvariablen.
|
||
|
||
### Anwendung
|
||
|
||
| Schlüssel | Standard | Bedeutung |
|
||
|---|---|---|
|
||
| `APP_BASE_URL` | *(aus dem Request)* | Basis-URL **ohne** Schrägstrich am Ende. Daraus wird beim ersten Start die Standarddomain angelegt. Weitere Domains werden danach im Admin-Bereich verwaltet – `APP_BASE_URL` dient dann nur noch als Rückfallwert. |
|
||
| `APP_ENV` | `production` | `development` zeigt Fehlerdetails im Browser an. |
|
||
| `APP_TIMEZONE` | `Europe/Berlin` | Zeitzone für die Anzeige. Gespeichert wird immer UTC. |
|
||
| `REDIRECT_STATUS` | `302` | `301`/`308` = permanent (wird vom Browser gecacht → es werden weniger Klicks gezählt), `302`/`307` = temporär (empfohlen). Ungültige Werte fallen auf `302` zurück. |
|
||
|
||
### Datenbank
|
||
|
||
**Standardmäßig SQLite** – eine einzelne Datei unter `data/database.sqlite`, angesprochen
|
||
über PDO. Es ist kein Datenbankserver nötig, nichts einzurichten und nichts zu warten; die
|
||
Datei wird beim ersten Aufruf samt Schema automatisch angelegt. Für einen URL-Shortener ist
|
||
das die passende Wahl: Weiterleitungen sind reine Lesezugriffe über einen Index, und
|
||
Schreibvorgänge (ein `UPDATE` plus ein `INSERT` je Klick) sind selten genug, dass SQLite
|
||
auch bei einigen hunderttausend Klicks im Monat nicht ins Schwitzen kommt. Die Anwendung
|
||
schaltet dafür `WAL` ein, sodass sich Lesen und Schreiben nicht gegenseitig blockieren.
|
||
|
||
**Optional MySQL/MariaDB** – sinnvoll, wenn ohnehin ein Datenbankserver läuft, wenn
|
||
mehrere Webserver auf denselben Datenbestand zugreifen sollen oder wenn die vorhandene
|
||
Backup-Routine auf Datenbanken ausgelegt ist. Umschalten über `DB_DRIVER=mysql`; das
|
||
Schema wird dort ebenso automatisch angelegt.
|
||
|
||
Beide Wege nutzen dieselben vorbereiteten Statements – ein Wechsel ändert nichts an der
|
||
Bedienung. Ein *Umzug* der Daten von SQLite nach MySQL ist allerdings nicht eingebaut;
|
||
dafür bräuchte es einen Export/Import von Hand.
|
||
|
||
Tabellen: `domains`, `links`, `visits`, `qr_designs`, `settings`, `login_attempts`
|
||
sowie `migrations` (welche Migration bereits gelaufen ist).
|
||
|
||
**Backup:** Bei SQLite genügt es, den Ordner `data/` zu sichern – er enthält Datenbank und
|
||
hochgeladene Logos. Am saubersten im laufenden Betrieb mit
|
||
`sqlite3 data/database.sqlite ".backup data/backup.sqlite"`; ein einfaches Kopieren der
|
||
Datei funktioniert in der Praxis ebenfalls, sofern gerade nicht geschrieben wird.
|
||
|
||
| Schlüssel | Standard | Bedeutung |
|
||
|---|---|---|
|
||
| `DB_DRIVER` | `sqlite` | `sqlite` oder `mysql` |
|
||
| `DB_SQLITE_PATH` | `data/database.sqlite` | Pfad zur SQLite-Datei, relativ zum Projektstamm. |
|
||
| `DB_HOST` / `DB_PORT` | `127.0.0.1` / `3306` | nur bei `mysql` |
|
||
| `DB_NAME` / `DB_USER` / `DB_PASSWORD` | `urlshort` / `urlshort` / – | nur bei `mysql` |
|
||
| `DB_CHARSET` | `utf8mb4` | nur bei `mysql` |
|
||
|
||
Das Schema wird beim ersten Request automatisch angelegt (`migrations/`), ein
|
||
manueller Migrationsschritt entfällt.
|
||
|
||
### Admin-Zugang
|
||
|
||
| Schlüssel | Standard | Bedeutung |
|
||
|---|---|---|
|
||
| `ADMIN_USER` | `admin` | Benutzername |
|
||
| `ADMIN_PASSWORD_HASH` | *(leer)* | bcrypt-Hash. **Leer** → beim ersten Aufruf startet der Setup-Assistent und speichert die Zugangsdaten in der Datenbank. |
|
||
|
||
Hash selbst erzeugen:
|
||
|
||
```bash
|
||
php -r "echo password_hash('dein-passwort', PASSWORD_DEFAULT), PHP_EOL;"
|
||
# oder im Container:
|
||
docker compose exec app php -r "echo password_hash('dein-passwort', PASSWORD_DEFAULT), PHP_EOL;"
|
||
```
|
||
|
||
Ist `ADMIN_PASSWORD_HASH` gesetzt, gewinnt dieser Wert gegenüber der Datenbank –
|
||
das ist gleichzeitig der Weg, ein vergessenes Passwort zurückzusetzen.
|
||
|
||
### Sessions und Login-Schutz
|
||
|
||
| Schlüssel | Standard | Bedeutung |
|
||
|---|---|---|
|
||
| `SESSION_NAME` | `urlshort_session` | Name des Session-Cookies |
|
||
| `SESSION_LIFETIME` | `7200` | Sekunden bis zum automatischen Abmelden bei Inaktivität |
|
||
| `SESSION_ABSOLUTE_LIFETIME` | `43200` | Harte Obergrenze je Sitzung, unabhängig von Aktivität (`0` = aus) |
|
||
| `SESSION_SECURE` | `false` | Cookie nur über HTTPS. Wird bei einer HTTPS-Anfrage automatisch aktiviert; auf `true` setzen, wenn ausschließlich HTTPS verwendet wird |
|
||
| `LOGIN_MAX_ATTEMPTS` | `5` | Fehlversuche bis zur Sperre |
|
||
| `LOGIN_LOCKOUT_SECONDS` | `900` | Dauer der Sperre |
|
||
| `TRUSTED_PROXIES` | *(leer)* | Kommaliste aus IPs/CIDR-Bereichen, deren `X-Forwarded-*`-Header ausgewertet werden dürfen. **Leer lassen, wenn kein Proxy davorsteht** – sonst kann jeder die Client-IP fälschen und die Login-Sperre umgehen |
|
||
| `HSTS_MAX_AGE` | `31536000` | HSTS-Dauer in Sekunden (`0` = aus). Wird nur bei HTTPS-Anfragen gesendet |
|
||
|
||
> **Hinter Reverse Proxy, Cloudflare oder Loadbalancer:** ohne `TRUSTED_PROXIES` sieht
|
||
> die Anwendung nur die IP des Proxys – die Login-Sperre gilt dann für alle Besucher
|
||
> gemeinsam. Dort die IP des Proxys eintragen (bei Docker Compose z. B. das
|
||
> Bridge-Netz `172.16.0.0/12`), aber **nur** die tatsächlich vorgeschalteten Adressen.
|
||
|
||
### Datenschutz
|
||
|
||
| Schlüssel | Standard | Bedeutung |
|
||
|---|---|---|
|
||
| `TRACK_IP` | `none` | `none` = IP wird nirgends verarbeitet oder gespeichert. `hash` = statt der IP wird ein gesalzener, tageweise rotierender 128-Bit-Hash abgelegt. |
|
||
| `IP_HASH_SALT` | *(leer)* | Zufälliger Salt, nur bei `TRACK_IP=hash` relevant. Ohne Salt bleibt der Hash leer. |
|
||
| `VISIT_RETENTION_DAYS` | `365` | Einzelne Besuchsdatensätze älter als X Tage werden automatisch gelöscht (`0` = nie). Die Gesamt-Klickzahl am Link bleibt erhalten. |
|
||
|
||
### Uploads
|
||
|
||
| Schlüssel | Standard | Bedeutung |
|
||
|---|---|---|
|
||
| `UPLOAD_DIR` | `data/uploads` | Ablage der QR-Logos (außerhalb des Webroots) |
|
||
| `UPLOAD_MAX_BYTES` | `2097152` | Maximale Logogröße in Byte (2 MB) |
|
||
|
||
---
|
||
|
||
## Bedienung
|
||
|
||
### Anmelden
|
||
|
||
Beim ersten Aufruf erscheint der Setup-Assistent (sofern kein
|
||
`ADMIN_PASSWORD_HASH` gesetzt ist). Danach führt jeder Aufruf zur Anmeldemaske.
|
||
|
||
### Kurzlink anlegen
|
||
|
||
**Kurzlinks → Neuer Kurzlink**
|
||
|
||
- **Ziel-URL** – fehlt das Schema, wird `https://` ergänzt.
|
||
|
||
Erlaubt sind nur `http` und `https` (`javascript:`, `data:`, `file:`, `mailto:` werden abgelehnt).
|
||
Innerhalb dieser Schemata ist die Prüfung bewusst großzügig:
|
||
|
||
| Beispiel | |
|
||
|---|---|
|
||
| `https://müller.de/preise` | Umlaut-Domains (IDN) |
|
||
| `https://de.wikipedia.org/wiki/München` | Umlaute in Pfad und Query |
|
||
| `http://intranet/wiki`, `http://nas:5000/` | interne Hostnamen ohne Punkt |
|
||
| `https://[2001:db8::1]:8443/x` | IPv6-Adressen |
|
||
| `https://user:pass@beispiel.de/` | Benutzerinfo |
|
||
|
||
Gespeichert und angezeigt wird die **lesbare** Schreibweise. Erst beim Weiterleiten wird der
|
||
Hostname nach Punycode gewandelt (`müller.de` → `xn--mller-kva.de`), weil HTTP-Header laut
|
||
RFC 7230 ASCII sein müssen. Pfad und Query werden beim Speichern prozentkodiert, ohne
|
||
bereits kodierte Sequenzen ein zweites Mal zu kodieren.
|
||
- **Wunsch-Slug** – optional. Leer lassen für einen zufälligen Slug.
|
||
|
||
Erlaubt sind Buchstaben, Ziffern, Bindestrich, Unterstrich und Punkt. Der
|
||
**Schrägstrich trennt Abschnitte**, sodass sich auch mehrteilige Adressen abbilden
|
||
lassen:
|
||
|
||
| Eingabe | Ergebnis |
|
||
|---|---|
|
||
| `messe2026` | `deine-domain.de/messe2026` |
|
||
| `g/1AEJ0X90ZmF` | `deine-domain.de/g/1AEJ0X90ZmF` |
|
||
| `Flyer2026/Halle-3` | `deine-domain.de/Flyer2026/Halle-3` |
|
||
| `https://alt.de/g/ABC` | wird auf `g/ABC` reduziert |
|
||
|
||
**Groß- und Kleinschreibung bleibt erhalten.** Das ist wichtig, um bereits gedruckte
|
||
QR-Codes aus einem anderen System zu übernehmen – deren Pfade lassen sich
|
||
nachträglich nicht mehr ändern. Beim *Aufruf* wird die Schreibweise zusätzlich ohne
|
||
Beachtung erkannt, falls jemand den Code abtippt.
|
||
|
||
Zwei Links, die sich **nur** in der Schreibweise unterscheiden, sind auf derselben
|
||
Domain nicht möglich (`Messe` und `messe` schließen sich aus) – sonst wäre an einer
|
||
gedruckten Adresse nicht mehr erkennbar, welcher gemeint ist.
|
||
|
||
Umlaute und Leerzeichen werden weiterhin umgeschrieben (`Grüße 2026` → `Gruesse-2026`).
|
||
Der erste Abschnitt darf nicht `admin`, `login`, `assets`, `.well-known` usw. lauten,
|
||
damit die Verwaltung nicht verdeckt wird.
|
||
- **Interne Bezeichnung** – nur zur eigenen Orientierung, nirgends öffentlich.
|
||
- **Aktiv** – deaktivierte Links liefern die 404-Seite, behalten aber ihre Statistik.
|
||
|
||
Nach dem Speichern landest du direkt beim QR-Code-Generator.
|
||
|
||
### QR-Code gestalten
|
||
|
||
- Farben per Farbwähler, Hex-Feld oder Voreinstellung. Bei zu geringem Kontrast oder
|
||
invertierter Darstellung erscheint sofort eine Warnung – solche Codes werden von
|
||
vielen Scannern nicht erkannt.
|
||
- **Größe** betrifft den PNG-Download; SVG skaliert ohnehin verlustfrei.
|
||
- **Randbreite** ist die Quiet Zone in Modulen. Die Spezifikation empfiehlt 4.
|
||
- **Logo** – PNG, JPG oder SVG. Die Fehlerkorrektur springt automatisch auf Level H,
|
||
damit der Code trotz Abdeckung lesbar bleibt. Als Faustregel: nicht mehr als
|
||
~25 % der Breite und den fertigen Code immer mit einem echten Gerät testen.
|
||
- **Speichern** ist optional – Vorschau und Download nutzen immer die aktuell
|
||
eingestellten Werte. Gespeichert wird nur, damit die Einstellungen beim nächsten
|
||
Besuch wieder da sind.
|
||
|
||
> Der QR-Code zeigt auf den **Kurzlink**, nicht auf die Ziel-URL. Gedruckte Codes
|
||
> bleiben dadurch gültig, auch wenn sich das Ziel später ändert.
|
||
|
||
### Mehrere Domains
|
||
|
||
Standardmäßig gibt es genau eine Domain – sie wird beim ersten Start automatisch aus
|
||
`APP_BASE_URL` angelegt. Wer Kurzlinks unter mehreren Domains ausliefern will
|
||
(z. B. `short.example.de` für interne Links und `qr.example.de` für gedruckte QR-Codes),
|
||
trägt sie unter **Domains → Neue Domain** ein.
|
||
|
||
Danach steht im Kurzlink-Formular eine Domain-Auswahl. Der Slug muss nur noch **innerhalb
|
||
seiner Domain** eindeutig sein – `short.example.de/messe` und `qr.example.de/messe` sind
|
||
zwei unabhängige Links mit eigener Statistik und eigenem QR-Code.
|
||
|
||
#### Was beim Provider zu tun ist
|
||
|
||
Eine Domain hier einzutragen genügt nicht – sie muss beim Domain-Anbieter und beim Hoster
|
||
auf **diese** Installation zeigen. Dieselbe Anleitung steht auch im Admin-Bereich unter
|
||
**Domains**.
|
||
|
||
> **Wichtig:** Die neue Domain darf *keine* zweite Installation bekommen und *keine*
|
||
> „Domain-Weiterleitung“ des Registrars sein. Eine Weiterleitung ersetzt die Adresse im
|
||
> Browser – der Slug ginge dabei verloren. Die Domain muss vom **selben Webroot**
|
||
> beantwortet werden.
|
||
|
||
**1. DNS: Domain auf den Server zeigen lassen**
|
||
|
||
Beim Domain-Anbieter in der DNS- bzw. Zonenverwaltung anlegen:
|
||
|
||
| Typ | Name | Wert | Wofür |
|
||
|---|---|---|---|
|
||
| `A` | `qr` | `203.0.113.10` | IPv4 des Servers |
|
||
| `AAAA` | `qr` | `2001:db8::10` | IPv6, falls vorhanden |
|
||
| `CNAME` | `qr` | `short.example.de.` | Alternative zu A/AAAA |
|
||
|
||
Entweder A (+ AAAA) *oder* CNAME – nicht beides. Die richtige IP steht im Hosting-Panel
|
||
oder im vorhandenen Eintrag der Hauptdomain. Je nach TTL dauert die Verteilung wenige
|
||
Minuten bis einige Stunden.
|
||
|
||
Die Menüs heißen je nach Anbieter anders, gemeint ist immer dasselbe:
|
||
IONOS „Domains & SSL → Domain → DNS“ · Strato „Domainverwaltung → DNS-Einstellungen“ ·
|
||
Netcup „Domains → DNS“ · Hetzner „DNS Console → Zone → Records“ ·
|
||
Cloudflare „DNS → Records“ · All-Inkl „Domains → Einstellungen → DNS“.
|
||
Liegen Domain und Hosting beim selben Anbieter, wird der Eintrag beim Anlegen der
|
||
Subdomain oft automatisch gesetzt – dann entfällt dieser Schritt.
|
||
|
||
**2. Hosting: Domain auf dasselbe Verzeichnis legen**
|
||
|
||
- **Plesk:** „Websites & Domains → Subdomain hinzufügen“. Den vorgeschlagenen
|
||
**Dokumentenstamm** auf das Verzeichnis der vorhandenen Installation ändern (dort, wo
|
||
`index.php` liegt – meist `…/httpdocs/public`). Kein neues Hosting-Paket anlegen.
|
||
- **cPanel:** „Domains → Create A New Domain“, Haken bei *„Share document root“* setzen
|
||
bzw. den **Document Root** manuell auf das vorhandene Verzeichnis zeigen lassen.
|
||
- **Anderes Panel (IONOS, Strato, All-Inkl …):** „Subdomain anlegen“ bzw. „Domain
|
||
zuordnen“ mit frei wählbarem **Zielverzeichnis**. Manche nennen das „Alias-“ oder
|
||
„Addon-Domain“. Nicht geeignet: „Domain-Weiterleitung“ oder „Frame-Weiterleitung“.
|
||
- **Eigener Apache:**
|
||
|
||
```apache
|
||
<VirtualHost *:443>
|
||
ServerName short.example.de
|
||
ServerAlias qr.example.de
|
||
DocumentRoot /var/www/urlshort/public
|
||
</VirtualHost>
|
||
```
|
||
|
||
```bash
|
||
sudo apachectl configtest && sudo systemctl reload apache2
|
||
```
|
||
|
||
- **Eigener Nginx:**
|
||
|
||
```nginx
|
||
server {
|
||
server_name short.example.de qr.example.de;
|
||
root /var/www/urlshort/public;
|
||
}
|
||
```
|
||
|
||
```bash
|
||
sudo nginx -t && sudo systemctl reload nginx
|
||
```
|
||
|
||
- **Docker:** nichts zu tun – der Container beantwortet jeden Host. Steht ein Reverse
|
||
Proxy davor (Traefik, Nginx Proxy Manager, Caddy …), muss die Domain dort als weiterer
|
||
Host auf denselben Dienst zeigen.
|
||
|
||
**3. HTTPS: Zertifikat für die neue Domain**
|
||
|
||
Im Hosting-Panel bei der neuen (Sub-)Domain „Let’s Encrypt“ bzw. „SSL/TLS“ aktivieren –
|
||
cPanel erledigt das über AutoSSL meist selbst. Auf eigenem Server:
|
||
|
||
```bash
|
||
sudo certbot --expand -d short.example.de -d qr.example.de
|
||
```
|
||
|
||
Solange kein Zertifikat vorhanden ist, die Domain mit `http://` eintragen – sonst zeigen
|
||
Kurzlinks und QR-Codes auf eine Adresse, die der Browser nicht öffnen kann.
|
||
|
||
**4. Domain in der Anwendung eintragen**
|
||
|
||
Unter **Domains → Neue Domain**, exakt so, wie sie im Browser aufgerufen wird (gleiches
|
||
Schema, gleicher Port). Erst danach ordnet die Anwendung Aufrufe dieser Domain zu.
|
||
|
||
**5. Prüfen**
|
||
|
||
In der Domain-Liste gibt es je Eintrag einen Knopf **Prüfen**. Die Anwendung
|
||
löst den Hostnamen auf und ruft die Domain anschließend **von außen** auf – das Ergebnis
|
||
zeigt Schritt für Schritt, wie weit die Einrichtung ist:
|
||
|
||
| Prüfschritt | Was er belegt |
|
||
|---|---|
|
||
| **DNS** | Der Hostname löst auf – und auf welche IP-Adresse(n) |
|
||
| **Zuordnung** | Der Aufruf landet bei **dieser** Installation (nicht nur „irgendwo“) |
|
||
| **HTTPS** | Das Zertifikat gilt für diese Domain |
|
||
|
||
Für die Zuordnung wird ein Endpunkt unter `/.well-known/urlshort-check` aufgerufen. Er ist
|
||
nur mit einem kurzlebigen, signierten Token ansprechbar (Gültigkeit 3 Minuten) und
|
||
antwortet sonst mit `404`. Kommt der Aufruf mit gültigem Token an, ist damit bewiesen, dass
|
||
die Domain hier ankommt – nur diese Installation kann ein solches Token erzeugen.
|
||
|
||
> Eine reine DNS-Prüfung würde hier nicht reichen: Ein korrekter A-Record sagt nichts
|
||
> darüber aus, ob der Webserver die Domain auch dem richtigen Verzeichnis zuordnet – und
|
||
> genau das ist der häufigste Fehler.
|
||
|
||
Typische Ergebnisse und ihre Ursache:
|
||
|
||
| Meldung | Ursache |
|
||
|---|---|
|
||
| „lässt sich nicht auflösen“ | DNS-Eintrag fehlt oder ist noch nicht verteilt → Schritt 1 |
|
||
| „antwortet mit einer Weiterleitung“ | Domain-Weiterleitung beim Anbieter statt echter Zuordnung → Schritt 2 |
|
||
| „Antwort kommt nicht von dieser Installation“ | Die Domain zeigt auf ein anderes Verzeichnis oder eine zweite Installation → Schritt 2 |
|
||
| „Zertifikat gilt nicht für diese Domain“ | → Schritt 3 |
|
||
| „konnte nicht abgerufen werden“ | Webserver antwortet nicht auf diese Domain, Firewall oder Tippfehler |
|
||
|
||
**Wenn die Prüfung fehlschlägt, obwohl alles funktioniert:** Manche Server können ihren
|
||
eigenen öffentlichen Hostnamen nicht aufrufen (fehlendes NAT-Hairpinning). Lässt sich die
|
||
Domain von außen – etwa vom Handy über Mobilfunk – öffnen, ist alles in Ordnung. Die
|
||
Anwendung weist im Ergebnis selbst darauf hin.
|
||
|
||
Fehlen auf dem Server sowohl `ext-curl` als auch `allow_url_fopen`, kann die Zuordnung
|
||
nicht automatisch geprüft werden; die Prüfung sagt das dann ausdrücklich.
|
||
|
||
**Zuordnung im Detail:** Die Anwendung vergleicht den `Host`-Header der Anfrage mit den
|
||
eingetragenen Domains (Groß-/Kleinschreibung egal, Standardports `:80`/`:443` werden
|
||
ignoriert). Passt kein Eintrag – etwa beim Aufruf über die nackte IP-Adresse oder hinter
|
||
einem Proxy – greift die **Standarddomain**. Bestehende Links bleiben dadurch auch dann
|
||
erreichbar, wenn sich am Hostnamen etwas ändert.
|
||
|
||
Die Standarddomain kann nicht gelöscht werden; eine Domain mit Kurzlinks ebenfalls nicht,
|
||
solange noch Links daran hängen. Ein Link lässt sich jederzeit über **Bearbeiten** auf eine
|
||
andere Domain umziehen – bereits verteilte Links und gedruckte QR-Codes verlieren dabei
|
||
allerdings ihre Gültigkeit.
|
||
|
||
### Statistik lesen
|
||
|
||
**Kurzlinks → Statistik**: Gesamtklicks, Verlauf als Liniendiagramm (Zeitraum oben
|
||
rechts umschaltbar), Top-Referrer sowie die Aufteilung nach Gerät, Browser und
|
||
Plattform. Darunter die letzten 25 Aufrufe.
|
||
|
||
Steht `REDIRECT_STATUS` auf `301`, cachen Browser die Weiterleitung – wiederholte
|
||
Aufrufe derselben Person tauchen dann nicht in der Statistik auf. Für belastbare
|
||
Zahlen `302` verwenden.
|
||
|
||
---
|
||
|
||
## Projektstruktur
|
||
|
||
```
|
||
urlshort-qr-gen/
|
||
├── public/ ← Webroot
|
||
│ ├── index.php ← einziger Einstiegspunkt
|
||
│ ├── .htaccess ← Rewrites + Security-Header
|
||
│ └── assets/ ← CSS, JS, Favicon (kein Build-Schritt)
|
||
├── src/
|
||
│ ├── App.php ← Verdrahtung, Routing, Security-Header
|
||
│ ├── Config.php ← .env + Umgebungsvariablen
|
||
│ ├── Database.php ← PDO (SQLite/MySQL)
|
||
│ ├── Migrator.php ← führt migrations/ automatisch aus
|
||
│ ├── View.php ← minimale Template-Engine
|
||
│ ├── Http/ ← Request, Response, Router, BasePath
|
||
│ ├── Controller/ ← Setup, Auth, Dashboard, Link, Domain, Stats, Qr, Redirect
|
||
│ ├── Repository/ ← Datenzugriff (vorbereitete Statements)
|
||
│ ├── Security/ ← Session, Auth, CSRF, Rate-Limiting
|
||
│ ├── Service/ ← DomainChecker (DNS- und Zuordnungsprüfung)
|
||
│ ├── Qr/ ← QrOptions, QrRenderer, LogoStore
|
||
│ └── Support/ ← Slug, UrlValidator, Punycode, ShortDomain, UserAgentParser, Color
|
||
├── templates/ ← serverseitig gerenderte Seiten
|
||
├── migrations/ ← Schema je Treiber
|
||
├── data/ ← SQLite-Datei + hochgeladene Logos (beschreibbar!)
|
||
├── tests/ ← PHPUnit
|
||
├── docker/ ← php.ini, entrypoint.sh
|
||
├── Dockerfile
|
||
├── docker-compose.yml
|
||
├── .htaccess ← nur für Shared Hosting ohne eigenen Webroot
|
||
└── .env.example
|
||
```
|
||
|
||
---
|
||
|
||
## Sicherheit und Datenschutz
|
||
|
||
- **Nur Weiterleitungen sind öffentlich.** Jede andere Route verlangt eine gültige
|
||
Session; hochgeladene Logos werden ebenfalls nur nach Login ausgeliefert.
|
||
- **Ersteinrichtung:** Der Setup-Assistent verlangt zusätzlich ein Einmal-Token aus
|
||
`data/setup-token.txt` (steht auch im Serverprotokoll). Damit kann eine frisch
|
||
hochgeladene Installation nicht von jemandem übernommen werden, der sie zufällig
|
||
zuerst aufruft. Nach der Einrichtung wird die Datei gelöscht.
|
||
- **Datenbank:** ausschließlich vorbereitete Statements; Spalten- und
|
||
Sortiernamen kommen aus festen Allowlists.
|
||
- **XSS:** jede Ausgabe läuft durch `htmlspecialchars` mit `ENT_QUOTES`.
|
||
- **CSRF:** Synchronizer-Token in jedem Formular; ungültige Token ergeben 403.
|
||
- **Sessions:** `HttpOnly`, `SameSite=Lax`, `use_strict_mode`, ID-Wechsel beim Login,
|
||
Inaktivitätsgrenze **und** absolute Obergrenze. Über HTTPS wird das Cookie
|
||
automatisch auf `secure` gesetzt. Ein Passwortwechsel entwertet bestehende Sessions.
|
||
- **Login:** bcrypt, Fehlversuche werden pro IP-*Hash* gezählt und gesperrt. Dieselbe
|
||
Sperre gilt für das Einrichtungs-Token.
|
||
- **Proxy-Header:** `X-Forwarded-For` und `X-Forwarded-Proto` werden **nur** hinter
|
||
einem in `TRUSTED_PROXIES` eingetragenen Proxy ausgewertet. Ohne diese Einstellung
|
||
zählt allein `REMOTE_ADDR` – sonst ließe sich die Login-Sperre durch einen
|
||
erfundenen Header umgehen.
|
||
- **Host-Header:** wird gegen ein Muster geprüft, bevor er in erzeugte URLs einfließt
|
||
(und nur, wenn `APP_BASE_URL` nichts vorgibt).
|
||
- **Security-Header:** CSP ohne `unsafe-inline` für Skripte, `X-Content-Type-Options`,
|
||
`X-Frame-Options: DENY`, `Referrer-Policy`, `Permissions-Policy`,
|
||
`Cross-Origin-Opener-Policy`, `Cross-Origin-Resource-Policy`; über HTTPS zusätzlich
|
||
HSTS und `upgrade-insecure-requests`.
|
||
- **Externe Skripte:** Chart.js ist auf eine Version festgelegt und per **Subresource
|
||
Integrity** an seinen Hash gebunden – ein manipuliertes CDN könnte keinen fremden
|
||
Code einschleusen.
|
||
- **Uploads:** nur PNG/JPG/SVG, Prüfung gegen GD (beschädigte Dateien werden
|
||
abgelehnt), Größen- und Abmessungsgrenzen, eigener Dateiname, Ablage außerhalb des
|
||
Webroots. SVGs werden beim Speichern von Skripten, Event-Handlern, Entities (XXE),
|
||
Stylesheet-Anweisungen und allen externen Referenzen befreit; beim Ausliefern
|
||
greift zusätzlich eine abschottende CSP (`sandbox`), sodass ein durchgerutschtes
|
||
SVG im Browser trotzdem keinen Code ausführen könnte.
|
||
- **Domain-Prüfung:** ruft nur einen festen Pfad ab, folgt keinen Weiterleitungen,
|
||
begrenzt die Antwortgröße und verweigert link-lokale Adressen (`169.254.0.0/16`,
|
||
`fe80::/10`) – dort liegen bei Cloud-Anbietern die Metadaten-Dienste.
|
||
- **DSGVO:** IP-Adressen werden standardmäßig **nicht** gespeichert. Vom User-Agent
|
||
bleiben nur Gerätetyp, Browser und Plattform als Kategorien übrig; der Rohstring
|
||
wird zwar abgelegt, aber nur gekürzt und über `VISIT_RETENTION_DAYS` automatisch
|
||
wieder gelöscht. Ein Cookie-Banner ist für die Weiterleitungen nicht nötig, da
|
||
dort keine Cookies gesetzt werden.
|
||
|
||
Das einzige Fremd-CDN ist Chart.js (`cdn.jsdelivr.net`) für das Liniendiagramm im
|
||
Admin-Bereich. Ist es nicht erreichbar, zeichnet die Anwendung ein einfaches
|
||
Balkendiagramm selbst. Wer ganz ohne CDN auskommen will, legt
|
||
`chart.umd.min.js` unter `public/assets/` ab und passt die URL in
|
||
`public/assets/app.js` an.
|
||
|
||
---
|
||
|
||
## Tests
|
||
|
||
```bash
|
||
composer install
|
||
vendor/bin/phpunit
|
||
```
|
||
|
||
Ohne lokales PHP über Docker:
|
||
|
||
```bash
|
||
docker run --rm -v "$PWD:/app" -w /app urlshort-qr-gen:latest vendor/bin/phpunit
|
||
```
|
||
|
||
Die Suite läuft ohne Deprecation-Meldungen unter PHP 8.3, 8.4 und 8.5. Das ist kein
|
||
Selbstzweck: Viele Hoster haben `display_errors` erzwungen auf `On`. Eine einzige
|
||
Meldung würde dort verhindern, dass noch HTTP-Header gesendet werden – keine Session,
|
||
keine Weiterleitung – und jeden PNG-Download beschädigen. Zusätzlich puffert
|
||
`public/index.php` die Ausgabe und verwirft Fremdausgaben, bevor die Antwort rausgeht.
|
||
|
||
Abgedeckt sind die Kernlogik und die Weiterleitung:
|
||
|
||
- `SlugTest` – Zufallserzeugung, Kollisionsbehandlung, Normalisierung, Validierung
|
||
- `UrlValidatorTest` – Schema-Prüfung, Normalisierung, IDN, Prozentkodierung, abgelehnte Protokolle
|
||
- `PunycodeTest` – IDN-Kodierung, gegengeprüft gegen `idn_to_ascii`
|
||
- `ShortDomainTest` – Basis-URL und Host-Header müssen zueinander passen
|
||
- `CheckTokenTest` – Signatur, Ablauf und Manipulationsschutz der Domain-Prüfung
|
||
- `DomainCheckerTest` – Prüfergebnisse für nicht auflösbare und nicht erreichbare Domains
|
||
- `UserAgentParserTest` – Gerätetyp, Browser, Plattform
|
||
- `ColorTest` – Kontrastberechnung und Warnungen
|
||
- `QrRendererTest` – PNG-/SVG-Erzeugung, Größen, Logos, Level-H-Automatik
|
||
- `LogoStoreTest` – Upload-Prüfung, SVG-Bereinigung, Pfad-Traversal
|
||
- `RedirectTest` – Migrationen, Redirect-Status, 404, Klickzählung, Datensparsamkeit,
|
||
Domain-Auflösung über den `Host`-Header
|
||
- `MigrationUpgradeTest` – Update einer bestehenden Installation auf mehrere Domains
|
||
|
||
---
|
||
|
||
## Fehlerbehebung
|
||
|
||
**„Abhängigkeiten fehlen“ nach dem Upload**
|
||
`vendor/` wurde nicht mit hochgeladen. FTP-Clients überspringen große
|
||
Verzeichnisse gerne bei Abbrüchen – Upload wiederholen.
|
||
|
||
**Kurzlinks liefern 404, die Verwaltung funktioniert aber**
|
||
`mod_rewrite` ist nicht aktiv oder `AllowOverride` verbietet die `.htaccess`.
|
||
Beim Hoster aktivieren lassen.
|
||
|
||
**„Datenbankverzeichnis ist nicht beschreibbar“**
|
||
Schreibrechte für `data/` setzen (`755` oder `775`, Eigentümer = Webserver-Benutzer).
|
||
|
||
**Kurzlinks zeigen die falsche Domain**
|
||
`APP_BASE_URL` in der `.env` korrigieren (ohne Schrägstrich am Ende).
|
||
|
||
**Passwort vergessen**
|
||
`ADMIN_USER` und `ADMIN_PASSWORD_HASH` in der `.env` setzen – diese Werte haben
|
||
Vorrang vor der Datenbank.
|
||
|
||
**SVG-Logo fehlt im PNG-Download**
|
||
Für das Rastern von SVGs wird `imagick` benötigt. Ist die Erweiterung nicht
|
||
verfügbar, stattdessen ein PNG-Logo hochladen oder den SVG-Download nutzen.
|
||
|
||
**Statistik zählt zu wenig**
|
||
`REDIRECT_STATUS=301` lässt Browser die Weiterleitung cachen. Auf `302` umstellen.
|
||
|
||
---
|
||
|
||
## Lizenz
|
||
|
||
MIT
|