resolvePublishedRelease($slug); $type = LegalRequestType::tryFrom((string) $request->query('type')) ?? LegalRequestType::Report; return view('web.legal-request', [ 'release' => $release, 'type' => $type, 'types' => LegalRequestType::cases(), ]); } public function store(Request $request, string $slug): RedirectResponse { $release = $this->resolvePublishedRelease($slug); $validated = $request->validate([ 'type' => ['required', Rule::enum(LegalRequestType::class)], // E-Mail für DSGVO/Persönlichkeitsrecht nötig (Rückmeldung); bei // einer Meldung optional. 'requester_email' => [ Rule::requiredIf(fn () => in_array($request->input('type'), [ LegalRequestType::Dsgvo->value, LegalRequestType::PersonalRights->value, ], true)), 'nullable', 'email', 'max:255', ], 'message' => ['required', 'string', 'min:10', 'max:5000'], // Honeypots: müssen leer bleiben (versteckte Felder, nur für Bots). 'website' => ['nullable', 'size:0'], 'homepage' => ['nullable', 'size:0'], ]); $type = LegalRequestType::from($validated['type']); // Bot: ein ausgefülltes Honeypot-Feld → still und neutral abnicken, // nichts speichern (keine Fehlermeldung, die einem Bot etwas verrät). if (filled($request->input('website')) || filled($request->input('homepage'))) { return $this->done($release); } $this->ensureNotRateLimited($release); // Bremse: max. 1 offene Anfrage pro PM + Typ (nicht hart gesperrt – andere // Typen und spätere Anfragen nach Erledigung bleiben möglich). $hasOpen = LegalRequest::query() ->where('press_release_id', $release->getKey()) ->where('type', $type->value) ->open() ->exists(); if ($hasOpen) { return back()->with('legal-status', __('Zu dieser Pressemitteilung liegt bereits eine offene Anfrage dieser Art vor. Wir bearbeiten sie schnellstmöglich.')); } RateLimiter::hit($this->throttleKey($release), 3600); LegalRequest::query()->create([ 'press_release_id' => $release->getKey(), 'portal' => $release->portal?->value, 'type' => $type->value, 'status' => LegalRequestStatus::Open->value, 'requester_email' => $validated['requester_email'] ?? null, 'message' => $validated['message'], 'requester_ip' => $request->ip(), ]); return $this->done($release); } private function done(PressRelease $release): RedirectResponse { return redirect() ->route('release.detail', ['slug' => $release->slug]) ->with('legal-status', __('Vielen Dank. Ihre Anfrage ist eingegangen und wird von unserem Team geprüft.')); } private function resolvePublishedRelease(string $slug): PressRelease { $portal = str_contains(request()->getHost(), 'presseecho') ? Portal::Presseecho : Portal::Businessportal24; $release = PressRelease::query() ->withoutGlobalScope(PortalScope::class) ->whereIn('portal', [$portal->value, Portal::Both->value]) ->where('status', PressReleaseStatus::Published) ->where('language', 'de') ->whereNotNull('published_at') ->where('published_at', '<=', now()) ->where('slug', $slug) ->first(); abort_if($release === null, 404); return $release; } private function ensureNotRateLimited(PressRelease $release): void { if (! RateLimiter::tooManyAttempts($this->throttleKey($release), 5)) { return; } $seconds = RateLimiter::availableIn($this->throttleKey($release)); throw ValidationException::withMessages([ 'message' => __('Zu viele Anfragen. Bitte versuchen Sie es in :minutes Minuten erneut.', [ 'minutes' => max(1, (int) ceil($seconds / 60)), ]), ]); } private function throttleKey(PressRelease $release): string { return 'legal-request|'.request()->ip().'|'.$release->getKey(); } }