`. */ class ContentSecurityPolicy { public const REPORT_PATH = '/csp-report'; public static function header(): string { $own = array_merge(["'self'"], self::ownOrigins()); $umami = config('services.umami.enabled') ? self::originOf((string) config('services.umami.script_url')) : null; $errors = self::originOf((string) config('monitoring.browser.dsn')); // Cloudflare Turnstile (Spamschutz): Skript und Challenge-iframe. $turnstile = 'https://challenges.cloudflare.com'; $directives = [ 'default-src' => ["'self'"], 'script-src' => [...$own, "'unsafe-inline'", "'unsafe-eval'", $umami, $turnstile], 'style-src' => [...$own, "'unsafe-inline'"], 'img-src' => [...$own, 'data:', 'blob:', 'https:'], 'font-src' => [...$own, 'data:'], 'connect-src' => [...$own, $umami, $errors], 'media-src' => [...$own], 'object-src' => ["'none'"], 'frame-src' => ["'self'", $turnstile], 'frame-ancestors' => ["'self'"], 'base-uri' => ["'self'"], 'form-action' => [...$own, 'https://accounts.google.com', 'https://checkout.stripe.com', 'https://billing.stripe.com'], 'report-uri' => [self::REPORT_PATH], ]; return collect($directives) ->map(fn (array $sources, string $directive): string => trim($directive.' '.implode(' ', array_unique(array_filter($sources))))) ->implode('; '); } /** * Die drei Domains der Anwendung — Formulare und Assets können * domainübergreifend auf die anderen zeigen. * * @return list */ private static function ownOrigins(): array { return collect([ config('domains.domain_portal'), config('domains.domain_presseecho'), config('domains.domain_businessportal'), ])->filter()->map(fn (string $host): string => 'https://'.$host)->values()->all(); } private static function originOf(string $url): ?string { $parts = parse_url($url); if (! is_array($parts) || empty($parts['host'])) { return null; } return ($parts['scheme'] ?? 'https').'://'.$parts['host'].(isset($parts['port']) ? ':'.$parts['port'] : ''); } }