$query * @param array $post * @param array $server * @param array $files * @param list $trustedProxies IPs/CIDR-Bereiche, deren * X-Forwarded-*-Header ausgewertet werden duerfen. */ public function __construct( public readonly string $method, public readonly string $path, public readonly array $query, public readonly array $post, public readonly array $server, public readonly array $files, public readonly array $trustedProxies = [], ) { } /** * @param list $trustedProxies */ public static function fromGlobals(array $trustedProxies = []): self { $method = strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')); // Der Pfad kommt bevorzugt aus REQUEST_URI; das Basisverzeichnis // (Unterordner-Installation) wird abgeschnitten. $uri = (string) ($_SERVER['REQUEST_URI'] ?? '/'); $path = parse_url($uri, PHP_URL_PATH); $path = is_string($path) ? $path : '/'; $path = self::stripBasePath(rawurldecode($path), $_SERVER); return new self($method, $path, $_GET, $_POST, $_SERVER, $_FILES, $trustedProxies); } /** * Entfernt das Unterverzeichnis, in dem die Anwendung liegt * (z. B. /kurz/abc -> /abc bei Installation in /kurz). * * @param array $server */ public static function stripBasePath(string $path, array $server): string { $base = BasePath::detect($server); if ($base !== '' && str_starts_with($path, $base . '/')) { $path = substr($path, strlen($base)); } elseif ($base !== '' && $path === $base) { $path = '/'; } // Variante ohne eigenen Webroot: /public/... aus der Anfrage nehmen. if (str_starts_with($path, '/public/')) { $path = substr($path, strlen('/public')); } elseif ($path === '/public') { $path = '/'; } $path = '/' . ltrim($path, '/'); return $path === '/' ? '/' : rtrim($path, '/'); } public function isPost(): bool { return $this->method === 'POST'; } public function post(string $key, string $default = ''): string { $value = $this->post[$key] ?? null; return is_scalar($value) ? trim((string) $value) : $default; } public function query(string $key, string $default = ''): string { $value = $this->query[$key] ?? null; return is_scalar($value) ? trim((string) $value) : $default; } public function queryInt(string $key, int $default = 0): int { $value = $this->query($key); return is_numeric($value) ? (int) $value : $default; } public function hasPost(string $key): bool { return array_key_exists($key, $this->post); } public function header(string $name): ?string { $key = 'HTTP_' . str_replace('-', '_', strtoupper($name)); $value = $this->server[$key] ?? null; return is_string($value) ? $value : null; } public function userAgent(): string { return (string) ($this->server['HTTP_USER_AGENT'] ?? ''); } public function referrer(): string { return (string) ($this->server['HTTP_REFERER'] ?? ''); } /** * Client-IP. Wird ausschliesslich fluechtig verwendet * (Rate-Limiting, optionaler Hash) und nie im Klartext gespeichert. * * X-Forwarded-For ist frei setzbar und wird deshalb NUR ausgewertet, * wenn die Anfrage von einem als vertrauenswuerdig konfigurierten * Proxy stammt (TRUSTED_PROXIES). Ohne diese Einstellung zaehlt * allein REMOTE_ADDR - sonst liesse sich die Sperre nach zu vielen * Fehlanmeldungen durch einen erfundenen Header umgehen. */ public function clientIp(): string { $remote = (string) ($this->server['REMOTE_ADDR'] ?? ''); $remote = filter_var($remote, FILTER_VALIDATE_IP) !== false ? $remote : '0.0.0.0'; if (!$this->isFromTrustedProxy()) { return $remote; } $forwarded = (string) ($this->server['HTTP_X_FORWARDED_FOR'] ?? ''); if ($forwarded === '') { return $remote; } // Von rechts nach links: der letzte Eintrag, der nicht selbst // ein bekannter Proxy ist, ist der Client. foreach (array_reverse(explode(',', $forwarded)) as $candidate) { $candidate = self::stripPort(trim($candidate)); if (filter_var($candidate, FILTER_VALIDATE_IP) === false) { return $remote; } if (!IpRange::matchesAny($candidate, $this->trustedProxies)) { return $candidate; } } return $remote; } public function isFromTrustedProxy(): bool { if ($this->trustedProxies === []) { return false; } $remote = (string) ($this->server['REMOTE_ADDR'] ?? ''); return $remote !== '' && IpRange::matchesAny($remote, $this->trustedProxies); } /** * Laeuft die Anfrage ueber HTTPS? X-Forwarded-Proto wird nur hinter * einem vertrauenswuerdigen Proxy beruecksichtigt. */ public function isSecure(): bool { $https = $this->server['HTTPS'] ?? ''; if (is_string($https) && $https !== '' && strtolower($https) !== 'off') { return true; } if ((string) ($this->server['SERVER_PORT'] ?? '') === '443') { return true; } if ($this->isFromTrustedProxy()) { $proto = strtolower(trim((string) ($this->server['HTTP_X_FORWARDED_PROTO'] ?? ''))); return $proto === 'https'; } return false; } /** * Host-Header, aber nur wenn er wie ein Hostname aussieht. * Verhindert, dass ein erfundener Host in erzeugte URLs gelangt. */ public function host(): string { $host = strtolower(trim((string) ($this->server['HTTP_HOST'] ?? ''))); if ($host === '' || strlen($host) > 255) { return ''; } // Erlaubt: name(.name)*(:port) sowie IPv4 und IPv6 in Klammern. $pattern = '/^(?:\[[0-9a-f:.]+\]|[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?)(?::\d{1,5})?$/'; return preg_match($pattern, $host) === 1 ? $host : ''; } private static function stripPort(string $value): string { if (str_starts_with($value, '[')) { $end = strpos($value, ']'); return $end === false ? $value : substr($value, 1, $end - 1); } // Nur abschneiden, wenn genau ein Doppelpunkt vorkommt // (sonst ist es eine IPv6-Adresse ohne Klammern). return substr_count($value, ':') === 1 ? explode(':', $value)[0] : $value; } }