Zwei eigenstaendige Formulare unter /melden/rechtswidrig (DSA Art. 16:
URL, Name, E-Mail, Begruendung, Wahrheitsversicherung) und
/melden/kontaktdaten (DSGVO-Antrag) auf allen drei Domains. Gemeldete
URLs werden automatisch der passenden veroeffentlichten PM zugeordnet;
ohne Treffer bleibt die URL im payload. Jede Meldung erhaelt eine
Vorgangsnummer (M-{id}, analog K-{id} der Kuendigung), die meldende
Person unverzueglich eine Eingangsbestaetigung (DSA Art. 16 Abs. 4) und
das Melde-Postfach einen internen Hinweis — auch im bestehenden
Per-PM-Flow. Admin-Queue zeigt Vorgangsnummer, Name und gemeldete URL;
/melden verlinkt die neuen Formulare als ersten Weg.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
153 lines
6 KiB
PHP
153 lines
6 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Web;
|
|
|
|
use App\Enums\LegalRequestStatus;
|
|
use App\Enums\LegalRequestType;
|
|
use App\Enums\PressReleaseStatus;
|
|
use App\Http\Controllers\Controller;
|
|
use App\Mail\LegalRequestReceived;
|
|
use App\Mail\LegalRequestSubmitted;
|
|
use App\Models\LegalRequest;
|
|
use App\Models\PressRelease;
|
|
use App\Scopes\PortalScope;
|
|
use App\Support\WebPortalContext;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Illuminate\Support\Facades\RateLimiter;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Illuminate\View\View;
|
|
|
|
/**
|
|
* Eigenständige Meldeformulare (T10, Meldeverfahren v0.1): DSA-Meldung
|
|
* rechtswidriger Inhalte ("rechtswidrig") und DSGVO-Antrag auf Entfernung
|
|
* eigener Kontaktdaten ("kontaktdaten") — mit URL-Feld statt Bindung an
|
|
* eine konkrete Pressemitteilung. Speist dieselbe Admin-Queue wie die
|
|
* Melde-Links an den Mitteilungen; die meldende Person erhält unverzüglich
|
|
* eine Eingangsbestätigung mit Vorgangsnummer (DSA Art. 16 Abs. 4).
|
|
*/
|
|
class ReportFormController extends Controller
|
|
{
|
|
public const WAYS = ['rechtswidrig', 'kontaktdaten'];
|
|
|
|
public function show(string $edition, string $way): View
|
|
{
|
|
return view('web.melden-form', ['way' => $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', __('Vielen Dank. Ihre Meldung ist eingegangen.'));
|
|
}
|
|
|
|
$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' => __('Bitte bestätigen Sie, dass Sie die Meldung nach bestem Wissen und Gewissen abgeben.'),
|
|
]);
|
|
|
|
$this->ensureNotRateLimited($request);
|
|
RateLimiter::hit($this->throttleKey($request), 3600);
|
|
|
|
$release = $this->matchPublishedRelease($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(),
|
|
]);
|
|
|
|
// Unverzügliche Eingangsbestätigung (DSA Art. 16 Abs. 4) + interner
|
|
// Hinweis ans Melde-Postfach, damit Fristen nicht an der Queue hängen.
|
|
Mail::to($legalRequest->requester_email)->send(new LegalRequestReceived($legalRequest));
|
|
Mail::to($this->operatorRecipient($request))->send(new LegalRequestSubmitted($legalRequest));
|
|
|
|
return back()->with('report-status', __('Vielen Dank. Ihre Meldung ist unter der Vorgangsnummer :reference eingegangen — Sie erhalten eine Eingangsbestätigung per E-Mail.', [
|
|
'reference' => $legalRequest->reference(),
|
|
]));
|
|
}
|
|
|
|
/**
|
|
* Ordnet die gemeldete URL einer veröffentlichten Pressemitteilung zu,
|
|
* wenn sie einer Detail-URL entspricht — sonst bleibt die Meldung ohne
|
|
* PM-Bezug in der Queue (URL steht im payload).
|
|
*/
|
|
private function matchPublishedRelease(string $url): ?PressRelease
|
|
{
|
|
if (! preg_match('#/press-release/([^/?\#]+)#', $url, $matches)) {
|
|
return null;
|
|
}
|
|
|
|
return PressRelease::query()
|
|
->withoutGlobalScope(PortalScope::class)
|
|
->where('slug', urldecode($matches[1]))
|
|
->where('status', PressReleaseStatus::Published)
|
|
->first();
|
|
}
|
|
|
|
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' => __('Zu viele Anfragen. Bitte versuchen Sie es in :minutes Minuten erneut.', [
|
|
'minutes' => max(1, (int) ceil($seconds / 60)),
|
|
]),
|
|
]);
|
|
}
|
|
|
|
private function throttleKey(Request $request): string
|
|
{
|
|
return 'report-form|'.$request->ip();
|
|
}
|
|
}
|