- 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>
74 lines
2.6 KiB
PHP
74 lines
2.6 KiB
PHP
<?php
|
|
|
|
namespace App\Rules;
|
|
|
|
use App\Services\PressRelease\BlacklistService;
|
|
use Closure;
|
|
use Illuminate\Contracts\Validation\ValidationRule;
|
|
|
|
/**
|
|
* Kurzfelder, die öffentlich oder in Mails erscheinen (Firmenname, Adresse,
|
|
* Pressekontakt), sind keine Werbefläche: keine Links, keine
|
|
* E-Mail-Adressen, kein Blacklist-Treffer, keine Zeichenorgien.
|
|
*
|
|
* Bewusst deterministisch und eng — eine KI für sechs Wörter wäre langsam,
|
|
* und falsche Treffer kosten hier echte Firmen den Namen. Nackte Domains
|
|
* („meinshop.de GmbH") bleiben deshalb erlaubt: Sie sind Marke, und die
|
|
* Website ist ohnehin das eine systemseitig ausgezeichnete Linkfeld.
|
|
* Der Fließtext („Über das Unternehmen") läuft separat durch die Rot-Ebene
|
|
* der Meldungsprüfung (CompanyProfileCheck).
|
|
*/
|
|
class NoPromotionalText implements ValidationRule
|
|
{
|
|
/** Schema, www. — oder eine nackte Domain mit Pfad („spam.example/deal"). */
|
|
public const LINK_PATTERN = '~(?:https?://|www\.|\b[a-z0-9-]+\.[a-z]{2,}/\S)~iu';
|
|
|
|
private const EMAIL_PATTERN = '~[^\s@]+@[^\s@]+\.[a-z]{2,}~iu';
|
|
|
|
/** Drei und mehr Satz-/Währungszeichen am Stück: „!!!", „€€€", „$$$". */
|
|
private const NOISE_PATTERN = '~[!?$€£%*#]{3,}~u';
|
|
|
|
/** Drei und mehr Emoji/Symbole am Stück. */
|
|
private const EMOJI_PATTERN = '~(?:[\x{1F300}-\x{1FAFF}\x{2600}-\x{27BF}]\x{FE0F}?){3,}~u';
|
|
|
|
public function __construct(private readonly BlacklistService $blacklist) {}
|
|
|
|
public function validate(string $attribute, mixed $value, Closure $fail): void
|
|
{
|
|
if (! is_string($value) || trim($value) === '') {
|
|
return;
|
|
}
|
|
|
|
$reason = self::reasonFor($value, $this->blacklist);
|
|
|
|
if ($reason !== null) {
|
|
$fail($reason);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Grund der Ablehnung als Nutzertext, oder null, wenn der Text sauber ist.
|
|
*/
|
|
public static function reasonFor(string $value, BlacklistService $blacklist): ?string
|
|
{
|
|
if (preg_match(self::LINK_PATTERN, $value) === 1) {
|
|
return __('Links sind in diesem Feld nicht erlaubt. Die Website gehört in das Feld „Website".');
|
|
}
|
|
|
|
if (preg_match(self::EMAIL_PATTERN, $value) === 1) {
|
|
return __('E-Mail-Adressen gehören in das dafür vorgesehene Feld.');
|
|
}
|
|
|
|
if (preg_match(self::NOISE_PATTERN, $value) === 1 || preg_match(self::EMOJI_PATTERN, $value) === 1) {
|
|
return __('Bitte ohne Sonderzeichen-Ketten oder Emoji-Reihen.');
|
|
}
|
|
|
|
$word = $blacklist->find($value);
|
|
|
|
if ($word !== null) {
|
|
return __('Der Text enthält einen unzulässigen Begriff.');
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|