feat(links): Kurzlinks, Weiterleitung und Klickstatistik
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.
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Database;
|
||||
|
||||
/**
|
||||
* Key-Value-Speicher fuer Einstellungen, die zur Laufzeit gesetzt werden
|
||||
* (z. B. das Admin-Passwort aus dem Setup-Assistenten).
|
||||
*/
|
||||
final class SettingRepository
|
||||
{
|
||||
public const ADMIN_USER = 'admin_user';
|
||||
public const ADMIN_PASSWORD_HASH = 'admin_password_hash';
|
||||
public const APP_SECRET = 'app_secret';
|
||||
|
||||
public function __construct(private readonly Database $db)
|
||||
{
|
||||
}
|
||||
|
||||
public function get(string $key, ?string $default = null): ?string
|
||||
{
|
||||
$row = $this->db->fetchOne('SELECT value FROM settings WHERE key_name = ?', [$key]);
|
||||
|
||||
if ($row === null || $row['value'] === null || $row['value'] === '') {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return (string) $row['value'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Installationsgeheimnis fuer signierte Tokens. Wird beim ersten
|
||||
* Bedarf erzeugt und bleibt danach unveraendert.
|
||||
*/
|
||||
public function secret(): string
|
||||
{
|
||||
$secret = $this->get(self::APP_SECRET);
|
||||
|
||||
if ($secret === null || strlen($secret) < 32) {
|
||||
$secret = bin2hex(random_bytes(32));
|
||||
$this->set(self::APP_SECRET, $secret);
|
||||
}
|
||||
|
||||
return $secret;
|
||||
}
|
||||
|
||||
public function set(string $key, string $value): void
|
||||
{
|
||||
$now = gmdate('Y-m-d H:i:s');
|
||||
|
||||
$exists = $this->db->fetchValue('SELECT 1 FROM settings WHERE key_name = ?', [$key]);
|
||||
|
||||
if ($exists !== null) {
|
||||
$this->db->execute(
|
||||
'UPDATE settings SET value = ?, updated_at = ? WHERE key_name = ?',
|
||||
[$value, $now, $key],
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->db->execute(
|
||||
'INSERT INTO settings (key_name, value, updated_at) VALUES (?, ?, ?)',
|
||||
[$key, $value, $now],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user