Files
florian.wilde 583d9f0ae2 feat(links): Mehrteilige Slugs und erhaltene Gross-/Kleinschreibung
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.
2026-08-12 18:32:41 +02:00

125 lines
6.3 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* @var array<string,mixed>|null $link
* @var list<array<string,mixed>> $domains
* @var array{target_url:string,slug:string,title:string,is_active:string,domain_id:string} $values
* @var array<string,string> $errors
* @var App\Config $config
* @var App\Security\Csrf $csrf
*/
use App\Support\ShortDomain;
$isEdit = $link !== null;
$action = $isEdit ? $url('/admin/links/' . $link['id'] . '/edit') : $url('/admin/links/new');
$selectedId = (int) $values['domain_id'];
$selectedBase = $config->baseUrl();
foreach ($domains as $domain) {
if ((int) $domain['id'] === $selectedId) {
$selectedBase = (string) $domain['base_url'];
break;
}
}
?>
<div class="page-head">
<div>
<h1><?= $isEdit ? 'Kurzlink bearbeiten' : 'Neuer Kurzlink' ?></h1>
<?php if ($isEdit): ?>
<p class="muted mono"><?= $e(rtrim($selectedBase, '/') . '/' . $link['slug']) ?></p>
<?php else: ?>
<p class="muted">Ziel-URL eintragen – der Slug wird automatisch erzeugt, wenn du keinen angibst.</p>
<?php endif; ?>
</div>
<a href="<?= $e($url('/admin/links')) ?>" class="btn btn--ghost">Zurück zur Liste</a>
</div>
<div class="card card--form">
<form method="post" action="<?= $e($action) ?>" class="form">
<?= $csrf->field() ?>
<div class="field <?= isset($errors['target_url']) ? 'field--error' : '' ?>">
<label for="target_url">Ziel-URL <span class="req">*</span></label>
<input type="text" id="target_url" name="target_url" value="<?= $e($values['target_url']) ?>"
placeholder="https://www.beispiel.de/eine/lange/adresse" required autofocus
inputmode="url" spellcheck="false">
<p class="field__hint">
Nur <code>http://</code> und <code>https://</code>. Fehlt das Schema, wird <code>https://</code> ergänzt.
Umlaut-Domains (<code>müller.de</code>), Umlaute im Pfad, IPv6-Adressen sowie interne Hostnamen
ohne Punkt (<code>intranet</code>, <code>nas:5000</code>) sind zulässig.
</p>
<?php if (isset($errors['target_url'])): ?>
<p class="field__error"><?= $e($errors['target_url']) ?></p>
<?php endif; ?>
</div>
<div class="field <?= isset($errors['domain_id']) ? 'field--error' : '' ?>">
<label for="domain_id">Kurzlink-Domain</label>
<select id="domain_id" name="domain_id" data-domain-select>
<?php foreach ($domains as $domain): ?>
<option value="<?= (int) $domain['id'] ?>"
data-base-url="<?= $e(rtrim((string) $domain['base_url'], '/')) ?>"
<?= (int) $domain['id'] === $selectedId ? 'selected' : '' ?>>
<?= $e(ShortDomain::displayName((string) $domain['base_url'])) ?><?php
if (($domain['label'] ?? '') !== ''): ?> – <?= $e($domain['label']) ?><?php endif;
if ((int) $domain['is_default'] === 1): ?> (Standard)<?php endif; ?>
</option>
<?php endforeach; ?>
</select>
<p class="field__hint">
<a href="<?= $e($url('/admin/domains')) ?>">Domains verwalten</a> – derselbe Slug kann auf
verschiedenen Domains auf unterschiedliche Ziele zeigen.
</p>
<?php if (isset($errors['domain_id'])): ?>
<p class="field__error"><?= $e($errors['domain_id']) ?></p>
<?php endif; ?>
</div>
<div class="field <?= isset($errors['slug']) ? 'field--error' : '' ?>">
<label for="slug">Wunsch-Slug<?= $isEdit ? ' <span class="req">*</span>' : ' (optional)' ?></label>
<div class="input-prefix">
<span class="input-prefix__label mono" data-slug-prefix><?= $e(rtrim($selectedBase, '/')) ?>/</span>
<input type="text" id="slug" name="slug" value="<?= $e($values['slug']) ?>"
placeholder="<?= $isEdit ? '' : 'z. B. messe2026 oder g/1AEJ0X90ZmF (leer = zufällig)' ?>"
pattern="[A-Za-z0-9._~/-]*" spellcheck="false" autocapitalize="off" autocomplete="off"
<?= $isEdit ? 'required' : '' ?>>
</div>
<p class="field__hint">
Buchstaben, Ziffern, Bindestrich, Unterstrich und Punkt. Der Schrägstrich trennt
Abschnitte, sodass sich auch Adressen wie <code>g/1AEJ0X90ZmF</code> abbilden lassen –
praktisch, um bereits gedruckte QR-Codes aus einem anderen System zu übernehmen.
<strong>Groß- und Kleinschreibung bleibt erhalten</strong> und muss beim Anlegen
exakt stimmen; beim Aufruf wird sie zur Sicherheit auch ohne Beachtung erkannt.
</p>
<?php if (isset($errors['slug'])): ?>
<p class="field__error"><?= $e($errors['slug']) ?></p>
<?php endif; ?>
<?php if ($isEdit): ?>
<p class="field__hint warn">Achtung: Beim Ändern von Slug oder Domain funktionieren bereits
verteilte Links und gedruckte QR-Codes nicht mehr.</p>
<?php endif; ?>
</div>
<div class="field">
<label for="title">Interne Bezeichnung (optional)</label>
<input type="text" id="title" name="title" value="<?= $e($values['title']) ?>" maxlength="255"
placeholder="z. B. Flyer Messestand Halle 3">
<p class="field__hint">Nur zur eigenen Orientierung in der Liste – wird nirgends öffentlich angezeigt.</p>
</div>
<div class="field field--check">
<label class="check">
<input type="checkbox" name="is_active" value="1" <?= $values['is_active'] === '1' ? 'checked' : '' ?>>
<span>Kurzlink ist aktiv</span>
</label>
<p class="field__hint">Deaktivierte Links liefern eine 404-Seite aus, bleiben aber samt Statistik erhalten.</p>
</div>
<div class="form__actions">
<button type="submit" class="btn btn--primary"><?= $isEdit ? 'Änderungen speichern' : 'Kurzlink anlegen' ?></button>
<a href="<?= $e($url('/admin/links')) ?>" class="btn btn--ghost">Abbrechen</a>
</div>
</form>
</div>