PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 2.9.5
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v2.9.5
3.0.0 2.11.12 2.11.11 2.11.10 2.11.9 2.11.7 2.11.8 2.11.6 2.11.5 2.11.4 2.11.3 2.11.1 2.11.2 2.11.0 2.10.5 2.10.4 2.10.3 2.10.2 2.10.1 2.10.0 2.9.9 2.9.8 2.9.6 2.9.7 2.9.5 All 88 releases
vigilante / includes / class-comment-security.php

class-comment-security.php in Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… 2.9.5, at includes/class-comment-security.php

343 lines 10.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Comment Security Class
4 *
5 * Handles comment security settings and spam protection
6 *
7 * @package Vigilante
8 */
9
10 // Prevent direct access
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 /**
16 * Class Vigilante_Comment_Security
17 *
18 * Manages comment security features
19 */
20 class Vigilante_Comment_Security {
21
22 /**
23 * Settings instance
24 *
25 * @var Vigilante_Settings
26 */
27 private $settings;
28
29 /**
30 * Comment security options
31 *
32 * @var array
33 */
34 private $options;
35
36 /**
37 * Constructor
38 *
39 * @param Vigilante_Settings $settings Settings instance.
40 */
41 public function __construct( $settings ) {
42 $this->settings = $settings;
43 $this->options = $settings->get_section( 'wp_hardening' );
44
45 $this->init_hooks();
46 }
47
48 /**
49 * Initialize hooks
50 */
51 private function init_hooks() {
52 // Disable pingbacks/trackbacks
53 if ( ! empty( $this->options['disable_pingbacks'] ) ) {
54 add_filter( 'xmlrpc_methods', array( $this, 'disable_pingback_methods' ) );
55 add_filter( 'wp_headers', array( $this, 'remove_pingback_header' ) );
56 add_filter( 'pings_open', '__return_false', 9999 );
57 }
58
59 if ( ! empty( $this->options['disable_trackbacks'] ) ) {
60 add_filter( 'pings_open', '__return_false', 9999 );
61 }
62
63 // Close old comments (high priority to override WP native if needed)
64 if ( ! empty( $this->options['close_old_comments'] ) ) {
65 add_filter( 'comments_open', array( $this, 'close_old_comments' ), 9999, 2 );
66 }
67
68 // Honeypot
69 if ( ! empty( $this->options['honeypot_enabled'] ) ) {
70 add_action( 'comment_form', array( $this, 'add_honeypot_field' ) );
71 add_filter( 'preprocess_comment', array( $this, 'check_honeypot' ) );
72 }
73
74 // Link limit check
75 if ( ! empty( $this->options['link_limit'] ) ) {
76 add_filter( 'preprocess_comment', array( $this, 'check_link_limit' ) );
77 }
78
79 // Block patterns
80 if ( ! empty( $this->options['block_patterns'] ) ) {
81 add_filter( 'preprocess_comment', array( $this, 'check_blocked_patterns' ) );
82 }
83
84 // Block IPs
85 if ( ! empty( $this->options['block_ips'] ) ) {
86 add_filter( 'preprocess_comment', array( $this, 'check_blocked_ips' ) );
87 }
88 }
89
90 /**
91 * Disable pingback XML-RPC methods
92 *
93 * @param array $methods XML-RPC methods.
94 * @return array
95 */
96 public function disable_pingback_methods( $methods ) {
97 unset( $methods['pingback.ping'] );
98 unset( $methods['pingback.extensions.getPingbacks'] );
99 return $methods;
100 }
101
102 /**
103 * Remove X-Pingback header
104 *
105 * @param array $headers HTTP headers.
106 * @return array
107 */
108 public function remove_pingback_header( $headers ) {
109 unset( $headers['X-Pingback'] );
110 return $headers;
111 }
112
113 /**
114 * Close comments on old posts
115 *
116 * @param bool $open Whether comments are open.
117 * @param int $post_id Post ID.
118 * @return bool
119 */
120 public function close_old_comments( $open, $post_id ) {
121 $post = get_post( $post_id );
122
123 if ( ! $post ) {
124 return $open;
125 }
126
127 // Don't close comments on WooCommerce products (reviews)
128 if ( 'product' === $post->post_type ) {
129 return 'open' === $post->comment_status;
130 }
131
132 if ( ! $open ) {
133 return $open;
134 }
135
136 $days = absint( $this->options['close_after_days'] ?? 30 );
137 $post_date = strtotime( $post->post_date );
138 $cutoff = strtotime( "-{$days} days" );
139
140 if ( $post_date < $cutoff ) {
141 return false;
142 }
143
144 return $open;
145 }
146
147 /**
148 * Add honeypot field to comment form
149 */
150 public function add_honeypot_field() {
151 ?>
152 <p class="vigilante-hp-field" style="display:none !important;">
153 <label for="vigilante_hp_website"><?php esc_html_e( 'Website', 'vigilante' ); ?></label>
154 <input type="text" name="vigilante_hp_website" id="vigilante_hp_website" value="" autocomplete="off" tabindex="-1" />
155 </p>
156 <?php
157 }
158
159 /**
160 * Check honeypot field
161 *
162 * @param array $commentdata Comment data.
163 * @return array
164 */
165 public function check_honeypot( $commentdata ) {
166 // phpcs:ignore WordPress.Security.NonceVerification.Missing
167 if ( ! empty( $_POST['vigilante_hp_website'] ) ) {
168 wp_die(
169 esc_html__( 'Your comment could not be submitted. Please try again.', 'vigilante' ),
170 esc_html__( 'Comment Blocked', 'vigilante' ),
171 array( 'response' => 403, 'back_link' => true )
172 );
173 }
174
175 return $commentdata;
176 }
177
178 /**
179 * Check for excessive links in comment
180 *
181 * @param array $commentdata Comment data.
182 * @return array
183 */
184 public function check_link_limit( $commentdata ) {
185 $limit = absint( $this->options['link_limit'] ?? 2 );
186 $content = $commentdata['comment_content'];
187
188 // Count links
189 $link_count = preg_match_all( '/<a\s/i', $content, $matches );
190 $link_count += preg_match_all( '/https?:\/\//i', $content, $matches );
191
192 // Remove duplicates from the count
193 $link_count = $link_count / 2;
194
195 if ( $link_count > $limit ) {
196 wp_die(
197 sprintf(
198 /* translators: %d: Maximum number of links allowed */
199 esc_html__( 'Your comment contains too many links. Maximum allowed: %d', 'vigilante' ),
200 absint( $limit )
201 ),
202 esc_html__( 'Comment Blocked', 'vigilante' ),
203 array( 'response' => 403, 'back_link' => true )
204 );
205 }
206
207 return $commentdata;
208 }
209
210 /**
211 * Check for blocked patterns in comment
212 *
213 * @param array $commentdata Comment data.
214 * @return array
215 */
216 public function check_blocked_patterns( $commentdata ) {
217 $patterns = $this->options['block_patterns'] ?? array();
218
219 if ( empty( $patterns ) ) {
220 return $commentdata;
221 }
222
223 $content = strtolower( $commentdata['comment_content'] . ' ' . $commentdata['comment_author'] );
224
225 foreach ( $patterns as $pattern ) {
226 $pattern = trim( strtolower( $pattern ) );
227 if ( ! empty( $pattern ) && strpos( $content, $pattern ) !== false ) {
228 wp_die(
229 esc_html__( 'Your comment could not be submitted. It contains blocked content.', 'vigilante' ),
230 esc_html__( 'Comment Blocked', 'vigilante' ),
231 array( 'response' => 403, 'back_link' => true )
232 );
233 }
234 }
235
236 return $commentdata;
237 }
238
239 /**
240 * Check for blocked IPs
241 *
242 * @param array $commentdata Comment data.
243 * @return array
244 */
245 public function check_blocked_ips( $commentdata ) {
246 $blocked_ips = $this->options['block_ips'] ?? array();
247
248 if ( empty( $blocked_ips ) ) {
249 return $commentdata;
250 }
251
252 $commenter_ip = isset( $_SERVER['REMOTE_ADDR'] )
253 ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) )
254 : '';
255
256 foreach ( $blocked_ips as $blocked_ip ) {
257 $blocked_ip = trim( $blocked_ip );
258 if ( $blocked_ip === $commenter_ip ) {
259 wp_die(
260 esc_html__( 'Your comment could not be submitted.', 'vigilante' ),
261 esc_html__( 'Comment Blocked', 'vigilante' ),
262 array( 'response' => 403, 'back_link' => true )
263 );
264 }
265 }
266
267 return $commentdata;
268 }
269
270 /**
271 * Apply comment security settings to WordPress options
272 */
273 public function apply_settings() {
274 // Disable pingbacks
275 if ( ! empty( $this->options['disable_pingbacks'] ) ) {
276 update_option( 'default_pingback_flag', 0 );
277 }
278
279 // Disable trackbacks
280 if ( ! empty( $this->options['disable_trackbacks'] ) ) {
281 update_option( 'default_ping_status', 'closed' );
282 }
283
284 // Require moderation
285 if ( ! empty( $this->options['require_moderation'] ) ) {
286 update_option( 'comment_moderation', 1 );
287 }
288
289 // Require name and email
290 if ( ! empty( $this->options['require_name_email'] ) ) {
291 update_option( 'require_name_email', 1 );
292 }
293
294 // Require registration
295 if ( ! empty( $this->options['require_registration'] ) ) {
296 update_option( 'comment_registration', 1 );
297 }
298 }
299
300 /**
301 * Get spam statistics
302 *
303 * @return array
304 */
305 public function get_spam_stats() {
306 global $wpdb;
307
308 $stats = array(
309 'total_comments' => 0,
310 'approved' => 0,
311 'pending' => 0,
312 'spam' => 0,
313 'trash' => 0,
314 );
315
316 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
317 $counts = $wpdb->get_results(
318 "SELECT comment_approved, COUNT(*) as count FROM {$wpdb->comments} GROUP BY comment_approved",
319 ARRAY_A
320 );
321
322 foreach ( $counts as $count ) {
323 switch ( $count['comment_approved'] ) {
324 case '1':
325 $stats['approved'] = absint( $count['count'] );
326 break;
327 case '0':
328 $stats['pending'] = absint( $count['count'] );
329 break;
330 case 'spam':
331 $stats['spam'] = absint( $count['count'] );
332 break;
333 case 'trash':
334 $stats['trash'] = absint( $count['count'] );
335 break;
336 }
337 }
338
339 $stats['total_comments'] = $stats['approved'] + $stats['pending'] + $stats['spam'] + $stats['trash'];
340
341 return $stats;
342 }
343 }