T10: DSA-Meldeformulare mit URL-Feld, Vorgangsnummer und Eingangsbestaetigung
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>
This commit is contained in:
parent
61a677aa0b
commit
2a9be7a9af
14 changed files with 673 additions and 12 deletions
|
|
@ -6,11 +6,14 @@ use App\Enums\LegalRequestStatus;
|
||||||
use App\Enums\LegalRequestType;
|
use App\Enums\LegalRequestType;
|
||||||
use App\Enums\Portal;
|
use App\Enums\Portal;
|
||||||
use App\Enums\PressReleaseStatus;
|
use App\Enums\PressReleaseStatus;
|
||||||
|
use App\Mail\LegalRequestReceived;
|
||||||
|
use App\Mail\LegalRequestSubmitted;
|
||||||
use App\Models\LegalRequest;
|
use App\Models\LegalRequest;
|
||||||
use App\Models\PressRelease;
|
use App\Models\PressRelease;
|
||||||
use App\Scopes\PortalScope;
|
use App\Scopes\PortalScope;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
use Illuminate\Support\Facades\RateLimiter;
|
use Illuminate\Support\Facades\RateLimiter;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
@ -78,7 +81,7 @@ class LegalRequestController extends Controller
|
||||||
|
|
||||||
RateLimiter::hit($this->throttleKey($release), 3600);
|
RateLimiter::hit($this->throttleKey($release), 3600);
|
||||||
|
|
||||||
LegalRequest::query()->create([
|
$legalRequest = LegalRequest::query()->create([
|
||||||
'press_release_id' => $release->getKey(),
|
'press_release_id' => $release->getKey(),
|
||||||
'portal' => $release->portal?->value,
|
'portal' => $release->portal?->value,
|
||||||
'type' => $type->value,
|
'type' => $type->value,
|
||||||
|
|
@ -88,7 +91,19 @@ class LegalRequestController extends Controller
|
||||||
'requester_ip' => $request->ip(),
|
'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
|
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.'));
|
->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
|
private function resolvePublishedRelease(string $slug): PressRelease
|
||||||
{
|
{
|
||||||
$portal = str_contains(request()->getHost(), 'presseecho')
|
$portal = str_contains(request()->getHost(), 'presseecho')
|
||||||
|
|
|
||||||
153
app/Http/Controllers/Web/ReportFormController.php
Normal file
153
app/Http/Controllers/Web/ReportFormController.php
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
<?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();
|
||||||
|
}
|
||||||
|
}
|
||||||
36
app/Mail/LegalRequestReceived.php
Normal file
36
app/Mail/LegalRequestReceived.php
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Mail;
|
||||||
|
|
||||||
|
use App\Models\LegalRequest;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Mail\Mailables\Content;
|
||||||
|
use Illuminate\Mail\Mailables\Envelope;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unverzügliche Eingangsbestätigung einer Rechts-/Compliance-Meldung an die
|
||||||
|
* meldende Person (T10, DSA Art. 16 Abs. 4) — mit Vorgangsnummer sowie Datum
|
||||||
|
* und Uhrzeit des Eingangs.
|
||||||
|
*/
|
||||||
|
class LegalRequestReceived extends Mailable
|
||||||
|
{
|
||||||
|
use Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public readonly LegalRequest $legalRequest,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function envelope(): Envelope
|
||||||
|
{
|
||||||
|
return new Envelope(
|
||||||
|
subject: 'Eingangsbestätigung: Ihre Meldung '.$this->legalRequest->reference(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function content(): Content
|
||||||
|
{
|
||||||
|
return new Content(view: 'emails.legal.received');
|
||||||
|
}
|
||||||
|
}
|
||||||
36
app/Mail/LegalRequestSubmitted.php
Normal file
36
app/Mail/LegalRequestSubmitted.php
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Mail;
|
||||||
|
|
||||||
|
use App\Models\LegalRequest;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Mail\Mailables\Content;
|
||||||
|
use Illuminate\Mail\Mailables\Envelope;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interner Hinweis an das Melde-Postfach: neue Rechts-/Compliance-Anfrage in
|
||||||
|
* der Admin-Queue (DSA-Fristen — Meldungen sollen nicht erst beim nächsten
|
||||||
|
* Blick in den Admin auffallen).
|
||||||
|
*/
|
||||||
|
class LegalRequestSubmitted extends Mailable
|
||||||
|
{
|
||||||
|
use Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public readonly LegalRequest $legalRequest,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function envelope(): Envelope
|
||||||
|
{
|
||||||
|
return new Envelope(
|
||||||
|
subject: 'Neue Meldung '.$this->legalRequest->reference().': '.$this->legalRequest->type->label(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function content(): Content
|
||||||
|
{
|
||||||
|
return new Content(view: 'emails.legal.submitted');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
public function pressRelease(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(PressRelease::class);
|
return $this->belongsTo(PressRelease::class);
|
||||||
|
|
|
||||||
37
resources/views/emails/legal/received.blade.php
Normal file
37
resources/views/emails/legal/received.blade.php
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
<x-emails.layout title="Eingangsbestätigung Ihrer Meldung" preheader="Wir bestätigen den Eingang Ihrer Meldung und prüfen sie sorgfältig und zeitnah.">
|
||||||
|
<p>Guten Tag{{ $legalRequest->requesterName() ? ' '.$legalRequest->requesterName() : '' }},</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
wir bestätigen den Eingang Ihrer Meldung. Sie ist bei uns am
|
||||||
|
<strong>{{ $legalRequest->created_at->format('d.m.Y') }}
|
||||||
|
um {{ $legalRequest->created_at->format('H:i') }} Uhr</strong> eingegangen.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<dl class="meta">
|
||||||
|
<dt>Vorgangsnummer</dt>
|
||||||
|
<dd>{{ $legalRequest->reference() }}</dd>
|
||||||
|
<dt>Art der Meldung</dt>
|
||||||
|
<dd>{{ $legalRequest->type->publicLabel() }}</dd>
|
||||||
|
@if ($legalRequest->reportedUrl())
|
||||||
|
<dt>Betroffene URL</dt>
|
||||||
|
<dd>{{ $legalRequest->reportedUrl() }}</dd>
|
||||||
|
@elseif ($legalRequest->pressRelease)
|
||||||
|
<dt>Betroffene Pressemitteilung</dt>
|
||||||
|
<dd>{{ $legalRequest->pressRelease->title }}</dd>
|
||||||
|
@endif
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="note">
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</x-emails.layout>
|
||||||
41
resources/views/emails/legal/submitted.blade.php
Normal file
41
resources/views/emails/legal/submitted.blade.php
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
<x-emails.layout title="Neue Rechts-/Compliance-Meldung" preheader="Eine neue Meldung ist in der Admin-Queue eingegangen.">
|
||||||
|
<p>Eine neue Meldung ist eingegangen und wartet in der Queue „Recht & Compliance“.</p>
|
||||||
|
|
||||||
|
<dl class="meta">
|
||||||
|
<dt>Vorgangsnummer</dt>
|
||||||
|
<dd>{{ $legalRequest->reference() }}</dd>
|
||||||
|
<dt>Art</dt>
|
||||||
|
<dd>{{ $legalRequest->type->label() }}</dd>
|
||||||
|
@if ($legalRequest->portal)
|
||||||
|
<dt>Portal</dt>
|
||||||
|
<dd>{{ $legalRequest->portal }}</dd>
|
||||||
|
@endif
|
||||||
|
@if ($legalRequest->reportedUrl())
|
||||||
|
<dt>Gemeldete URL</dt>
|
||||||
|
<dd>{{ $legalRequest->reportedUrl() }}</dd>
|
||||||
|
@endif
|
||||||
|
@if ($legalRequest->pressRelease)
|
||||||
|
<dt>Zugeordnete Pressemitteilung</dt>
|
||||||
|
<dd>{{ $legalRequest->pressRelease->title }}</dd>
|
||||||
|
@endif
|
||||||
|
@if ($legalRequest->requesterName())
|
||||||
|
<dt>Name</dt>
|
||||||
|
<dd>{{ $legalRequest->requesterName() }}</dd>
|
||||||
|
@endif
|
||||||
|
@if ($legalRequest->requester_email)
|
||||||
|
<dt>E-Mail</dt>
|
||||||
|
<dd>{{ $legalRequest->requester_email }}</dd>
|
||||||
|
@endif
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<p><strong>Anliegen:</strong></p>
|
||||||
|
<p>{{ $legalRequest->message }}</p>
|
||||||
|
|
||||||
|
<div class="note">
|
||||||
|
<p>
|
||||||
|
DSA-Meldungen bitte zeitnah bearbeiten — die meldende Person hat eine
|
||||||
|
Eingangsbestätigung mit dieser Vorgangsnummer erhalten und erwartet eine
|
||||||
|
begründete Entscheidung.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</x-emails.layout>
|
||||||
|
|
@ -207,6 +207,9 @@ new #[Layout('components.layouts.app'), Title('Recht & Compliance')] class exten
|
||||||
<span class="text-[13px] font-semibold text-[color:var(--color-ink)]">
|
<span class="text-[13px] font-semibold text-[color:var(--color-ink)]">
|
||||||
{{ $request->type->label() }}
|
{{ $request->type->label() }}
|
||||||
</span>
|
</span>
|
||||||
|
<span class="block text-[11px] font-mono text-[color:var(--color-ink-4)]">
|
||||||
|
{{ $request->reference() }}
|
||||||
|
</span>
|
||||||
</flux:table.cell>
|
</flux:table.cell>
|
||||||
<flux:table.cell>
|
<flux:table.cell>
|
||||||
<span class="badge dot {{ $this->statusBadgeClass($request->status) }}">
|
<span class="badge dot {{ $this->statusBadgeClass($request->status) }}">
|
||||||
|
|
@ -215,7 +218,7 @@ new #[Layout('components.layouts.app'), Title('Recht & Compliance')] class exten
|
||||||
</flux:table.cell>
|
</flux:table.cell>
|
||||||
<flux:table.cell>
|
<flux:table.cell>
|
||||||
<span class="text-[12.5px] text-[color:var(--color-ink)]">
|
<span class="text-[12.5px] text-[color:var(--color-ink)]">
|
||||||
{{ \Illuminate\Support\Str::limit($request->pressRelease?->title ?? __('PM gelöscht'), 60) }}
|
{{ \Illuminate\Support\Str::limit($request->pressRelease?->title ?? $request->reportedUrl() ?? __('PM gelöscht'), 60) }}
|
||||||
</span>
|
</span>
|
||||||
</flux:table.cell>
|
</flux:table.cell>
|
||||||
<flux:table.cell>
|
<flux:table.cell>
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,16 @@ new #[Layout('components.layouts.app'), Title('Anfrage · Recht & Compliance')]
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<dl class="grid gap-3 sm:grid-cols-2 text-[12.5px]">
|
<dl class="grid gap-3 sm:grid-cols-2 text-[12.5px]">
|
||||||
|
<div>
|
||||||
|
<dt class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-1">{{ __('Vorgangsnummer') }}</dt>
|
||||||
|
<dd class="text-[color:var(--color-ink)] font-mono">{{ $request->reference() }}</dd>
|
||||||
|
</div>
|
||||||
|
@if ($request->requesterName())
|
||||||
|
<div>
|
||||||
|
<dt class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-1">{{ __('Name') }}</dt>
|
||||||
|
<dd class="text-[color:var(--color-ink)]">{{ $request->requesterName() }}</dd>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
<div>
|
<div>
|
||||||
<dt class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-1">{{ __('Absender') }}</dt>
|
<dt class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-1">{{ __('Absender') }}</dt>
|
||||||
<dd class="text-[color:var(--color-ink)] break-all">
|
<dd class="text-[color:var(--color-ink)] break-all">
|
||||||
|
|
@ -144,6 +154,10 @@ new #[Layout('components.layouts.app'), Title('Anfrage · Recht & Compliance')]
|
||||||
<a href="{{ route('admin.press-releases.show', $request->pressRelease->id) }}" wire:navigate
|
<a href="{{ route('admin.press-releases.show', $request->pressRelease->id) }}" wire:navigate
|
||||||
class="ml-2 text-[12px] text-[color:var(--color-hub)] underline underline-offset-2">{{ __('öffnen') }}</a>
|
class="ml-2 text-[12px] text-[color:var(--color-hub)] underline underline-offset-2">{{ __('öffnen') }}</a>
|
||||||
@endif
|
@endif
|
||||||
|
@elseif ($request->reportedUrl())
|
||||||
|
{{ __('Keine PM zugeordnet — gemeldete URL:') }}
|
||||||
|
<a href="{{ $request->reportedUrl() }}" target="_blank" rel="noopener"
|
||||||
|
class="text-[color:var(--color-hub)] underline underline-offset-2 break-all">{{ $request->reportedUrl() }}</a>
|
||||||
@else
|
@else
|
||||||
{{ __('PM wurde gelöscht') }}
|
{{ __('PM wurde gelöscht') }}
|
||||||
@endif
|
@endif
|
||||||
|
|
|
||||||
132
resources/views/web/melden-form.blade.php
Normal file
132
resources/views/web/melden-form.blade.php
Normal file
|
|
@ -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')
|
||||||
|
<x-web.static-page
|
||||||
|
eyebrow="Rechtliches"
|
||||||
|
:title="$pageTitle"
|
||||||
|
breadcrumb="Inhalte melden"
|
||||||
|
>
|
||||||
|
<div class="max-w-[640px]">
|
||||||
|
<p class="text-[13.5px] text-ink-2 leading-relaxed mb-2">
|
||||||
|
@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
|
||||||
|
</p>
|
||||||
|
<p class="text-[12.5px] text-ink-3 leading-relaxed mb-6">
|
||||||
|
Alle Wege im Überblick: <a href="{{ route('melden') }}" class="underline underline-offset-2 hover:text-ink">Inhalte melden oder Änderungen beantragen</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@if (session('report-status'))
|
||||||
|
<div class="mb-6 px-4 py-3 border border-bg-rule bg-bg-elev text-[13px] text-ink-2" role="status">
|
||||||
|
{{ session('report-status') }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<form method="POST" action="{{ route('melden.form.store', ['way' => $way]) }}" class="space-y-5">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="reported_url" class="block text-[12px] font-semibold text-ink mb-1.5">URL der betroffenen Pressemitteilung</label>
|
||||||
|
<input id="reported_url" type="url" name="reported_url" value="{{ old('reported_url') }}" required
|
||||||
|
placeholder="https://…"
|
||||||
|
class="w-full border border-bg-rule px-3 py-2.5 text-[13px] bg-white text-ink focus:outline-none focus:border-brand" />
|
||||||
|
@error('reported_url')
|
||||||
|
<p class="mt-1 text-[12px] text-red-600">{{ $message }}</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="requester_name" class="block text-[12px] font-semibold text-ink mb-1.5">Ihr Name</label>
|
||||||
|
<input id="requester_name" type="text" name="requester_name" value="{{ old('requester_name') }}" required
|
||||||
|
autocomplete="name"
|
||||||
|
class="w-full border border-bg-rule px-3 py-2.5 text-[13px] bg-white text-ink focus:outline-none focus:border-brand" />
|
||||||
|
@error('requester_name')
|
||||||
|
<p class="mt-1 text-[12px] text-red-600">{{ $message }}</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="requester_email" class="block text-[12px] font-semibold text-ink mb-1.5">
|
||||||
|
{{ $isIllegalContent ? 'Ihre E-Mail-Adresse' : 'Ihre E-Mail-Adresse (für Rückmeldung und ggf. Bestätigungslink)' }}
|
||||||
|
</label>
|
||||||
|
<input id="requester_email" type="email" name="requester_email" value="{{ old('requester_email') }}" required
|
||||||
|
autocomplete="email"
|
||||||
|
class="w-full border border-bg-rule px-3 py-2.5 text-[13px] bg-white text-ink focus:outline-none focus:border-brand" />
|
||||||
|
@error('requester_email')
|
||||||
|
<p class="mt-1 text-[12px] text-red-600">{{ $message }}</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="message" class="block text-[12px] font-semibold text-ink mb-1.5">
|
||||||
|
{{ $isIllegalContent ? 'Begründung: Warum halten Sie den Inhalt für rechtswidrig?' : 'Welche Angaben sind betroffen?' }}
|
||||||
|
</label>
|
||||||
|
<textarea id="message" name="message" rows="6" required
|
||||||
|
class="w-full border border-bg-rule px-3 py-2.5 text-[13px] bg-white text-ink focus:outline-none focus:border-brand"
|
||||||
|
placeholder="{{ $isIllegalContent ? 'Bitte beschreiben Sie konkret, welche Inhalte betroffen sind und warum.' : 'z. B. „Mein Name und meine Telefonnummer stehen im Pressekontakt.“' }}">{{ old('message') }}</textarea>
|
||||||
|
@error('message')
|
||||||
|
<p class="mt-1 text-[12px] text-red-600">{{ $message }}</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($isIllegalContent)
|
||||||
|
<div>
|
||||||
|
<label class="flex items-start gap-2.5 text-[12.5px] text-ink-2 leading-relaxed cursor-pointer">
|
||||||
|
<input type="checkbox" name="truth_affirmed" value="1" @checked(old('truth_affirmed'))
|
||||||
|
class="mt-0.5 flex-shrink-0" />
|
||||||
|
<span>
|
||||||
|
Ich versichere, dass ich diese Meldung nach bestem Wissen und Gewissen
|
||||||
|
abgebe und die Angaben richtig und vollständig sind.
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
@error('truth_affirmed')
|
||||||
|
<p class="mt-1 text-[12px] text-red-600">{{ $message }}</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- Bot-Schutz: Honeypot-Felder (siehe legal-request.blade.php). --}}
|
||||||
|
<div class="hidden" aria-hidden="true">
|
||||||
|
<label for="website">Website</label>
|
||||||
|
<input id="website" type="text" name="website" tabindex="-1" autocomplete="off" value="{{ old('website') }}" />
|
||||||
|
<label for="homepage">Homepage</label>
|
||||||
|
<input id="homepage" type="text" name="homepage" tabindex="-1" autocomplete="off" value="{{ old('homepage') }}" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit"
|
||||||
|
class="bg-brand hover:bg-brand-deep text-white px-5 py-2.5 text-[13px] font-semibold transition-colors">
|
||||||
|
{{ $isIllegalContent ? 'Meldung absenden' : 'Antrag absenden' }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="mt-6 text-[11.5px] text-ink-3 leading-relaxed">
|
||||||
|
Ihre Angaben werden ausschließlich zur Bearbeitung Ihrer Meldung verarbeitet —
|
||||||
|
Details in der <a href="{{ route('datenschutz') }}" class="underline hover:text-ink">Datenschutzerklärung</a>.
|
||||||
|
Meldungen und die Ausübung gesetzlicher Rechte sind kostenlos.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</x-web.static-page>
|
||||||
|
@endsection
|
||||||
|
|
@ -50,11 +50,13 @@
|
||||||
<li>sonstigen gesetzlichen Verstößen.</li>
|
<li>sonstigen gesetzlichen Verstößen.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>
|
<p>
|
||||||
<strong>Meldung:</strong> Öffnen Sie die betroffene Pressemitteilung und nutzen Sie dort
|
<strong>Meldung:</strong> Nutzen Sie unser
|
||||||
den Link <em>„Pressemitteilung melden"</em> — die Mitteilung wird dabei automatisch dem
|
<a href="{{ route('melden.form', ['way' => 'rechtswidrig']) }}">Meldeformular für
|
||||||
Vorgang zugeordnet. Alternativ senden Sie Ihre Meldung mit der URL der betroffenen
|
rechtswidrige Inhalte</a> — Sie erhalten unverzüglich eine Eingangsbestätigung mit
|
||||||
Pressemitteilung an
|
Vorgangsnummer. Alternativ öffnen Sie die betroffene Pressemitteilung und nutzen dort
|
||||||
<a href="mailto:{{ $reportEmail }}">{{ $reportEmail }}</a>.
|
den Link <em>„Inhalt melden"</em> (die Mitteilung wird dabei automatisch dem Vorgang
|
||||||
|
zugeordnet), oder Sie senden Ihre Meldung mit der URL der betroffenen Pressemitteilung
|
||||||
|
an <a href="mailto:{{ $reportEmail }}">{{ $reportEmail }}</a>.
|
||||||
</p>
|
</p>
|
||||||
<p>Damit wir Ihre Meldung prüfen können, benötigen wir:</p>
|
<p>Damit wir Ihre Meldung prüfen können, benötigen wir:</p>
|
||||||
<ul>
|
<ul>
|
||||||
|
|
@ -81,9 +83,11 @@
|
||||||
E-Mail-Adresse.
|
E-Mail-Adresse.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Antrag:</strong> Öffnen Sie die betroffene Pressemitteilung und nutzen Sie dort
|
<strong>Antrag:</strong> Nutzen Sie unser
|
||||||
den Link <em>„Datenschutz & Persönlichkeitsrecht"</em> — oder senden Sie Ihren
|
<a href="{{ route('melden.form', ['way' => 'kontaktdaten']) }}">Antragsformular zur
|
||||||
Antrag an <a href="mailto:{{ $reportEmail }}">{{ $reportEmail }}</a>.
|
Entfernung eigener Kontaktdaten</a>. Alternativ öffnen Sie die betroffene
|
||||||
|
Pressemitteilung und nutzen dort den Link <em>„Eigene Daten entfernen"</em> — oder Sie
|
||||||
|
senden Ihren Antrag an <a href="mailto:{{ $reportEmail }}">{{ $reportEmail }}</a>.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Wir prüfen Ihren Antrag nach den Vorgaben der Datenschutz-Grundverordnung. Die Prüfung und
|
Wir prüfen Ihren Antrag nach den Vorgaben der Datenschutz-Grundverordnung. Die Prüfung und
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ use App\Http\Controllers\Web\NewsroomDetailController;
|
||||||
use App\Http\Controllers\Web\NewsroomsController;
|
use App\Http\Controllers\Web\NewsroomsController;
|
||||||
use App\Http\Controllers\Web\PreiseController;
|
use App\Http\Controllers\Web\PreiseController;
|
||||||
use App\Http\Controllers\Web\ReleaseDetailController;
|
use App\Http\Controllers\Web\ReleaseDetailController;
|
||||||
|
use App\Http\Controllers\Web\ReportFormController;
|
||||||
use App\Http\Controllers\Web\SitemapController;
|
use App\Http\Controllers\Web\SitemapController;
|
||||||
use App\Http\Controllers\Web\SucheController;
|
use App\Http\Controllers\Web\SucheController;
|
||||||
use App\Http\Controllers\Web\TerminationController;
|
use App\Http\Controllers\Web\TerminationController;
|
||||||
|
|
@ -232,6 +233,16 @@ Route::prefix('{edition}')->whereIn('edition', SetEdition::EDITIONS)->group(func
|
||||||
// drei Domains ausgeliefert.
|
// drei Domains ausgeliefert.
|
||||||
Route::view('richtlinien', 'web.richtlinien')->name('richtlinien');
|
Route::view('richtlinien', 'web.richtlinien')->name('richtlinien');
|
||||||
Route::view('melden', 'web.melden')->name('melden');
|
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');
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,13 @@
|
||||||
use App\Enums\LegalRequestStatus;
|
use App\Enums\LegalRequestStatus;
|
||||||
use App\Enums\LegalRequestType;
|
use App\Enums\LegalRequestType;
|
||||||
use App\Enums\Portal;
|
use App\Enums\Portal;
|
||||||
|
use App\Mail\LegalRequestReceived;
|
||||||
|
use App\Mail\LegalRequestSubmitted;
|
||||||
use App\Models\LegalRequest;
|
use App\Models\LegalRequest;
|
||||||
use App\Models\PressRelease;
|
use App\Models\PressRelease;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Database\Seeders\RolesAndPermissionsSeeder;
|
use Database\Seeders\RolesAndPermissionsSeeder;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
use Livewire\Volt\Volt;
|
use Livewire\Volt\Volt;
|
||||||
use Tests\TestCase;
|
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 () {
|
test('a dsgvo request is stored in the queue', function () {
|
||||||
/** @var TestCase $this */
|
/** @var TestCase $this */
|
||||||
|
Mail::fake();
|
||||||
$release = publishedRelease();
|
$release = publishedRelease();
|
||||||
|
|
||||||
$this->post(route('legal-request.store', ['slug' => $release->slug]), [
|
$this->post(route('legal-request.store', ['slug' => $release->slug]), [
|
||||||
'type' => LegalRequestType::Dsgvo->value,
|
'type' => LegalRequestType::Dsgvo->value,
|
||||||
'requester_email' => 'betroffen@example.test',
|
'requester_email' => 'betroffen@example.test',
|
||||||
'message' => 'Bitte meine personenbezogenen Daten anonymisieren.',
|
'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();
|
$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->press_release_id)->toBe($release->id);
|
||||||
expect($request->requester_email)->toBe('betroffen@example.test');
|
expect($request->requester_email)->toBe('betroffen@example.test');
|
||||||
expect($request->portal)->toBe(Portal::Businessportal24->value);
|
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 () {
|
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 () {
|
test('a report can be submitted without an email and feeds the same queue', function () {
|
||||||
/** @var TestCase $this */
|
/** @var TestCase $this */
|
||||||
|
Mail::fake();
|
||||||
$release = publishedRelease();
|
$release = publishedRelease();
|
||||||
|
|
||||||
$this->post(route('legal-request.store', ['slug' => $release->slug]), [
|
$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->type)->toBe(LegalRequestType::Report);
|
||||||
expect($request->requester_email)->toBeNull();
|
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) {
|
test('a filled honeypot field blocks bots without creating a request', function (string $field) {
|
||||||
|
|
|
||||||
129
tests/Feature/Web/ReportFormTest.php
Normal file
129
tests/Feature/Web/ReportFormTest.php
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
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 Illuminate\Support\Facades\Mail;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
test('the standalone report forms render on every domain', function (string $way, string $heading) {
|
||||||
|
/** @var TestCase $this */
|
||||||
|
$this->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();
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue