LinkShorter v2 – Ausführliche Anleitung 1. Überblick & Architektur LinkShorter ist ein selbstgehosteter URL-Shortener, der vollständig in PHP geschrieben ist und eine SQLite-Datenbank verwendet – es wird also kein MySQL/MariaDB-Server benötigt. Die gesamte Anwendung besteht aus einer einzigen Einstiegsdatei (index.php), die als Front-Controller fungiert: Alle HTTP-Requests werden über die .htaccess per mod_rewrite an diese Datei weitergeleitet. Technische Kernkomponenten Komponente Datei Verantwortung Routing & Controller index.php Einziger Entry-Point, wertet $_GET['route'] aus Datenbank includes/db.php SQLite-Verbindung via PDO, Schema-Migration Authentifizierung includes/auth.php Session-basiertes Login Slug-Generierung includes/slug.php Zufällige Kurzlinks via SHA-256 Link-Logik includes/links.php CRUD, Click-Recording, Batch-Import OpenGraph includes/opengraph.php Metadaten-Extraktion von Ziel-URLs QR-Code includes/qrcode.php Eigene QR-Code-Implementierung (kein externer Service!) Datenfluss bei einem Kurzlink-Aufruf Browser → Apache (.htaccess Rewrite) → index.php?route=mein-slug → getLinkBySlug("mein-slug") → Prüfung: aktiv? abgelaufen? Passwort? Crawler? → recordClick() → HTTP 302 Redirect → Ziel-URL 2. Installation & Konfiguration Voraussetzungen PHP ≥ 7.4 mit aktivierten Extensions: pdo_sqlite, gd (für PNG-QR-Codes), mbstring Apache mit mod_rewrite aktiviert Schreibrechte auf das Verzeichnis data/ Schritt 1: Dateien hochladen Laden Sie alle Dateien auf Ihren Webserver hoch. Die Verzeichnisstruktur sollte so aussehen: / ├── index.php ├── config.php ├── .htaccess ├── assets/ │ ├── style.css │ └── app.js ├── includes/ │ ├── db.php │ ├── auth.php │ ├── slug.php │ ├── links.php │ ├── opengraph.php │ └── qrcode.php ├── templates/ │ ├── dashboard.php │ ├── edit.php │ ├── login.php │ ├── stats.php │ ├── settings.php │ ├── password.php │ ├── og_proxy.php │ ├── unavailable.php │ └── 404.php └── data/ ← wird automatisch erstellt ├── linkshorter.db ├── qr_cache/ └── qr_icon.svg Schritt 2: Konfiguration anpassen Öffnen Sie config.php und passen Sie die Werte an: $link['og_title'], 'og_description' => $link['og_description'], 'og_image' => $link['og_image'], ]; Link löschen Das Löschen erfolgt via POST-Request mit einer JavaScript-Bestätigung. Dank ON DELETE CASCADE in der Datenbank-Schema-Definition werden zugehörige Click-Datensätze automatisch mitgelöscht. 5. Passwortgeschützte Links Wenn ein Link mit Passwort versehen ist, zeigt der Server die Seite password.php an. Der Ablauf: Besucher → /mein-slug → getLinkBySlug() findet Link mit password ≠ null → GET-Request: Zeige Passwort-Formular → POST-Request mit link_password: → password_verify(eingabe, hash) → true: recordClick + Redirect → false: Fehlermeldung Wichtig: Das Passwort wird immer als bcrypt-Hash gespeichert ( password_hash($password, PASSWORD_DEFAULT)). Beim Vergleich wird password_verify() verwendet, was automatisch den Salt aus dem Hash extrahiert. Das bedeutet: Selbst wenn die Datenbank kompromittiert wird, sind die Link-Passwörter nicht im Klartext einsehbar. 6. QR-Codes LinkShorter enthält eine vollständig eigene QR-Code-Implementierung – es werden keine externen APIs oder Libraries benötigt. Technischer Tiefgang: QR-Code-Generierung Die Funktion qrEncode implementiert den kompletten QR-Code-Standard: Versionswahl: Basierend auf der Datenlänge wird die minimale QR-Version (1–40) bestimmt. Version 1 hat 21×21 Module, Version 40 hat 177×177 Module. Die Kapazitätstabelle $capacityL enthält die maximale Byte-Kapazität für Error Correction Level L (Low, ~7% Fehlerkorrektur). Matrix-Aufbau: placeFinderPattern: Drei 7×7-Erkennungsmuster in den Ecken (oben-links, oben-rechts, unten-links) Timing-Patterns: Abwechselnde Module in Zeile 6 und Spalte 6 placeAlignmentPattern: Ab Version 2 werden Ausrichtungsmuster platziert (Positionen aus getAlignmentPatternPositions) Datenkodierung (encodeData): Mode Indicator: 0100 (Byte-Modus) Zeichenzähler: 8 Bit (Version 1–9) oder 16 Bit (ab Version 10) Daten als 8-Bit-Bytes Padding mit 0xEC 0x11 (abwechselnd) Reed-Solomon-Fehlerkorrektur via generateECCodewords Galois-Feld-Arithmetik: Für die Fehlerkorrektur werden Berechnungen im GF(2⁸) durchgeführt. Die Funktionen gfMultiply, gfExp und gfLog implementieren die Multiplikation über das irreduzible Polynom 0x11D (x⁸ + x⁴ + x³ + x² + 1). Data-Interleaving: Bei mehreren Blöcken werden die Daten- und EC-Codewords interleaved (verschachtelt), um Burst-Fehler besser zu korrigieren. Maskierung: Alle 8 Maskmuster werden getestet. calculatePenalty berechnet die Strafpunkte nach vier Regeln: Fünf oder mehr gleiche Module in einer Reihe 2×2-Blöcke gleicher Module Spezielle Muster (1:1:3:1:1) Verhältnis dunkler zu heller Module nahe 50% Das Mask-Pattern mit der niedrigsten Penalty wird gewählt. Ausgabeformate PNG (getQRCodePNG): Erzeugt via GD-Library ( imagecreatetruecolor), mit optionalem Center-Icon via ImageMagick ( convert-Befehl) SVG (getQRCodeSVG): Reine XML-Generierung, Icon wird als eingebettetes SVG eingefügt Caching Beide Formate werden im Verzeichnis data/qr_cache/ gecacht (24 Stunden TTL). Der Cache-Key ist ein MD5-Hash aus Daten + Größe + Format. Custom Icon Unter Settings können Sie ein SVG-Icon hochladen (admin/action mit action=upload_icon), das im Zentrum aller QR-Codes angezeigt wird. Beim Upload wird der QR-Cache geleert, damit die neuen QR-Codes das Icon enthalten. 7. OpenGraph-Proxying Wenn ein Kurzlink in sozialen Netzwerken geteilt wird, erkennt LinkShorter den Crawler anhand des User-Agents: $isCrawler = preg_match( '/facebookexternalhit|Twitterbot|LinkedInBot|WhatsApp|Slackbot|TelegramBot|Discordbot|bot|crawler|spider/i', $ua ); Statt den Crawler weiterzuleiten, wird og_proxy.php ausgeliefert – eine minimale HTML-Seite mit den OpenGraph-Meta-Tags der Ziel-URL. Das bewirkt, dass in der Vorschau (z.B. auf Facebook, Twitter, Slack) das Bild, der Titel und die Beschreibung der Original-Seite angezeigt werden, obwohl der geteilte Link eine kurze URL ist. Technischer Hintergrund: fetchOpenGraph extrahiert beim Erstellen eines Links die Meta-Daten: HTTP-Request an die Ziel-URL mit 10-Sekunden-Timeout HTML-Parsing via DOMDocument Extraktion von og:title, og:description, og:image Fallback auf -Tag, wenn kein og:title vorhanden Die SSL-Verifikation ist bewusst deaktiviert ( verify_peer => false), um Probleme mit selbstsignierten Zertifikaten zu vermeiden. In Produktionsumgebungen sollte das ggf. angepasst werden. Wichtig: Das OpenGraph-Proxying funktioniert auch für passwortgeschützte Links – Crawler erhalten die Vorschau, ohne ein Passwort eingeben zu müssen. Normale Benutzer werden weiterhin nach dem Passwort gefragt. 8. Statistiken & Click-Tracking Was wird erfasst? Bei jedem erfolgreichen Redirect (auch nach Passworteingabe) ruft recordClick zwei Datenbankoperationen aus: Inkrement des Klickzählers in der links-Tabelle Detaillierter Click-Eintrag in der clicks-Tabelle: ip: IP-Adresse des Besuchers ( $_SERVER['REMOTE_ADDR']) user_agent: Browser-Kennung referer: Woher der Besucher kam clicked_at: Zeitstempel (automatisch via SQLite datetime('now')) Stats-Seite Die Stats-Seite (stats.php) zeigt: Gesamtklicks als große Zahl Max Clicks (Limit oder ∞) Status (aktiv/inaktiv) Letzte 100 Klicks als Tabelle mit IP, Referer, User-Agent und Zeitstempel Die Abfrage in getClickStats ist auf 100 Einträge limitiert ( LIMIT 100), um bei viel-geklickten Links die Performance zu gewährleisten. Link-Deaktivierung Ein Link wird automatisch als „nicht verfügbar" angezeigt, wenn: active = 0 (manuell deaktiviert) max_clicks erreicht wurde ( clicks >= max_clicks) expires_at in der Vergangenheit liegt In allen drei Fällen wird unavailable.php angezeigt. 9. Die REST-API LinkShorter bietet zwei API-Endpunkte, die mit HTTP Basic Authentication geschützt sind. POST /api/shorten Erstellt einen neuen Kurzlink. Request: POST /api/shorten HTTP/1.1 Authorization: Basic base64(username:password) Content-Type: application/json { "url": "https://example.com/sehr-lange-url", "slug": "custom", // optional "password": "geheim", // optional "expires_at": "2025-12-31T23:59", // optional "max_clicks": 100 // optional } Response (Erfolg): { "short_url": "https://kurz.example.com/custom", "slug": "custom" } Response (Fehler): { "error": "Slug already exists" } GET /api/check Prüft die Erreichbarkeit und Authentifizierung einer Instanz. Wird von der Chrome-Extension beim Hinzufügen einer neuen Instanz verwendet. Response: { "status": "ok", "title": "LinkShorter" } Technischer Hintergrund Die Authentifizierung wird im index.php inline geprüft: $authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? ''; if (preg_match('/^Basic\s+(.+)$/i', $authHeader, $m)) { $decoded = base64_decode($m[1]); list($user, $pass) = explode(':', $decoded, 2); // Vergleich mit ADMIN_USERNAME und ADMIN_PASSWORD } Hinweis: Apache kann den Authorization-Header manchmal nicht an PHP weiterleiten. In diesem Fall muss in der .htaccess folgende Zeile ergänzt werden: SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 10. Die Chrome-Extension Überblick Die Chrome-Extension ermöglicht es, die aktuelle Tab-URL mit einem Klick zu kürzen, ohne das Dashboard öffnen zu müssen. Sie unterstützt mehrere LinkShorter-Instanzen, was nützlich ist, wenn man verschiedene Domains für verschiedene Zwecke nutzt. Installation Navigieren Sie in Chrome zu chrome://extensions/ Aktivieren Sie den Entwicklermodus (oben rechts) Klicken Sie „Entpackte Erweiterung laden" Wählen Sie den Ordner chrome-extension/ Dateien der Extension Datei Funktion manifest.json Extension-Konfiguration (Manifest V3) popup.html UI-Struktur popup.css Styling popup.js Gesamte Logik Instanz hinzufügen Klicken Sie auf das ⚙-Symbol (Settings) Geben Sie ein: Name: Anzeigename (z.B. „Mein Server") URL: Die BASE_URL Ihrer LinkShorter-Installation Username/Password: Wie in config.php konfiguriert Klicken Sie „Add & Verify" Technischer Hintergrund: Beim Hinzufügen wird zunächst ein GET /api/check ausgeführt, um die Verbindung und Authentifizierung zu testen. Erst wenn {"status": "ok"} zurückkommt, wird die Instanz gespeichert. Die Instanzen werden in chrome.storage.sync gespeichert, was bedeutet, dass sie über mehrere Chrome-Installationen hinweg synchronisiert werden (wenn der Nutzer in Chrome eingeloggt ist). URL kürzen Navigieren Sie zur gewünschten Webseite Klicken Sie auf das LinkShorter-Icon in der Toolbar Die aktuelle Tab-URL wird automatisch eingetragen (via chrome.tabs.query) Optional: Wählen Sie eine andere Instanz oder geben Sie einen Custom Slug ein Klicken Sie „Shorten" Die verkürzte URL erscheint mit Copy-Button Berechtigungen Die Extension benötigt nur zwei Berechtigungen (manifest.json): activeTab: Zugriff auf die URL des aktiven Tabs storage: Speichern der Instanz-Konfigurationen Es werden keine Host-Permissions benötigt, da fetch() in Manifest V3 standardmäßig CORS-Requests durchführen darf (die API-Endpunkte müssen allerdings CORS erlauben oder die Extension muss die Requests direkt an die URL senden). 11. Automatische Link-Expiration (Cron) Einrichtung Der Endpunkt /cron deaktiviert alle abgelaufenen Links. Die Funktion expireLinks führt folgendes SQL aus: UPDATE links SET active = 0 WHERE expires_at IS NOT NULL AND expires_at <= datetime('now') AND active = 1 Cron-Job einrichten Fügen Sie in Ihrer Crontab folgenden Eintrag hinzu (z.B. alle 5 Minuten): */5 * * * * curl -s https://kurz.example.com/cron > /dev/null 2>&1 Alternativ via PHP-CLI: */5 * * * * php /var/www/html/index.php route=cron > /dev/null 2>&1 Die Antwort ist ein JSON-Objekt: {"expired": 3} – die Anzahl der gerade deaktivierten Links. Hinweis: Der Cron-Endpunkt ist nicht authentifiziert. Er führt jedoch nur eine Statusänderung durch (aktiv → inaktiv) und gibt keine sensiblen Daten zurück. Wenn Sie das absichern möchten, können Sie einen API-Key prüfen oder den Zugriff per .htaccess einschränken. Wichtig: Auch ohne Cron werden abgelaufene Links beim Aufrufen als „nicht verfügbar" angezeigt, da die Prüfung strtotime($link['expires_at']) <= time() direkt in der Routing-Logik stattfindet. Der Cron-Job sorgt lediglich dafür, dass der active-Status in der Datenbank korrekt gesetzt wird (relevant für die Dashboard-Anzeige). 12. Sicherheitshinweise Passwort in config.php Das Admin-Passwort in config.php steht im Klartext. Stellen Sie sicher, dass: Die Datei nicht über den Webserver direkt abrufbar ist Ein starkes Passwort verwendet wird (nicht das Standard- hackme123!) Datenbankschutz Die .htaccess blockiert den direkten Zugriff auf .db- und .sqlite-Dateien: <FilesMatch "\.db$"> Require all denied </FilesMatch> XSS-Schutz Alle Benutzereingaben werden in den Templates mit htmlspecialchars() escaped, z.B.: <?= htmlspecialchars($link['slug']) ?> SQL-Injection-Schutz Alle Datenbankabfragen verwenden Prepared Statements mit Paramter-Binding: $stmt = $db->prepare('SELECT * FROM links WHERE slug = ?'); $stmt->execute([$slug]); Die einzige Ausnahme ist die dynamische ORDER BY-Klausel in getAllLinks, die aber über ein Whitelist-Array abgesichert ist. CSRF-Schutz Aktuell gibt es keinen CSRF-Token-Schutz. Da die Anwendung nur einen einzigen Admin-User hat, ist das Risiko begrenzt, aber bei einer Erweiterung sollte ein Token-System implementiert werden. 13. Technische Architektur im Detail Routing-System Das Routing in index.php funktioniert als Kaskade von if-Statements: Eingang: $_GET['route'] (via .htaccess Rewrite) ↓ 1. Exakte Routen: 'cron', 'api/shorten', 'api/check', 'admin/action' ↓ 2. QR-Routen: 'qr/png', 'qr/svg', 'qr/png/download', 'qr/svg/download' ↓ 3. Slug-Auflösung: Beliebiger Pfad → getLinkBySlug() ↓ 4. Seiten-Routing: $_GET['page'] → login, dashboard, edit, stats, settings Datenbank-Schema links ( id INTEGER PRIMARY KEY AUTOINCREMENT, slug TEXT UNIQUE NOT NULL, -- Der Kurzlink-Code url TEXT NOT NULL, -- Ziel-URL password TEXT DEFAULT NULL, -- bcrypt-Hash oder NULL expires_at TEXT DEFAULT NULL, -- ISO-8601 Ablaufzeit max_clicks INTEGER DEFAULT NULL, -- Klick-Limit clicks INTEGER DEFAULT 0, -- Aktueller Zähler active INTEGER DEFAULT 1, -- 1 = aktiv, 0 = deaktiviert og_title TEXT, -- OpenGraph-Cache og_description TEXT, og_image TEXT, created_at TEXT, -- datetime('now') updated_at TEXT ) clicks ( id INTEGER PRIMARY KEY AUTOINCREMENT, link_id INTEGER NOT NULL, -- FK → links.id ip TEXT, user_agent TEXT, referer TEXT, clicked_at TEXT -- datetime('now') FOREIGN KEY (link_id) REFERENCES links(id) ON DELETE CASCADE ) settings ( key TEXT PRIMARY KEY, value TEXT -- Aktuell nicht aktiv genutzt ) Frontend-JavaScript app.js ist bewusst minimal und framework-frei: Batch-Modal: Toggle der CSS-Klasse active auf dem Overlay QR-Modal: Dynamisches Setzen der src- und href-Attribute basierend auf dem Slug Slug-Preview: Live-Aktualisierung beim Tippen mit Regex-Filterung Copy-Button: Via navigator.clipboard.writeText() mit visueller Bestätigung Delete-Confirmation: Nativer confirm()-Dialog Performance-Überlegungen SQLite WAL-Modus: Ermöglicht gleichzeitige Leser während ein Schreibvorgang läuft QR-Code-Cache: Verhindert wiederholte Berechnung (24h TTL) OpenGraph-Cache in DB: Meta-Daten werden nur beim Erstellen/Ändern der URL abgerufen Statische Assets: CSS und JS werden direkt ausgeliefert (kein Build-Prozess nötig) Zusammenfassung LinkShorter v2 ist eine schlanke, selbstgehostete Lösung ohne externe Abhängigkeiten. Die bemerkenswerteste technische Leistung ist die vollständige QR-Code-Implementierung in reinem PHP, inklusive Reed-Solomon-Fehlerkorrektur und Galois-Feld-Arithmetik. Die Chrome-Extension ergänzt das System um einen komfortablen Workflow direkt aus dem Browser heraus, wobei die Multi-Instanz-Unterstützung besonders für Nutzer mit mehreren Domains nützlich ist.