presseportale/app/Rules/NoPromotionalText.php
Kevin Adametz 5d8e379e85 companies: Kurzfelder des Profils ohne Links, Adressen und Spam (NoPromotionalText)
Firmenname, Adresse und Pressekontakt erscheinen oeffentlich bzw. in
Mails, wurden aber nicht geprueft — nur die Beschreibung lief durch die
Rot-Ebene. Neue deterministische Regel: keine Links, keine E-Mail-
Adressen, keine Zeichen-/Emoji-Ketten, kein Blacklist-Treffer; nackte
Domains ohne Pfad bleiben erlaubt (Marke). Adresse zusaetzlich durch die
Rot-Ebene wie die Beschreibung.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 07:55:07 +00:00

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"). */
private 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;
}
}