From 249f1952576324331e64aa1b750eeb114bd8ebbf Mon Sep 17 00:00:00 2001 From: Vladimir Bryzgalov Date: Tue, 21 Apr 2026 16:06:32 +0500 Subject: [PATCH] feat(classifier): count_placeholder_fields() helper for drop-logic Co-Authored-By: Claude Sonnet 4.6 --- includes/class-classifier.php | 36 ++++++++++++++++++++++++++++++++ tests/ClassifierDropTest.php | 39 +++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/includes/class-classifier.php b/includes/class-classifier.php index 63c6860..84a095c 100644 --- a/includes/class-classifier.php +++ b/includes/class-classifier.php @@ -46,6 +46,14 @@ final class Classifier 'rambler.ru', 'gmail.com', ]; + /** Service field keys excluded from placeholder count. */ + private const SERVICE_FIELD_KEYS = [ + 'acceptance-data', + '_wpcf7', '_wpcf7_version', '_wpcf7_locale', '_wpcf7_unit_tag', + '_wpcf7_container_post', '_wpcf7_posted_data_hash', + 'g-recaptcha-response', + ]; + /** * @param array $fields CF7 posted_data (field => value or array of values) * @param array{reason?:string} $meta Context: reason = Akismet / Honeypot / CF7 rules / Неизвестно @@ -266,4 +274,32 @@ final class Classifier if ($name === '') return false; return (bool)preg_match('/^[A-Za-z0-9]{1,3}$/', $name); } + + /** + * Counts non-empty fields whose value is exactly 1-2 ASCII alphanumeric chars + * (the typical "AB", "70" placeholder pattern bots fill custom-keyed fields with). + * + * Skips service keys (acceptance-data, _wpcf7*, g-recaptcha-response) and any key + * starting with `_wpcf7`. Array values are joined without separators before length + * check (so `['A','B']` collapses to "AB" — still counted). + * + * @param array $fields CF7 posted_data + * @return int Number of fields matching the placeholder pattern + */ + public static function count_placeholder_fields(array $fields): int + { + $count = 0; + foreach ($fields as $k => $v) { + if (is_string($k)) { + if (in_array($k, self::SERVICE_FIELD_KEYS, true)) continue; + if (strpos($k, '_wpcf7') === 0) continue; + } + if (is_array($v)) $v = implode('', array_map('strval', $v)); + $v = trim((string)$v); + if ($v === '') continue; + if (!preg_match('/^[A-Za-z0-9]{1,2}$/', $v)) continue; + $count++; + } + return $count; + } } diff --git a/tests/ClassifierDropTest.php b/tests/ClassifierDropTest.php index ff77f92..ec21dd7 100644 --- a/tests/ClassifierDropTest.php +++ b/tests/ClassifierDropTest.php @@ -102,4 +102,43 @@ final class ClassifierDropTest extends Cf7stgTestCase $fields = ['your-name' => ['OB', 'MU']]; self::assertFalse(Classifier::has_short_latin_name($fields)); } + + public function test_count_placeholder_fields_screenshot_1(): void + { + $fields = require __DIR__ . '/fixtures/spam-screenshot-1.php'; + // text-785: OB(2), text-787: 70(2), text-786: BP(2), text-788: ZZ(2), text-789: QV(2) + // text-790: 10 цифр — не placeholder. acceptance-data: служебное → исключено. + self::assertGreaterThanOrEqual(3, Classifier::count_placeholder_fields($fields)); + } + + public function test_count_placeholder_fields_ignores_empty(): void + { + $fields = ['a' => 'AB', 'b' => '', 'c' => 'XY', 'd' => null, 'e' => 'CD']; + self::assertSame(3, Classifier::count_placeholder_fields($fields)); + } + + public function test_count_placeholder_fields_ignores_long_values(): void + { + $fields = ['a' => 'AB', 'b' => 'Hello world', 'c' => 'CD']; + self::assertSame(2, Classifier::count_placeholder_fields($fields)); + } + + public function test_count_placeholder_fields_ignores_service_keys(): void + { + $fields = [ + 'acceptance-data' => '1', + '_wpcf7' => 'AB', + '_wpcf7_unit_tag' => 'XY', + 'g-recaptcha-response' => 'CD', + 'real' => 'EF', + ]; + self::assertSame(1, Classifier::count_placeholder_fields($fields)); + } + + public function test_count_placeholder_fields_ignores_non_alnum(): void + { + // Кириллица или с пробелами не считается placeholder + $fields = ['a' => 'Ия', 'b' => 'a b', 'c' => 'XY']; + self::assertSame(1, Classifier::count_placeholder_fields($fields)); + } }