diff --git a/app/Http/Controllers/LegalRequestController.php b/app/Http/Controllers/LegalRequestController.php index 566f94ca..807a9081 100644 --- a/app/Http/Controllers/LegalRequestController.php +++ b/app/Http/Controllers/LegalRequestController.php @@ -6,11 +6,14 @@ use App\Enums\LegalRequestStatus; use App\Enums\LegalRequestType; use App\Enums\Portal; use App\Enums\PressReleaseStatus; +use App\Mail\LegalRequestReceived; +use App\Mail\LegalRequestSubmitted; use App\Models\LegalRequest; use App\Models\PressRelease; use App\Scopes\PortalScope; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\RateLimiter; use Illuminate\Validation\Rule; use Illuminate\Validation\ValidationException; @@ -78,7 +81,7 @@ class LegalRequestController extends Controller RateLimiter::hit($this->throttleKey($release), 3600); - LegalRequest::query()->create([ + $legalRequest = LegalRequest::query()->create([ 'press_release_id' => $release->getKey(), 'portal' => $release->portal?->value, 'type' => $type->value, @@ -88,7 +91,19 @@ class LegalRequestController extends Controller 'requester_ip' => $request->ip(), ]); - return $this->done($release); + // T10/DSA Art. 16 Abs. 4: unverzügliche Eingangsbestätigung mit + // Vorgangsnummer (sofern eine E-Mail-Adresse angegeben wurde) + + // interner Hinweis ans Melde-Postfach. + if ($legalRequest->requester_email) { + Mail::to($legalRequest->requester_email)->send(new LegalRequestReceived($legalRequest)); + } + Mail::to($this->operatorRecipient())->send(new LegalRequestSubmitted($legalRequest)); + + return redirect() + ->route('release.detail', ['slug' => $release->slug]) + ->with('legal-status', __('Vielen Dank. Ihre Anfrage ist unter der Vorgangsnummer :reference eingegangen und wird von unserem Team geprüft.', [ + 'reference' => $legalRequest->reference(), + ])); } private function done(PressRelease $release): RedirectResponse @@ -98,6 +113,15 @@ class LegalRequestController extends Controller ->with('legal-status', __('Vielen Dank. Ihre Anfrage ist eingegangen und wird von unserem Team geprüft.')); } + private function operatorRecipient(): string + { + $brand = str_contains(request()->getHost(), 'presseecho') + ? config('domains.domains.presseecho.brand', []) + : config('domains.domains.businessportal24.brand', []); + + return $brand['report_email'] ?? (string) config('legal.contact_email'); + } + private function resolvePublishedRelease(string $slug): PressRelease { $portal = str_contains(request()->getHost(), 'presseecho') diff --git a/app/Http/Controllers/Web/ReportFormController.php b/app/Http/Controllers/Web/ReportFormController.php new file mode 100644 index 00000000..52d6a400 --- /dev/null +++ b/app/Http/Controllers/Web/ReportFormController.php @@ -0,0 +1,153 @@ + $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(); + } +} diff --git a/app/Mail/LegalRequestReceived.php b/app/Mail/LegalRequestReceived.php new file mode 100644 index 00000000..74846497 --- /dev/null +++ b/app/Mail/LegalRequestReceived.php @@ -0,0 +1,36 @@ +legalRequest->reference(), + ); + } + + public function content(): Content + { + return new Content(view: 'emails.legal.received'); + } +} diff --git a/app/Mail/LegalRequestSubmitted.php b/app/Mail/LegalRequestSubmitted.php new file mode 100644 index 00000000..458905da --- /dev/null +++ b/app/Mail/LegalRequestSubmitted.php @@ -0,0 +1,36 @@ +legalRequest->reference().': '.$this->legalRequest->type->label(), + ); + } + + public function content(): Content + { + return new Content(view: 'emails.legal.submitted'); + } +} diff --git a/app/Models/LegalRequest.php b/app/Models/LegalRequest.php index b739dd6a..c8ce6046 100644 --- a/app/Models/LegalRequest.php +++ b/app/Models/LegalRequest.php @@ -44,6 +44,32 @@ class LegalRequest extends Model ]; } + /** + * Öffentliche Vorgangsnummer (T10) — analog zur Kündigung ("K-{id}"). + * Wird in der Eingangsbestätigung genannt und bei Rückfragen referenziert. + */ + public function reference(): string + { + return 'M-'.$this->id; + } + + /** + * Name des Meldenden (Standalone-Formulare, DSA Art. 16) — strukturierte + * Zusatzfelder liegen bewusst im payload (WS-3). + */ + public function requesterName(): ?string + { + return $this->payload['requester_name'] ?? null; + } + + /** + * Gemeldete URL, wie vom Meldenden angegeben (Standalone-Formulare). + */ + public function reportedUrl(): ?string + { + return $this->payload['reported_url'] ?? null; + } + public function pressRelease(): BelongsTo { return $this->belongsTo(PressRelease::class); diff --git a/resources/views/emails/legal/received.blade.php b/resources/views/emails/legal/received.blade.php new file mode 100644 index 00000000..d76affa5 --- /dev/null +++ b/resources/views/emails/legal/received.blade.php @@ -0,0 +1,37 @@ + +

Guten Tag{{ $legalRequest->requesterName() ? ' '.$legalRequest->requesterName() : '' }},

+ +

+ wir bestätigen den Eingang Ihrer Meldung. Sie ist bei uns am + {{ $legalRequest->created_at->format('d.m.Y') }} + um {{ $legalRequest->created_at->format('H:i') }} Uhr eingegangen. +

+ +
+
Vorgangsnummer
+
{{ $legalRequest->reference() }}
+
Art der Meldung
+
{{ $legalRequest->type->publicLabel() }}
+ @if ($legalRequest->reportedUrl()) +
Betroffene URL
+
{{ $legalRequest->reportedUrl() }}
+ @elseif ($legalRequest->pressRelease) +
Betroffene Pressemitteilung
+
{{ $legalRequest->pressRelease->title }}
+ @endif +
+ +

+ Wir prüfen Ihre Meldung sorgfältig und zeitnah. Anschließend teilen wir Ihnen + unsere Entscheidung mit einer Begründung mit. Bitte geben Sie bei Rückfragen + die Vorgangsnummer an. +

+ +
+

+ Diese Bestätigung wurde automatisch erstellt und dokumentiert Datum und + Uhrzeit des Eingangs Ihrer Meldung. Die Meldung rechtswidriger Inhalte und + die Ausübung gesetzlicher Rechte sind kostenlos. +

+
+
diff --git a/resources/views/emails/legal/submitted.blade.php b/resources/views/emails/legal/submitted.blade.php new file mode 100644 index 00000000..dd09d27e --- /dev/null +++ b/resources/views/emails/legal/submitted.blade.php @@ -0,0 +1,41 @@ + +

Eine neue Meldung ist eingegangen und wartet in der Queue „Recht & Compliance“.

+ +
+
Vorgangsnummer
+
{{ $legalRequest->reference() }}
+
Art
+
{{ $legalRequest->type->label() }}
+ @if ($legalRequest->portal) +
Portal
+
{{ $legalRequest->portal }}
+ @endif + @if ($legalRequest->reportedUrl()) +
Gemeldete URL
+
{{ $legalRequest->reportedUrl() }}
+ @endif + @if ($legalRequest->pressRelease) +
Zugeordnete Pressemitteilung
+
{{ $legalRequest->pressRelease->title }}
+ @endif + @if ($legalRequest->requesterName()) +
Name
+
{{ $legalRequest->requesterName() }}
+ @endif + @if ($legalRequest->requester_email) +
E-Mail
+
{{ $legalRequest->requester_email }}
+ @endif +
+ +

Anliegen:

+

{{ $legalRequest->message }}

+ +
+

+ DSA-Meldungen bitte zeitnah bearbeiten — die meldende Person hat eine + Eingangsbestätigung mit dieser Vorgangsnummer erhalten und erwartet eine + begründete Entscheidung. +

+
+
diff --git a/resources/views/livewire/admin/legal-requests/index.blade.php b/resources/views/livewire/admin/legal-requests/index.blade.php index 4d3090c3..a386feb5 100644 --- a/resources/views/livewire/admin/legal-requests/index.blade.php +++ b/resources/views/livewire/admin/legal-requests/index.blade.php @@ -207,6 +207,9 @@ new #[Layout('components.layouts.app'), Title('Recht & Compliance')] class exten {{ $request->type->label() }} + + {{ $request->reference() }} + @@ -215,7 +218,7 @@ new #[Layout('components.layouts.app'), Title('Recht & Compliance')] class exten - {{ \Illuminate\Support\Str::limit($request->pressRelease?->title ?? __('PM gelöscht'), 60) }} + {{ \Illuminate\Support\Str::limit($request->pressRelease?->title ?? $request->reportedUrl() ?? __('PM gelöscht'), 60) }} diff --git a/resources/views/livewire/admin/legal-requests/show.blade.php b/resources/views/livewire/admin/legal-requests/show.blade.php index e0f6efdb..a66b8c3f 100644 --- a/resources/views/livewire/admin/legal-requests/show.blade.php +++ b/resources/views/livewire/admin/legal-requests/show.blade.php @@ -120,6 +120,16 @@ new #[Layout('components.layouts.app'), Title('Anfrage · Recht & Compliance')]
+
+
{{ __('Vorgangsnummer') }}
+
{{ $request->reference() }}
+
+ @if ($request->requesterName()) +
+
{{ __('Name') }}
+
{{ $request->requesterName() }}
+
+ @endif
{{ __('Absender') }}
@@ -144,6 +154,10 @@ new #[Layout('components.layouts.app'), Title('Anfrage · Recht & Compliance')] {{ __('öffnen') }} @endif + @elseif ($request->reportedUrl()) + {{ __('Keine PM zugeordnet — gemeldete URL:') }} + {{ $request->reportedUrl() }} @else {{ __('PM wurde gelöscht') }} @endif diff --git a/resources/views/web/melden-form.blade.php b/resources/views/web/melden-form.blade.php new file mode 100644 index 00000000..87e206f1 --- /dev/null +++ b/resources/views/web/melden-form.blade.php @@ -0,0 +1,132 @@ +@extends('web.layouts.web-master') + +@php + // T10: Eigenständige Meldeformulare (Meldeverfahren v0.1). Weg 1 = + // DSA-Meldung rechtswidriger Inhalte, Weg 2 = DSGVO-Antrag auf + // Entfernung eigener Kontaktdaten. Beide speisen die Admin-Queue + // "Recht & Compliance" und bestätigen den Eingang per E-Mail. + $isIllegalContent = $way === 'rechtswidrig'; + $pageTitle = $isIllegalContent + ? 'Rechtswidrige Inhalte melden' + : 'Eigene Kontaktdaten entfernen lassen'; +@endphp + +@section('title', $pageTitle) +@section('meta_description', $isIllegalContent + ? 'Rechtswidrige Inhalte oder Rechteverletzungen in einer Pressemitteilung melden (DSA) — kostenlos, mit Eingangsbestätigung.' + : 'Entfernung eigener personenbezogener Kontaktdaten aus einer Pressemitteilung beantragen (DSGVO) — kostenlos.') + +@section('content') + +
+

+ @if ($isIllegalContent) + Nutzen Sie dieses Formular, wenn Sie eine veröffentlichte Pressemitteilung für + rechtswidrig halten oder Ihre Rechte verletzt sehen. Die Meldung ist kostenlos; + Sie erhalten unverzüglich eine Eingangsbestätigung mit Vorgangsnummer und + anschließend unsere begründete Entscheidung. + @else + Nutzen Sie dieses Formular, wenn Ihre persönlichen Kontaktdaten (z. B. Name, + Telefonnummer oder E-Mail-Adresse) in einer Pressemitteilung stehen. Die Prüfung + erfolgt nach den Vorgaben der DSGVO und ist für Sie kostenlos. + @endif +

+

+ Alle Wege im Überblick: Inhalte melden oder Änderungen beantragen +

+ + @if (session('report-status')) +
+ {{ session('report-status') }} +
+ @endif + +
+ @csrf + +
+ + + @error('reported_url') +

{{ $message }}

+ @enderror +
+ +
+ + + @error('requester_name') +

{{ $message }}

+ @enderror +
+ +
+ + + @error('requester_email') +

{{ $message }}

+ @enderror +
+ +
+ + + @error('message') +

{{ $message }}

+ @enderror +
+ + @if ($isIllegalContent) +
+ + @error('truth_affirmed') +

{{ $message }}

+ @enderror +
+ @endif + + {{-- Bot-Schutz: Honeypot-Felder (siehe legal-request.blade.php). --}} + + + +
+ +

+ Ihre Angaben werden ausschließlich zur Bearbeitung Ihrer Meldung verarbeitet — + Details in der Datenschutzerklärung. + Meldungen und die Ausübung gesetzlicher Rechte sind kostenlos. +

+
+
+@endsection diff --git a/resources/views/web/melden.blade.php b/resources/views/web/melden.blade.php index 62b28a34..9df8a326 100644 --- a/resources/views/web/melden.blade.php +++ b/resources/views/web/melden.blade.php @@ -50,11 +50,13 @@
  • sonstigen gesetzlichen Verstößen.
  • - Meldung: Öffnen Sie die betroffene Pressemitteilung und nutzen Sie dort - den Link „Pressemitteilung melden" — die Mitteilung wird dabei automatisch dem - Vorgang zugeordnet. Alternativ senden Sie Ihre Meldung mit der URL der betroffenen - Pressemitteilung an - {{ $reportEmail }}. + Meldung: Nutzen Sie unser + Meldeformular für + rechtswidrige Inhalte — Sie erhalten unverzüglich eine Eingangsbestätigung mit + Vorgangsnummer. Alternativ öffnen Sie die betroffene Pressemitteilung und nutzen dort + den Link „Inhalt melden" (die Mitteilung wird dabei automatisch dem Vorgang + zugeordnet), oder Sie senden Ihre Meldung mit der URL der betroffenen Pressemitteilung + an {{ $reportEmail }}.

    Damit wir Ihre Meldung prüfen können, benötigen wir:

      @@ -81,9 +83,11 @@ E-Mail-Adresse.

      - Antrag: Öffnen Sie die betroffene Pressemitteilung und nutzen Sie dort - den Link „Datenschutz & Persönlichkeitsrecht" — oder senden Sie Ihren - Antrag an {{ $reportEmail }}. + Antrag: Nutzen Sie unser + Antragsformular zur + Entfernung eigener Kontaktdaten. Alternativ öffnen Sie die betroffene + Pressemitteilung und nutzen dort den Link „Eigene Daten entfernen" — oder Sie + senden Ihren Antrag an {{ $reportEmail }}.

      Wir prüfen Ihren Antrag nach den Vorgaben der Datenschutz-Grundverordnung. Die Prüfung und diff --git a/routes/web.php b/routes/web.php index a0f9fd54..7b4b91a1 100644 --- a/routes/web.php +++ b/routes/web.php @@ -9,6 +9,7 @@ use App\Http\Controllers\Web\NewsroomDetailController; use App\Http\Controllers\Web\NewsroomsController; use App\Http\Controllers\Web\PreiseController; use App\Http\Controllers\Web\ReleaseDetailController; +use App\Http\Controllers\Web\ReportFormController; use App\Http\Controllers\Web\SitemapController; use App\Http\Controllers\Web\SucheController; use App\Http\Controllers\Web\TerminationController; @@ -232,6 +233,16 @@ Route::prefix('{edition}')->whereIn('edition', SetEdition::EDITIONS)->group(func // drei Domains ausgeliefert. Route::view('richtlinien', 'web.richtlinien')->name('richtlinien'); Route::view('melden', 'web.melden')->name('melden'); + + // T10: Eigenständige Meldeformulare mit URL-Feld (Meldeverfahren Weg 1/2) + // — Vorgangsnummer + Eingangsbestätigung per E-Mail (DSA Art. 16). + Route::get('melden/{way}', [ReportFormController::class, 'show']) + ->whereIn('way', ReportFormController::WAYS) + ->name('melden.form'); + Route::post('melden/{way}', [ReportFormController::class, 'store']) + ->middleware('throttle:10,1') + ->whereIn('way', ReportFormController::WAYS) + ->name('melden.form.store'); }); // --------------------------------------------------------------------------- diff --git a/tests/Feature/LegalRequestTest.php b/tests/Feature/LegalRequestTest.php index 05eac3bb..4b10a730 100644 --- a/tests/Feature/LegalRequestTest.php +++ b/tests/Feature/LegalRequestTest.php @@ -3,10 +3,13 @@ use App\Enums\LegalRequestStatus; use App\Enums\LegalRequestType; use App\Enums\Portal; +use App\Mail\LegalRequestReceived; +use App\Mail\LegalRequestSubmitted; use App\Models\LegalRequest; use App\Models\PressRelease; use App\Models\User; use Database\Seeders\RolesAndPermissionsSeeder; +use Illuminate\Support\Facades\Mail; use Livewire\Volt\Volt; use Tests\TestCase; @@ -51,13 +54,15 @@ test('the form 404s for a non-published release', function () { test('a dsgvo request is stored in the queue', function () { /** @var TestCase $this */ + Mail::fake(); $release = publishedRelease(); $this->post(route('legal-request.store', ['slug' => $release->slug]), [ 'type' => LegalRequestType::Dsgvo->value, 'requester_email' => 'betroffen@example.test', 'message' => 'Bitte meine personenbezogenen Daten anonymisieren.', - ])->assertRedirect(route('release.detail', ['slug' => $release->slug])); + ])->assertRedirect(route('release.detail', ['slug' => $release->slug])) + ->assertSessionHas('legal-status'); $request = LegalRequest::query()->firstOrFail(); @@ -66,6 +71,11 @@ test('a dsgvo request is stored in the queue', function () { expect($request->press_release_id)->toBe($release->id); expect($request->requester_email)->toBe('betroffen@example.test'); expect($request->portal)->toBe(Portal::Businessportal24->value); + + // T10: Vorgangsnummer im Hinweis + Eingangsbestätigung/Queue-Hinweis. + expect(session('legal-status'))->toContain($request->reference()); + Mail::assertSent(LegalRequestReceived::class, fn ($mail) => $mail->hasTo('betroffen@example.test')); + Mail::assertSent(LegalRequestSubmitted::class); }); test('a personal rights request requires an email', function () { @@ -82,6 +92,7 @@ test('a personal rights request requires an email', function () { test('a report can be submitted without an email and feeds the same queue', function () { /** @var TestCase $this */ + Mail::fake(); $release = publishedRelease(); $this->post(route('legal-request.store', ['slug' => $release->slug]), [ @@ -93,6 +104,10 @@ test('a report can be submitted without an email and feeds the same queue', func expect($request->type)->toBe(LegalRequestType::Report); expect($request->requester_email)->toBeNull(); + + // Ohne E-Mail keine Eingangsbestätigung — der interne Hinweis geht trotzdem raus. + Mail::assertNotSent(LegalRequestReceived::class); + Mail::assertSent(LegalRequestSubmitted::class); }); test('a filled honeypot field blocks bots without creating a request', function (string $field) { diff --git a/tests/Feature/Web/ReportFormTest.php b/tests/Feature/Web/ReportFormTest.php new file mode 100644 index 00000000..cca7bfd7 --- /dev/null +++ b/tests/Feature/Web/ReportFormTest.php @@ -0,0 +1,129 @@ +get("https://presseecho.test/de/melden/{$way}") + ->assertSuccessful() + ->assertSeeText($heading) + ->assertSee('name="reported_url"', false); + + $this->get("https://pressekonto.test/de/melden/{$way}")->assertSuccessful(); +})->with([ + 'rechtswidrig' => ['rechtswidrig', 'Rechtswidrige Inhalte melden'], + 'kontaktdaten' => ['kontaktdaten', 'Eigene Kontaktdaten entfernen lassen'], +]); + +test('an illegal content report creates a request and confirms receipt with a reference', function () { + /** @var TestCase $this */ + Mail::fake(); + + $response = $this->from('https://businessportal24.test/de/melden/rechtswidrig') + ->post('https://businessportal24.test/de/melden/rechtswidrig', [ + 'reported_url' => 'https://businessportal24.com/de/press-release/irgendein-artikel', + 'requester_name' => 'Erika Musterfrau', + 'requester_email' => 'erika@example.com', + 'message' => 'Diese Mitteilung enthält nachweislich falsche Tatsachenbehauptungen über mich.', + 'truth_affirmed' => '1', + ]); + + $response->assertRedirect('https://businessportal24.test/de/melden/rechtswidrig') + ->assertSessionHas('report-status'); + + $request = LegalRequest::query()->sole(); + expect($request->type)->toBe(LegalRequestType::Report); + expect($request->requester_email)->toBe('erika@example.com'); + expect($request->requesterName())->toBe('Erika Musterfrau'); + expect($request->reportedUrl())->toBe('https://businessportal24.com/de/press-release/irgendein-artikel'); + expect($request->payload['truth_affirmed'])->toBeTrue(); + expect($request->portal)->toBe('businessportal24'); + expect(session('report-status'))->toContain($request->reference()); + + // DSA Art. 16 Abs. 4: unverzügliche Eingangsbestätigung + interner Hinweis. + Mail::assertSent(LegalRequestReceived::class, fn ($mail) => $mail->hasTo('erika@example.com')); + Mail::assertSent(LegalRequestSubmitted::class); +}); + +test('a reported url matching a published release is linked to it', function () { + /** @var TestCase $this */ + Mail::fake(); + + $release = PressRelease::factory() + ->published() + ->forPortal(Portal::Businessportal24) + ->create(['published_at' => now()->subDay()]); + + $this->post('https://businessportal24.test/de/melden/rechtswidrig', [ + 'reported_url' => 'https://businessportal24.com/de/press-release/'.$release->slug, + 'requester_name' => 'Erika Musterfrau', + 'requester_email' => 'erika@example.com', + 'message' => 'Diese Mitteilung verletzt meine Markenrechte an mehreren Stellen.', + 'truth_affirmed' => '1', + ]); + + expect(LegalRequest::query()->sole()->press_release_id)->toBe($release->id); +}); + +test('the illegal content report requires the truthfulness affirmation and a valid url', function () { + /** @var TestCase $this */ + Mail::fake(); + + $this->from('https://businessportal24.test/de/melden/rechtswidrig') + ->post('https://businessportal24.test/de/melden/rechtswidrig', [ + 'reported_url' => 'keine-url', + 'requester_name' => 'Erika Musterfrau', + 'requester_email' => 'erika@example.com', + 'message' => 'Diese Mitteilung enthält falsche Tatsachenbehauptungen über mich.', + ]) + ->assertSessionHasErrors(['reported_url', 'truth_affirmed']); + + expect(LegalRequest::query()->count())->toBe(0); + Mail::assertNothingSent(); +}); + +test('the contact data removal form creates a dsgvo request without affirmation checkbox', function () { + /** @var TestCase $this */ + Mail::fake(); + + $this->post('https://presseecho.test/de/melden/kontaktdaten', [ + 'reported_url' => 'https://presseecho.de/de/press-release/alter-artikel', + 'requester_name' => 'Max Mustermann', + 'requester_email' => 'max@example.com', + 'message' => 'Mein Name und meine Telefonnummer stehen im Pressekontakt dieser Mitteilung.', + ])->assertSessionHas('report-status'); + + $request = LegalRequest::query()->sole(); + expect($request->type)->toBe(LegalRequestType::Dsgvo); + expect($request->portal)->toBe('presseecho'); + Mail::assertSent(LegalRequestReceived::class); +}); + +test('a filled honeypot is silently discarded', function () { + /** @var TestCase $this */ + Mail::fake(); + + $this->post('https://businessportal24.test/de/melden/rechtswidrig', [ + 'reported_url' => 'https://businessportal24.com/de/press-release/spam', + 'requester_name' => 'Bot', + 'requester_email' => 'bot@example.com', + 'message' => 'Automatisch generierter Inhalt mit ausreichend vielen Zeichen.', + 'truth_affirmed' => '1', + 'website' => 'https://spam.example.com', + ])->assertSessionHas('report-status'); + + expect(LegalRequest::query()->count())->toBe(0); + Mail::assertNothingSent(); +}); + +test('an unknown form way responds 404', function () { + /** @var TestCase $this */ + $this->get('https://businessportal24.test/de/melden/sonstiges')->assertNotFound(); +});