*/ public function all(): array { return array_map(static fn (array $row): array => [ 'id' => $row['id'], 'from' => $row['from_path'], 'to' => $row['to_path'], 'status' => (int) $row['status'], ], $this->db->fetchAllAssociative('SELECT * FROM redirect WHERE website_id = ? ORDER BY from_path', ['default'])); } /** @return array{to: ?string, status: int}|null */ public function find(string $path): ?array { $row = $this->db->fetchAssociative('SELECT to_path, status FROM redirect WHERE website_id = ? AND from_path = ?', ['default', trim($path, '/')]); return false === $row ? null : ['to' => $row['to_path'], 'status' => (int) $row['status']]; } public function save(string $from, string $to, int $status): void { $from = trim(trim($from), '/'); $to = trim(trim($to), '/'); if (!in_array($status, [301, 410], true)) { throw new InvalidPage('Erlaubt sind 301 für eine verschobene Adresse und 410 für eine entfernte.'); } if (1 !== preg_match('#^[a-z0-9]+(?:[-/][a-z0-9]+)*$#D', $from) || strlen($from) > 400) { throw new InvalidPage('Die alte Adresse darf nur Kleinbuchstaben, Ziffern, Bindestriche und Schrägstriche enthalten.'); } if (301 === $status) { if (1 !== preg_match('#^[a-z0-9]+(?:[-/][a-z0-9]+)*$#D', $to) || strlen($to) > 400) { throw new InvalidPage('Das Ziel darf nur Kleinbuchstaben, Ziffern, Bindestriche und Schrägstriche enthalten.'); } if ($from === $to) { throw new InvalidPage('Eine Adresse kann nicht auf sich selbst weiterleiten.'); } // One hop only: a chain would cost an extra round trip and can become a loop. $next = $this->find($to); if (null !== $next) { throw new InvalidPage(sprintf('Das Ziel /%s leitet selbst weiter. Zeige direkt auf das Endziel.', $to)); } } if (self::MAX <= (int) $this->db->fetchOne('SELECT COUNT(*) FROM redirect WHERE website_id = ?', ['default']) && null === $this->find($from)) { throw new InvalidPage(sprintf('Mehr als %d Weiterleitungen sind nicht vorgesehen.', self::MAX)); } $this->db->transactional(function () use ($from, $to, $status): void { $this->db->executeStatement('DELETE FROM redirect WHERE website_id = ? AND from_path = ?', ['default', $from]); $this->db->insert('redirect', [ 'id' => bin2hex(random_bytes(16)), 'website_id' => 'default', 'from_path' => $from, 'to_path' => 301 === $status ? $to : null, 'status' => $status, 'created_at' => gmdate('Y-m-d H:i:s'), ]); }); } public function delete(string $id): void { $this->db->executeStatement('DELETE FROM redirect WHERE id = ? AND website_id = ?', [$id, 'default']); } /** Records that an address moved, used when a page is renamed. Never overwrites an entry. */ public function remember(string $from, string $to): void { if ($from === $to || null !== $this->find($from)) { return; } try { $this->save($from, $to, 301); } catch (InvalidPage) { // A path the rules reject is simply not remembered; renaming must not fail for it. } } }