$way]); } public function store(Request $request, string $edition, string $way): RedirectResponse { // Bot: ein ausgefülltes Honeypot-Feld → still und neutral abnicken, // bewusst VOR der Validierung (sonst verrät die Regel die Falle). if (filled($request->input('website')) || filled($request->input('homepage'))) { return back()->with('report-status', __('static.report_form.flash_honeypot')); } $validated = $request->validate([ 'reported_url' => ['required', 'url', 'max:500'], 'requester_name' => ['required', 'string', 'max:120'], 'requester_email' => ['required', 'email', 'max:255'], 'message' => ['required', 'string', 'min:20', 'max:5000'], // DSA Art. 16 Abs. 1 lit. d: Erklärung, dass die Meldung nach // bestem Wissen und Gewissen abgegeben wird (nur Weg 1). 'truth_affirmed' => $way === 'rechtswidrig' ? ['accepted'] : ['nullable'], ], [ 'truth_affirmed.accepted' => __('static.report_form.validation_truth'), ]); $this->ensureNotRateLimited($request); RateLimiter::hit($this->throttleKey($request), 3600); // Zuordnung zu einer PM nur, wenn die URL einer Detail-Adresse eines // Portals entspricht — sonst bleibt die Meldung ohne PM-Bezug in der // Queue (URL steht im payload). $release = $this->reportedUrls->resolve($validated['reported_url']); $legalRequest = LegalRequest::query()->create([ 'press_release_id' => $release?->getKey(), 'portal' => $release?->portal?->value ?? $this->portalFromHost($request), 'type' => $way === 'kontaktdaten' ? LegalRequestType::Dsgvo->value : LegalRequestType::Report->value, 'status' => LegalRequestStatus::Open->value, 'requester_email' => $validated['requester_email'], 'message' => $validated['message'], 'payload' => [ 'way' => $way, 'reported_url' => $validated['reported_url'], 'requester_name' => $validated['requester_name'], 'truth_affirmed' => $request->boolean('truth_affirmed'), ], 'requester_ip' => $request->ip(), ]); // Eingangsbestätigung (DSA Art. 16 Abs. 4) + interner Hinweis ans // Melde-Postfach. Versand über die Queue (afterCommit), damit die // Response nicht am SMTP-Roundtrip hängt — setzt einen laufenden // Queue-Worker voraus (Ops-Monitoring erforderlich, da die Zustellung // fristrelevant ist). Mail::to($legalRequest->requester_email)->queue( (new LegalRequestReceived($legalRequest))->afterCommit() ); Mail::to($this->operatorRecipient($request))->queue( (new LegalRequestSubmitted($legalRequest))->afterCommit() ); return back()->with('report-status', __('static.report_form.flash_received', [ 'reference' => $legalRequest->reference(), ])); } private function portalFromHost(Request $request): ?string { $host = $request->getHost(); if (str_contains($host, 'presseecho')) { return 'presseecho'; } if (str_contains($host, 'businessportal')) { return 'businessportal24'; } return null; // Hub: Meldung ohne Portal-Zuordnung. } private function operatorRecipient(Request $request): string { if (str_contains($request->getHost(), 'pressekonto')) { $brand = config('domains.domains.pressekonto.brand', []); } else { $brand = WebPortalContext::fromRequest()->domainConfig['brand'] ?? []; } return $brand['report_email'] ?? (string) config('legal.contact_email'); } private function ensureNotRateLimited(Request $request): void { if (! RateLimiter::tooManyAttempts($this->throttleKey($request), 5)) { return; } $seconds = RateLimiter::availableIn($this->throttleKey($request)); throw ValidationException::withMessages([ 'message' => __('static.report_form.throttle', [ 'minutes' => max(1, (int) ceil($seconds / 60)), ]), ]); } private function throttleKey(Request $request): string { return 'report-form|'.$request->ip(); } }