feat(domains): Mehrere Kurzlink-Domains je Installation
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.
This commit is contained in:
@@ -0,0 +1,336 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Config;
|
||||
use App\Http\Request;
|
||||
use App\Http\Response;
|
||||
use App\Repository\DomainRepository;
|
||||
use App\Repository\SettingRepository;
|
||||
use App\Security\Auth;
|
||||
use App\Security\Csrf;
|
||||
use App\Security\Session;
|
||||
use App\Service\DomainChecker;
|
||||
use App\Support\CheckToken;
|
||||
use App\Support\ShortDomain;
|
||||
use App\View;
|
||||
|
||||
/**
|
||||
* Verwaltung der Kurzlink-Domains.
|
||||
*/
|
||||
final class DomainController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
Config $config,
|
||||
View $view,
|
||||
private readonly Session $session,
|
||||
private readonly Csrf $csrf,
|
||||
private readonly Auth $auth,
|
||||
private readonly DomainRepository $domains,
|
||||
private readonly DomainChecker $checker,
|
||||
private readonly SettingRepository $settings,
|
||||
) {
|
||||
parent::__construct($config, $view);
|
||||
}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
if (($response = $this->requireAuth($this->auth, $this->session, $request)) !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
// Stellt sicher, dass die Standarddomain existiert.
|
||||
$this->domains->defaultDomain($this->config->baseUrl());
|
||||
|
||||
// Ergebnis einer eben angestossenen Pruefung einmalig anzeigen.
|
||||
$checkResult = $this->session->get('_domain_check');
|
||||
$this->session->remove('_domain_check');
|
||||
|
||||
return $this->html('domains/index', [
|
||||
'domains' => $this->domains->all(),
|
||||
'requestHost' => ShortDomain::normalizeRequestHost((string) ($request->server['HTTP_HOST'] ?? '')),
|
||||
'checkResult' => is_array($checkResult) ? $checkResult : null,
|
||||
'pageTitle' => 'Domains',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prueft, ob die Domain tatsaechlich bei dieser Installation ankommt.
|
||||
*
|
||||
* @param array<string,string> $args
|
||||
*/
|
||||
public function check(Request $request, array $args = []): Response
|
||||
{
|
||||
if (($response = $this->requireAuth($this->auth, $this->session, $request)) !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
if (($response = $this->requireCsrf($this->csrf, $request)) !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$domain = $this->domains->find((int) ($args['id'] ?? 0));
|
||||
if ($domain === null) {
|
||||
return $this->domainNotFound();
|
||||
}
|
||||
|
||||
$this->session->set('_domain_check', [
|
||||
'domain_id' => (int) $domain['id'],
|
||||
'domain' => ShortDomain::displayName((string) $domain['base_url']),
|
||||
'result' => $this->checker->check($domain),
|
||||
]);
|
||||
|
||||
return $this->redirect('/admin/domains');
|
||||
}
|
||||
|
||||
/**
|
||||
* Oeffentlicher Endpunkt, den die Pruefung von aussen aufruft.
|
||||
*
|
||||
* Ohne gueltiges, kurzlebiges Token antwortet er wie eine beliebige
|
||||
* unbekannte Adresse - es wird also nichts ueber die Installation
|
||||
* preisgegeben. Kommt ein gueltiges Token an, ist damit bewiesen,
|
||||
* dass der Aufruf hier gelandet ist.
|
||||
*/
|
||||
public function verify(Request $request): Response
|
||||
{
|
||||
if (!CheckToken::isValid($request->query('token'), $this->settings->secret())) {
|
||||
return Response::html('Not Found', 404);
|
||||
}
|
||||
|
||||
return Response::json(['ok' => true])
|
||||
->withHeader('Cache-Control', 'no-store');
|
||||
}
|
||||
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
if (($response = $this->requireAuth($this->auth, $this->session, $request)) !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
return $this->html('domains/form', [
|
||||
'domain' => null,
|
||||
'values' => ['base_url' => '', 'label' => '', 'is_default' => '0'],
|
||||
'errors' => [],
|
||||
'pageTitle' => 'Neue Domain',
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): Response
|
||||
{
|
||||
if (($response = $this->requireAuth($this->auth, $this->session, $request)) !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
if (($response = $this->requireCsrf($this->csrf, $request)) !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$values = $this->readValues($request);
|
||||
$baseUrl = ShortDomain::normalizeBaseUrl($values['base_url']);
|
||||
$errors = $this->validate($baseUrl);
|
||||
|
||||
if ($errors === [] && $this->domains->hostExists(ShortDomain::hostOf($baseUrl))) {
|
||||
$errors['base_url'] = 'Diese Domain ist bereits eingetragen.';
|
||||
}
|
||||
|
||||
if ($errors !== []) {
|
||||
return $this->html('domains/form', [
|
||||
'domain' => null,
|
||||
'values' => $values,
|
||||
'errors' => $errors,
|
||||
'pageTitle' => 'Neue Domain',
|
||||
], 422);
|
||||
}
|
||||
|
||||
// Die erste Domain ist zwangslaeufig die Standarddomain.
|
||||
$isDefault = $values['is_default'] === '1' || $this->domains->count() === 0;
|
||||
$this->domains->create($baseUrl, $values['label'] !== '' ? $values['label'] : null, $isDefault);
|
||||
|
||||
$this->session->flash('success', sprintf(
|
||||
'Domain "%s" wurde angelegt. Bitte daran denken, sie im DNS und beim Webserver auf diese Installation zu zeigen.',
|
||||
ShortDomain::displayName($baseUrl),
|
||||
));
|
||||
|
||||
return $this->redirect('/admin/domains');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,string> $args
|
||||
*/
|
||||
public function edit(Request $request, array $args = []): Response
|
||||
{
|
||||
if (($response = $this->requireAuth($this->auth, $this->session, $request)) !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$domain = $this->domains->find((int) ($args['id'] ?? 0));
|
||||
if ($domain === null) {
|
||||
return $this->domainNotFound();
|
||||
}
|
||||
|
||||
return $this->html('domains/form', [
|
||||
'domain' => $domain,
|
||||
'values' => [
|
||||
'base_url' => (string) $domain['base_url'],
|
||||
'label' => (string) ($domain['label'] ?? ''),
|
||||
'is_default' => ((int) $domain['is_default'] === 1) ? '1' : '0',
|
||||
],
|
||||
'errors' => [],
|
||||
'pageTitle' => 'Domain bearbeiten',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,string> $args
|
||||
*/
|
||||
public function update(Request $request, array $args = []): Response
|
||||
{
|
||||
if (($response = $this->requireAuth($this->auth, $this->session, $request)) !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
if (($response = $this->requireCsrf($this->csrf, $request)) !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$id = (int) ($args['id'] ?? 0);
|
||||
$domain = $this->domains->find($id);
|
||||
if ($domain === null) {
|
||||
return $this->domainNotFound();
|
||||
}
|
||||
|
||||
$values = $this->readValues($request);
|
||||
$baseUrl = ShortDomain::normalizeBaseUrl($values['base_url']);
|
||||
$errors = $this->validate($baseUrl);
|
||||
|
||||
if ($errors === [] && $this->domains->hostExists(ShortDomain::hostOf($baseUrl), $id)) {
|
||||
$errors['base_url'] = 'Diese Domain ist bereits eingetragen.';
|
||||
}
|
||||
|
||||
if ($errors !== []) {
|
||||
return $this->html('domains/form', [
|
||||
'domain' => $domain,
|
||||
'values' => $values,
|
||||
'errors' => $errors,
|
||||
'pageTitle' => 'Domain bearbeiten',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$this->domains->update($id, $baseUrl, $values['label'] !== '' ? $values['label'] : null);
|
||||
|
||||
if ($values['is_default'] === '1') {
|
||||
$this->domains->makeDefault($id);
|
||||
}
|
||||
|
||||
$this->session->flash('success', 'Die Domain wurde gespeichert.');
|
||||
|
||||
return $this->redirect('/admin/domains');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,string> $args
|
||||
*/
|
||||
public function makeDefault(Request $request, array $args = []): Response
|
||||
{
|
||||
if (($response = $this->requireAuth($this->auth, $this->session, $request)) !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
if (($response = $this->requireCsrf($this->csrf, $request)) !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$domain = $this->domains->find((int) ($args['id'] ?? 0));
|
||||
if ($domain === null) {
|
||||
return $this->domainNotFound();
|
||||
}
|
||||
|
||||
$this->domains->makeDefault((int) $domain['id']);
|
||||
$this->session->flash('success', sprintf(
|
||||
'"%s" ist jetzt die Standarddomain.',
|
||||
ShortDomain::displayName((string) $domain['base_url']),
|
||||
));
|
||||
|
||||
return $this->redirect('/admin/domains');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,string> $args
|
||||
*/
|
||||
public function destroy(Request $request, array $args = []): Response
|
||||
{
|
||||
if (($response = $this->requireAuth($this->auth, $this->session, $request)) !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
if (($response = $this->requireCsrf($this->csrf, $request)) !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$domain = $this->domains->find((int) ($args['id'] ?? 0));
|
||||
if ($domain === null) {
|
||||
return $this->domainNotFound();
|
||||
}
|
||||
|
||||
$id = (int) $domain['id'];
|
||||
$name = ShortDomain::displayName((string) $domain['base_url']);
|
||||
|
||||
if ((int) $domain['is_default'] === 1) {
|
||||
$this->session->flash(
|
||||
'error',
|
||||
'Die Standarddomain kann nicht gelöscht werden. Bitte zuerst eine andere Domain als Standard festlegen.',
|
||||
);
|
||||
|
||||
return $this->redirect('/admin/domains');
|
||||
}
|
||||
|
||||
$linkCount = $this->domains->linkCount($id);
|
||||
if ($linkCount > 0) {
|
||||
$this->session->flash('error', sprintf(
|
||||
'An "%s" hängen noch %d Kurzlink(s). Bitte diese zuerst löschen oder auf eine andere Domain umstellen.',
|
||||
$name,
|
||||
$linkCount,
|
||||
));
|
||||
|
||||
return $this->redirect('/admin/domains');
|
||||
}
|
||||
|
||||
$this->domains->delete($id);
|
||||
$this->session->flash('success', sprintf('Domain "%s" wurde gelöscht.', $name));
|
||||
|
||||
return $this->redirect('/admin/domains');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,string>
|
||||
*/
|
||||
private function validate(string $baseUrl): array
|
||||
{
|
||||
$error = ShortDomain::validationError($baseUrl);
|
||||
|
||||
return $error === null ? [] : ['base_url' => $error];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{base_url:string,label:string,is_default:string}
|
||||
*/
|
||||
private function readValues(Request $request): array
|
||||
{
|
||||
return [
|
||||
'base_url' => $request->post('base_url'),
|
||||
'label' => mb_substr($request->post('label'), 0, 255),
|
||||
'is_default' => $request->hasPost('is_default') ? '1' : '0',
|
||||
];
|
||||
}
|
||||
|
||||
private function domainNotFound(): Response
|
||||
{
|
||||
return $this->html('errors/error', [
|
||||
'title' => 'Domain nicht gefunden',
|
||||
'message' => 'Die gewünschte Domain existiert nicht (mehr).',
|
||||
'pageTitle' => 'Nicht gefunden',
|
||||
], 404);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Database;
|
||||
use App\Support\ShortDomain;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Die Domains, unter denen Kurzlinks erreichbar sind.
|
||||
*
|
||||
* Es gibt immer genau eine Standarddomain. Sie wird beim ersten Zugriff
|
||||
* aus APP_BASE_URL erzeugt; Bestandslinks (domain_id = 0) werden dabei
|
||||
* einmalig nachgetragen.
|
||||
*/
|
||||
final class DomainRepository
|
||||
{
|
||||
/** Kennung fuer noch nicht zugeordnete Bestandslinks. */
|
||||
public const UNASSIGNED = 0;
|
||||
|
||||
public function __construct(private readonly Database $db)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string,mixed>>
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
return $this->db->fetchAll(
|
||||
'SELECT d.*,
|
||||
(SELECT COUNT(*) FROM links l WHERE l.domain_id = d.id) AS link_count
|
||||
FROM domains d
|
||||
ORDER BY d.is_default DESC, d.host ASC',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>|null
|
||||
*/
|
||||
public function find(int $id): ?array
|
||||
{
|
||||
return $this->db->fetchOne('SELECT * FROM domains WHERE id = ?', [$id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>|null
|
||||
*/
|
||||
public function findByHost(string $host): ?array
|
||||
{
|
||||
$host = ShortDomain::normalizeRequestHost($host);
|
||||
if ($host === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->db->fetchOne('SELECT * FROM domains WHERE host = ?', [$host]);
|
||||
}
|
||||
|
||||
public function hostExists(string $host, ?int $exceptId = null): bool
|
||||
{
|
||||
$host = ShortDomain::normalizeRequestHost($host);
|
||||
|
||||
if ($exceptId === null) {
|
||||
return $this->db->fetchValue('SELECT 1 FROM domains WHERE host = ?', [$host]) !== null;
|
||||
}
|
||||
|
||||
return $this->db->fetchValue(
|
||||
'SELECT 1 FROM domains WHERE host = ? AND id <> ?',
|
||||
[$host, $exceptId],
|
||||
) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standarddomain. Legt sie beim allerersten Aufruf aus der
|
||||
* konfigurierten Basis-URL an.
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function defaultDomain(string $fallbackBaseUrl): array
|
||||
{
|
||||
$domain = $this->db->fetchOne('SELECT * FROM domains WHERE is_default = 1 ORDER BY id ASC LIMIT 1');
|
||||
if ($domain !== null) {
|
||||
return $domain;
|
||||
}
|
||||
|
||||
// Es gibt Domains, aber keine ist als Standard markiert.
|
||||
$domain = $this->db->fetchOne('SELECT * FROM domains ORDER BY id ASC LIMIT 1');
|
||||
if ($domain !== null) {
|
||||
$this->makeDefault((int) $domain['id']);
|
||||
|
||||
return $this->find((int) $domain['id']) ?? $domain;
|
||||
}
|
||||
|
||||
return $this->seedFromBaseUrl($fallbackBaseUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loest die Domain einer eingehenden Anfrage auf. Unbekannte Hosts
|
||||
* landen auf der Standarddomain - so funktioniert die Anwendung auch
|
||||
* beim Aufruf per IP-Adresse oder hinter einem Proxy.
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function resolveForRequest(string $requestHost, string $fallbackBaseUrl): array
|
||||
{
|
||||
return $this->findByHost($requestHost) ?? $this->defaultDomain($fallbackBaseUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function seedFromBaseUrl(string $baseUrl): array
|
||||
{
|
||||
$normalized = ShortDomain::normalizeBaseUrl($baseUrl);
|
||||
$host = ShortDomain::hostOf($normalized);
|
||||
|
||||
if ($host === '') {
|
||||
throw new RuntimeException(
|
||||
'Aus APP_BASE_URL laesst sich keine gueltige Domain ableiten: "' . $baseUrl . '".',
|
||||
);
|
||||
}
|
||||
|
||||
$id = $this->create($normalized, null, true);
|
||||
|
||||
// Bestandslinks der Standarddomain zuordnen.
|
||||
$this->db->execute(
|
||||
'UPDATE links SET domain_id = ? WHERE domain_id = ?',
|
||||
[$id, self::UNASSIGNED],
|
||||
);
|
||||
|
||||
return $this->find($id) ?? throw new RuntimeException('Standarddomain konnte nicht angelegt werden.');
|
||||
}
|
||||
|
||||
public function create(string $baseUrl, ?string $label, bool $isDefault): int
|
||||
{
|
||||
$baseUrl = ShortDomain::normalizeBaseUrl($baseUrl);
|
||||
$host = ShortDomain::hostOf($baseUrl);
|
||||
|
||||
$this->db->execute(
|
||||
'INSERT INTO domains (host, base_url, label, is_default, created_at) VALUES (?, ?, ?, ?, ?)',
|
||||
[$host, $baseUrl, $label, $isDefault ? 1 : 0, gmdate('Y-m-d H:i:s')],
|
||||
);
|
||||
|
||||
$id = $this->db->lastInsertId();
|
||||
|
||||
if ($isDefault) {
|
||||
$this->makeDefault($id);
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
public function update(int $id, string $baseUrl, ?string $label): void
|
||||
{
|
||||
$baseUrl = ShortDomain::normalizeBaseUrl($baseUrl);
|
||||
$host = ShortDomain::hostOf($baseUrl);
|
||||
|
||||
$this->db->execute(
|
||||
'UPDATE domains SET host = ?, base_url = ?, label = ? WHERE id = ?',
|
||||
[$host, $baseUrl, $label, $id],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Markiert genau eine Domain als Standard.
|
||||
*/
|
||||
public function makeDefault(int $id): void
|
||||
{
|
||||
$this->db->execute('UPDATE domains SET is_default = 0 WHERE id <> ?', [$id]);
|
||||
$this->db->execute('UPDATE domains SET is_default = 1 WHERE id = ?', [$id]);
|
||||
}
|
||||
|
||||
public function linkCount(int $id): int
|
||||
{
|
||||
return (int) $this->db->fetchValue('SELECT COUNT(*) FROM links WHERE domain_id = ?', [$id]);
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
return (int) $this->db->fetchValue('SELECT COUNT(*) FROM domains');
|
||||
}
|
||||
|
||||
public function delete(int $id): void
|
||||
{
|
||||
$this->db->execute('DELETE FROM domains WHERE id = ?', [$id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut die Kurz-URL eines Links.
|
||||
*
|
||||
* @param array<string,mixed> $domain
|
||||
*/
|
||||
public static function shortUrl(array $domain, string $slug): string
|
||||
{
|
||||
return rtrim((string) $domain['base_url'], '/') . '/' . $slug;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
/**
|
||||
* Hilfsfunktionen fuer die Domains, unter denen Kurzlinks ausgeliefert
|
||||
* werden (z. B. "short.example.de" und "qr.example.de").
|
||||
*
|
||||
* Der Host enthaelt den Port, sofern einer angegeben ist - genau wie der
|
||||
* Host-Header der Anfrage. Dadurch laesst sich beides direkt vergleichen.
|
||||
*/
|
||||
final class ShortDomain
|
||||
{
|
||||
public const MAX_BASE_URL_LENGTH = 255;
|
||||
|
||||
/**
|
||||
* Bereitet eine Eingabe wie "qr.example.de" oder
|
||||
* "https://qr.example.de/" zu einer Basis-URL ohne Schraegstrich
|
||||
* am Ende auf.
|
||||
*/
|
||||
public static function normalizeBaseUrl(string $input): string
|
||||
{
|
||||
$input = trim($input);
|
||||
if ($input === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!preg_match('#^[a-zA-Z][a-zA-Z0-9+.-]*://#', $input)) {
|
||||
$input = 'https://' . ltrim($input, '/');
|
||||
}
|
||||
|
||||
$normalized = UrlValidator::normalize($input);
|
||||
|
||||
return rtrim($normalized, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert den Hostanteil inklusive Port, in Kleinbuchstaben.
|
||||
*/
|
||||
public static function hostOf(string $baseUrl): string
|
||||
{
|
||||
if (preg_match('#^[a-zA-Z][a-zA-Z0-9+.-]*://([^/?\#]+)#', trim($baseUrl), $matches) !== 1) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$authority = $matches[1];
|
||||
|
||||
// Benutzerinfo gehoert nicht zum Host.
|
||||
$atPosition = strrpos($authority, '@');
|
||||
if ($atPosition !== false) {
|
||||
$authority = substr($authority, $atPosition + 1);
|
||||
}
|
||||
|
||||
return mb_strtolower($authority, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalisiert den Host-Header einer Anfrage fuer den Vergleich.
|
||||
*/
|
||||
public static function normalizeRequestHost(string $host): string
|
||||
{
|
||||
$host = mb_strtolower(trim($host), 'UTF-8');
|
||||
|
||||
// Standardports weglassen, damit "example.de:443" und
|
||||
// "example.de" dieselbe Domain treffen.
|
||||
return (string) preg_replace('/:(80|443)$/', '', $host);
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert eine Fehlermeldung oder null, wenn die Basis-URL taugt.
|
||||
*/
|
||||
public static function validationError(string $baseUrl): ?string
|
||||
{
|
||||
if (trim($baseUrl) === '') {
|
||||
return 'Bitte eine Domain angeben.';
|
||||
}
|
||||
|
||||
if (strlen($baseUrl) > self::MAX_BASE_URL_LENGTH) {
|
||||
return sprintf('Die Domain darf hoechstens %d Zeichen lang sein.', self::MAX_BASE_URL_LENGTH);
|
||||
}
|
||||
|
||||
$error = UrlValidator::validationError($baseUrl);
|
||||
if ($error !== null) {
|
||||
return $error;
|
||||
}
|
||||
|
||||
if (self::hostOf($baseUrl) === '') {
|
||||
return 'Die Domain enthaelt keinen gueltigen Hostnamen.';
|
||||
}
|
||||
|
||||
// Query und Fragment ergeben fuer eine Basis-URL keinen Sinn.
|
||||
if (str_contains($baseUrl, '?') || str_contains($baseUrl, '#')) {
|
||||
return 'Die Domain darf keine Parameter (? oder #) enthalten.';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Anzeigename: Schema weglassen, wenn es https ist.
|
||||
*/
|
||||
public static function displayName(string $baseUrl): string
|
||||
{
|
||||
return (string) preg_replace('#^https://#', '', $baseUrl);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user