diff --git a/includes/class-itk-admin.php b/includes/class-itk-admin.php index d066d31..2d02445 100644 --- a/includes/class-itk-admin.php +++ b/includes/class-itk-admin.php @@ -131,8 +131,10 @@ class ITK_Admin { break; case 'save_settings_honeypot': $this->save_settings_form('itk_honeypot', [ - 'min_time', 'max_time', 'retain_days', - ], []); + 'min_time', 'max_time', 'retain_days', 'max_links', + ], [ + 'block_url_in_email', 'block_links_new_commenters', + ]); $this->redirect(['tab' => 'honeypot', 'saved' => 1]); break; case 'save_settings_waf': @@ -847,6 +849,34 @@ class ITK_Admin { +
+

Comment Spam Content

+

Content-based rules that catch spam comments that pass the honeypot and timing checks.

+ ['Block URL in Email Field', 'Block comments where the email field contains a URL instead of a real address'], + 'block_links_new_commenters' => ['Block Links from New Commenters', 'Block any comment with a URL or website from an author with zero approved comments'], + ]; + foreach ($content_toggles as $key => [$label, $desc]): + $this->render_toggle('itk_honeypot', $key, $label, $desc, $opts); + endforeach; + ?> +
+ + + + + + + +
Max Links per Comment + +

Block comments with more than this many URLs. Set 0 to disable.

+
+ +
+
+ check_honeypot('comment')) { wp_die('Spam detected. Please go back and try again.', 'Spam Blocked', ['response' => 403]); } + if (!$this->check_comment_content($comment_data)) { + wp_die('Your comment was flagged as spam. Please remove any links and try again.', 'Spam Blocked', ['response' => 403]); + } return $comment_data; } + /* ── Comment content spam checks ─────────────────────────── */ + + private function check_comment_content(array $data): bool { + if (ITK_Whitelist::allowed()) return true; + + $email = trim($data['comment_author_email'] ?? ''); + $content = $data['comment_content'] ?? ''; + $url = trim($data['comment_author_url'] ?? ''); + + // 1. URL in email field — real emails never contain / or ? + if (!empty(self::$opts['block_url_in_email']) && $email !== '') { + $is_url_in_email = !filter_var($email, FILTER_VALIDATE_EMAIL) + && preg_match('#(https?://|[a-z0-9-]+\.[a-z]{2,}/|[?&]ref=)#i', $email); + if ($is_url_in_email) { + $this->log_block('comment', 'URL in email field: ' . substr($email, 0, 100)); + return false; + } + } + + // 2. Count URLs in comment body + $url_count = preg_match_all('#https?://\S+#i', $content); + $max_links = (int)(self::$opts['max_links'] ?? 2); + if ($max_links > 0 && $url_count > $max_links) { + $this->log_block('comment', "Too many URLs in comment ({$url_count})"); + return false; + } + + // 3. Any URL from a first-time commenter (0 approved comments) + if (!empty(self::$opts['block_links_new_commenters']) && ($url_count > 0 || $url !== '')) { + $approved = (int) get_comments([ + 'author_email' => $email, + 'status' => 'approve', + 'count' => true, + ]); + if ($approved === 0) { + $this->log_block('comment', 'Link from first-time commenter'); + return false; + } + } + + return true; + } + public function validate_login($user, string $username, string $password) { if (empty(self::$opts['protect_login'])) return $user; if (!empty($username) && !$this->check_honeypot('login')) { diff --git a/informatiq-toolkit.php b/informatiq-toolkit.php index 63af885..3b1d361 100644 --- a/informatiq-toolkit.php +++ b/informatiq-toolkit.php @@ -149,6 +149,9 @@ class InformatiQ_Toolkit { 'min_time' => 3, 'max_time' => 7200, 'retain_days' => 90, + 'block_url_in_email' => 1, + 'block_links_new_commenters' => 1, + 'max_links' => 2, ]); }