presseportale/app/Console/Commands/QuarantineContactSpam.php
Kevin Adametz 06d2889a4d feat: Spamschutz für Kontakt-Widget und Registrierung mit Cloudflare Turnstile
- Turnstile für Gäste und unbestätigte Konten (TurnstileVerifier,
  <x-turnstile>); ist Cloudflare nicht erreichbar, wird durchgelassen und
  im Verlauf, in der Kontakt-Queue und in der Betriebsüberwachung markiert
- Inhaltsfilter des Kontaktformulars (fremde Links, Floskel mit
  Seitentitel, Zeichensalat, Sperrliste, Absenden unter 3 Sekunden);
  Treffer landen in der Spam-Quarantäne ohne Mail, Löschung nach 30 Tagen
- Admin: Filter „Spam“, Sammelaktion, „Kein Spam“ holt die Mail nach
- Registrierung lehnt Zufallsnamen ab; users:prune-bot-registrations
  (täglicher Lauf erst mit USERS_PRUNE_BOT_REGISTRATIONS=true)
- contact-messages:quarantine-spam für den Altbestand
- CSP erlaubt challenges.cloudflare.com, Log-Kanal spam
- Datenschutzerklärung: Abschnitt 6a Cloudflare Turnstile (Vorschlag)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 08:49:58 +00:00

60 lines
2.1 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Models\ContactMessage;
use App\Services\Spam\ContactSpamFilter;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
/**
* Einmalig für den Altbestand (Spamschutz, Entscheidung 2): offene
* Kontakt-Anliegen von Gästen nach denselben Regeln wie beim Eingang in die
* Spam-Quarantäne legen. Was die Regeln nicht erkennen, markiert der Admin
* über die Sammelaktion der Kontakt-Queue.
*/
class QuarantineContactSpam extends Command
{
protected $signature = 'contact-messages:quarantine-spam {--dry-run : Nur auflisten, nichts ändern}';
protected $description = 'Legt offene Kontakt-Anliegen von Gästen, die der Spamfilter erkennt, in die Quarantäne.';
public function handle(ContactSpamFilter $filter): int
{
$hits = ContactMessage::query()
->open()
->whereNull('user_id')
->orderBy('id')
->get()
->map(fn (ContactMessage $message): array => [
'message' => $message,
// Ohne Zeitstempel: Der Altbestand hatte kein Formular-Feld dafür.
'reason' => $filter->reasonFor((string) $message->name, (string) $message->message, null),
])
->filter(fn (array $hit): bool => $hit['reason'] !== null);
$this->table(
['Vorgang', 'Eingang', 'Grund', 'Nachricht'],
$hits->map(fn (array $hit): array => [
$hit['message']->reference(),
$hit['message']->created_at?->format('d.m.Y'),
$hit['reason'],
Str::limit(Str::squish((string) $hit['message']->message), 60),
])->all(),
);
if ($this->option('dry-run')) {
$this->info("Probelauf: {$hits->count()} Anliegen würden als Spam markiert.");
return self::SUCCESS;
}
foreach ($hits as $hit) {
$hit['message']->markAsSpam(null, $hit['reason']);
}
$this->info("{$hits->count()} Anliegen als Spam markiert. Offen bleiben: ".ContactMessage::query()->open()->count().'.');
return self::SUCCESS;
}
}