PluginProbe
bbPress / 2.6.19
bbPress v2.6.19
2.6.19 2.6.18 2.6.17 trunk 2.0 2.0-beta-1 2.0-beta-2b 2.0-beta-3 2.0-beta-3b 2.0-rc-2 2.0-rc-3 2.0-rc-4 2.0-rc-5 2.0.1 2.0.2 2.0.3 2.1 2.1-beta-1 2.1-rc1 2.1-rc2 2.1-rc3 2.1-rc4 2.1.1 2.1.2 2.1.3 All 74 releases
bbpress / includes / extend / akismet.php

akismet.php in bbPress 2.6.19, at includes/extend/akismet.php

1,309 lines 37.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Main bbPress Akismet Class
5 *
6 * @package bbPress
7 * @subpackage Akismet
8 */
9
10 // Exit if accessed directly
11 defined( 'ABSPATH' ) || exit;
12
13 if ( ! class_exists( 'BBP_Akismet' ) ) :
14 /**
15 * Loads Akismet extension
16 *
17 * @since 2.0.0 bbPress (r3278)
18 *
19 * @package bbPress
20 * @subpackage Akismet
21 */
22 class BBP_Akismet {
23
24 /**
25 * The last post checked by Akismet.
26 *
27 * @since 2.6.10 bbPress (r7252)
28 *
29 * @var array $last_post Default empty array.
30 */
31 protected $last_post = array();
32
33 /**
34 * The main bbPress Akismet loader
35 *
36 * @since 2.0.0 bbPress (r3278)
37 */
38 public function __construct() {
39 $this->setup_actions();
40 }
41
42 /**
43 * Setup the admin hooks
44 *
45 * @since 2.0.0 bbPress (r3376)
46 *
47 * @access private
48 */
49 private function setup_actions() {
50
51 // Prevent debug notices
52 $checks = array();
53
54 // bbPress functions to check for spam
55 $checks['check'] = array(
56 'bbp_new_topic_pre_insert' => 1, // New topic check
57 'bbp_new_reply_pre_insert' => 1, // New reply check
58 'bbp_edit_topic_pre_insert' => 1, // Edit topic check
59 'bbp_edit_reply_pre_insert' => 1 // Edit reply check
60 );
61
62 // bbPress functions for spam and ham submissions
63 $checks['submit'] = array(
64 'bbp_spammed_topic' => 10, // Spammed topic
65 'bbp_unspammed_topic' => 10, // Unspammed reply
66 'bbp_spammed_reply' => 10, // Spammed reply
67 'bbp_unspammed_reply' => 10, // Unspammed reply
68 );
69
70 // Add the checks
71 foreach ( $checks as $type => $functions ) {
72 foreach ( $functions as $function => $priority ) {
73 add_filter( $function, array( $this, $type . '_post' ), $priority );
74 }
75 }
76
77 // Update post meta
78 add_action( 'wp_insert_post', array( $this, 'update_post_meta' ), 10, 2 );
79
80 // Cleanup
81 add_action( 'akismet_scheduled_delete', array( $this, 'delete_old_spam' ) );
82 add_action( 'akismet_scheduled_delete', array( $this, 'delete_old_spam_meta' ) );
83 add_action( 'akismet_scheduled_delete', array( $this, 'delete_orphaned_spam_meta' ) );
84
85 // Admin
86 if ( is_admin() ) {
87 add_action( 'add_meta_boxes', array( $this, 'add_metaboxes' ) );
88 }
89 }
90
91 /**
92 * Converts topic/reply data into Akismet comment checking format
93 *
94 * @since 2.0.0 bbPress (r3278)
95 *
96 * @param array $post_data
97 *
98 * @return array Array of post data
99 */
100 public function check_post( $post_data = array() ) {
101
102 // Define local variables
103 $user_data = array();
104 $post_permalink = '';
105
106 // Cast the post_author to 0 if it's empty
107 if ( empty( $post_data['post_author'] ) ) {
108 $post_data['post_author'] = 0;
109 }
110
111 /** Author ************************************************************/
112
113 $user_data['last_active'] = '';
114 $user_data['registered'] = date( 'Y-m-d H:i:s' ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
115 $user_data['total_posts'] = (int) bbp_get_user_post_count( $post_data['post_author'] );
116
117 // Get user data
118 $userdata = get_userdata( $post_data['post_author'] );
119
120 // Author has a WordPress account
121 if ( ! empty( $userdata ) ) {
122 $user_data['name'] = $userdata->display_name;
123 $user_data['email'] = $userdata->user_email;
124 $user_data['website'] = $userdata->user_url;
125 $user_data['registered'] = $userdata->user_registered;
126
127 } else {
128 $anonymous_data = bbp_filter_anonymous_post_data();
129 $valid_anonymous = ! bbp_has_errors();
130
131 $user_data['name'] = $valid_anonymous ? $anonymous_data['bbp_anonymous_name'] : '';
132 $user_data['email'] = $valid_anonymous ? $anonymous_data['bbp_anonymous_email'] : '';
133 $user_data['website'] = $valid_anonymous ? $anonymous_data['bbp_anonymous_website'] : '';
134 }
135
136 /** Post **************************************************************/
137
138 if ( ! empty( $post_data['post_parent'] ) ) {
139
140 // Use post parent for permalink
141 $post_permalink = get_permalink( $post_data['post_parent'] );
142
143 // Use post parent to get datetime of last reply on this topic
144 $reply_id = bbp_get_topic_last_reply_id( $post_data['post_parent'] );
145 if ( ! empty( $reply_id ) ) {
146 $user_data['last_active'] = get_post_field( 'post_date', $reply_id );
147 }
148 }
149
150 // Pass title & content together into comment content
151 $_post_content = trim( $post_data['post_title'] . "\n\n" . $post_data['post_content'] );
152
153 // Check if the post data is spammy...
154 $_post = $this->maybe_spam(
155 array(
156 'comment_author' => $user_data['name'],
157 'comment_author_email' => $user_data['email'],
158 'comment_author_url' => $user_data['website'],
159 'comment_content' => $_post_content,
160 'comment_post_ID' => $post_data['post_parent'],
161 'comment_type' => $post_data['post_type'],
162 'comment_total' => $user_data['total_posts'],
163 'comment_last_active_gmt' => $user_data['last_active'],
164 'comment_account_registered_gmt' => $user_data['registered'],
165 'permalink' => $post_permalink,
166 'referrer' => wp_get_raw_referer(),
167 'user_agent' => bbp_current_author_ua(),
168 'user_ID' => $post_data['post_author'],
169 'user_ip' => bbp_current_author_ip(),
170 'user_role' => $this->get_user_roles( $post_data['post_author'] ),
171 )
172 );
173
174 // Set the results (from maybe_spam() above)
175 $post_data['bbp_akismet_result_headers'] = $_post['bbp_akismet_result_headers'];
176 $post_data['bbp_akismet_result'] = $_post['bbp_akismet_result'];
177 // Intentionally retain extra form and server fields for later Akismet reports.
178 // Third-party plugins may add fields that help identify spam.
179 $post_data['bbp_post_as_submitted'] = $_post;
180
181 // Avoid recursion by unsetting results from post-as-submitted
182 unset(
183 $post_data['bbp_post_as_submitted']['bbp_akismet_result_headers'],
184 $post_data['bbp_post_as_submitted']['bbp_akismet_result']
185 );
186
187 // Allow post_data to be manipulated
188 $post_data = apply_filters( 'bbp_akismet_check_post', $post_data );
189
190 // Parse and log the last response
191 $this->last_post = $this->parse_response( $post_data );
192
193 // Return the last response back to the filter
194 return $this->last_post;
195 }
196
197 /**
198 * Parse the response from the Akismet service, and alter the post data as
199 * necessary. For example, switch the status to `spam` if spammy.
200 *
201 * Note: this method also is responsible for allowing users who can moderate to
202 * never have their posts marked as spam. This is because they are "trusted"
203 * users. However, their posts are still sent to Akismet to be checked.
204 *
205 * @since 2.6.0 bbPress (r6874)
206 *
207 * @param array $post_data
208 *
209 * @return array
210 */
211 private function parse_response( $post_data = array() ) {
212
213 // Get the parent ID of the post as submitted
214 $parent_id = ! empty( $post_data['bbp_post_as_submitted']['comment_post_ID'] )
215 ? absint( $post_data['bbp_post_as_submitted']['comment_post_ID'] )
216 : 0;
217
218 // Allow moderators to skip spam (includes per-forum moderators via $parent_id)
219 $skip_spam = current_user_can( 'moderate', $parent_id );
220
221 // Bail early if current user can skip spam enforcement
222 if ( apply_filters( 'bbp_bypass_spam_enforcement', $skip_spam, $post_data ) ) {
223 return $post_data;
224 }
225
226 // Discard obvious spam
227 if ( get_option( 'akismet_strictness' ) ) {
228
229 // Akismet is 100% confident this is spam
230 if (
231 ! empty( $post_data['bbp_akismet_result_headers']['x-akismet-pro-tip'] )
232 &&
233 ( 'discard' === $post_data['bbp_akismet_result_headers']['x-akismet-pro-tip'] )
234 ) {
235
236 // URL to redirect to (current, or forum root)
237 $redirect_to = ( ! empty( $_SERVER['HTTP_HOST'] ) && ! empty( $_SERVER['REQUEST_URI'] ) )
238 ? bbp_get_url_scheme() . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']
239 : bbp_get_root_url();
240
241 // Do the redirect (post data not saved!)
242 bbp_redirect( $redirect_to );
243 }
244 }
245
246 // Result is spam, so set the status as such
247 if ( 'true' === $post_data['bbp_akismet_result'] ) {
248
249 // Let plugins do their thing
250 do_action( 'bbp_akismet_spam_caught' );
251
252 // Set post_status to spam
253 $post_data['post_status'] = bbp_get_spam_status_id();
254
255 // Filter spammy tags into meta data
256 add_filter( 'bbp_new_reply_pre_set_terms', array( $this, 'filter_post_terms' ), 1, 3 );
257 }
258
259 // Return the (potentially modified) post data
260 return $post_data;
261 }
262
263 /**
264 * Submit a post for spamming or hamming
265 *
266 * @since 2.0.0 bbPress (r3308)
267 *
268 * @param int $post_id
269 *
270 * @global string $akismet_api_host
271 * @global string $akismet_api_port
272 * @global object $current_user
273 * @global object $current_site
274 *
275 * @return array Array of existing topic terms
276 */
277 public function submit_post( $post_id = 0 ) {
278 global $current_user, $current_site;
279
280 // Innocent until proven guilty
281 $request_type = 'ham';
282 $current_filter = current_filter();
283
284 // Check this filter and adjust the $request_type accordingly
285 switch ( $current_filter ) {
286
287 // Mysterious, and straight from the can
288 case 'bbp_spammed_topic' :
289 case 'bbp_spammed_reply' :
290 $request_type = 'spam';
291 break;
292
293 // Honey-glazed, a straight off the bone
294 case 'bbp_unspammed_topic' :
295 case 'bbp_unspammed_reply' :
296 $request_type = 'ham';
297 break;
298
299 // Possibly poison...
300 default :
301 return;
302 }
303
304 // Setup some variables
305 $post_id = (int) $post_id;
306
307 // Make sure we have a post
308 $_post = get_post( $post_id );
309
310 // Bail if get_post() fails
311 if ( empty( $_post ) ) {
312 return;
313 }
314
315 // Bail if we're spamming, but the post_status isn't spam
316 if ( ( 'spam' === $request_type ) && ( bbp_get_spam_status_id() !== $_post->post_status ) ) {
317 return;
318 }
319
320 // Pass title & content together into comment content
321 $_post_content = trim( $_post->post_title . "\n\n" . $_post->post_content );
322
323 // Set some default post_data
324 $post_data = array(
325 'comment_approved' => $_post->post_status,
326 'comment_author' => $_post->post_author ? get_the_author_meta( 'display_name', $_post->post_author ) : get_post_meta( $post_id, '_bbp_anonymous_name', true ),
327 'comment_author_email' => $_post->post_author ? get_the_author_meta( 'email', $_post->post_author ) : get_post_meta( $post_id, '_bbp_anonymous_email', true ),
328 'comment_author_url' => $_post->post_author ? bbp_get_user_profile_url( $_post->post_author ) : get_post_meta( $post_id, '_bbp_anonymous_website', true ),
329 'comment_content' => $_post_content,
330 'comment_date_gmt' => $_post->post_date_gmt,
331 'comment_ID' => $post_id,
332 'comment_post_ID' => $_post->post_parent,
333 'comment_type' => $_post->post_type,
334 'permalink' => get_permalink( $post_id ),
335 'user_ID' => $_post->post_author,
336 'user_ip' => get_post_meta( $post_id, '_bbp_author_ip', true ),
337 'user_role' => $this->get_user_roles( $_post->post_author ),
338 );
339
340 // Use the original version stored in post_meta if available
341 $as_submitted = get_post_meta( $post_id, '_bbp_akismet_as_submitted', true );
342 if ( $as_submitted && is_array( $as_submitted ) && isset( $as_submitted['comment_content'] ) ) {
343 $post_data = array_merge( $post_data, $as_submitted );
344 }
345
346 // Add the reporter IP address
347 $post_data['reporter_ip'] = bbp_current_author_ip();
348
349 // Add some reporter info
350 if ( is_object( $current_user ) ) {
351 $post_data['reporter'] = $current_user->user_login;
352 }
353
354 // Add the current site domain
355 if ( is_object( $current_site ) ) {
356 $post_data['site_domain'] = $current_site->domain;
357 }
358
359 // Place your slide beneath the microscope
360 $post_data = $this->maybe_spam( $post_data, 'submit', $request_type );
361
362 // Manual user action
363 if ( isset( $post_data['reporter'] ) ) {
364
365 // What kind of action
366 switch ( $request_type ) {
367
368 // Spammy
369 case 'spam' :
370 if ( 'topic' === $post_data['comment_type'] ) {
371 /* translators: %s: reporter name */
372 $message = sprintf( esc_html__( '%s reported this topic as spam', 'bbpress' ),
373 $post_data['reporter']
374 );
375 } elseif ( 'reply' === $post_data['comment_type'] ) {
376 /* translators: %s: reporter name */
377 $message = sprintf( esc_html__( '%s reported this reply as spam', 'bbpress' ),
378 $post_data['reporter']
379 );
380 } else {
381 /* translators: 1: reporter name, 2: comment type */
382 $message = sprintf( esc_html__( '%1$s reported this %2$s as spam', 'bbpress' ),
383 $post_data['reporter'],
384 $post_data['comment_type']
385 );
386 }
387
388 $this->update_post_history( $post_id, $message, 'report-spam' );
389 update_post_meta( $post_id, '_bbp_akismet_user_result', 'true' );
390 update_post_meta( $post_id, '_bbp_akismet_user', $post_data['reporter'] );
391 break;
392
393 // Hammy
394 case 'ham' :
395 if ( 'topic' === $post_data['comment_type'] ) {
396 /* translators: %s: reporter name */
397 $message = sprintf( esc_html__( '%s reported this topic as not spam', 'bbpress' ),
398 $post_data['reporter']
399 );
400 } elseif ( 'reply' === $post_data['comment_type'] ) {
401 /* translators: %s: reporter name */
402 $message = sprintf( esc_html__( '%s reported this reply as not spam', 'bbpress' ),
403 $post_data['reporter']
404 );
405 } else {
406 /* translators: 1: reporter name, 2: comment type */
407 $message = sprintf( esc_html__( '%1$s reported this %2$s as not spam', 'bbpress' ),
408 $post_data['reporter'],
409 $post_data['comment_type']
410 );
411 }
412
413 $this->update_post_history( $post_id, $message, 'report-ham' );
414 update_post_meta( $post_id, '_bbp_akismet_user_result', 'false' );
415 update_post_meta( $post_id, '_bbp_akismet_user', $post_data['reporter'] );
416
417 // @todo Topic term revision history
418 break;
419
420 // Possible other actions
421 default :
422 break;
423 }
424 }
425
426 do_action( 'bbp_akismet_submit_' . $request_type . '_post', $post_id, $post_data['bbp_akismet_result'] );
427 }
428
429 /**
430 * Ping Akismet service and check for spam/ham response
431 *
432 * @since 2.0.0 bbPress (r3278)
433 *
434 * @param array $post_data
435 * @param string $check Accepts check|submit
436 * @param string $spam Accepts spam|ham
437 *
438 * @global string $akismet_api_host
439 * @global string $akismet_api_port
440 *
441 * @return array Array of post data
442 */
443 private function maybe_spam( $post_data = array(), $check = 'check', $spam = 'spam' ) {
444 global $akismet_api_host, $akismet_api_port;
445
446 // Define variables
447 $query_string = $path = '';
448 $response = array( '', '' );
449
450 // Make sure post data is an array
451 if ( ! is_array( $post_data ) ) {
452 $post_data = array();
453 }
454
455 // Populate post data
456 $post_data['blog'] = get_option( 'home' );
457 $post_data['blog_charset'] = get_option( 'blog_charset' );
458 $post_data['blog_lang'] = get_locale();
459 $post_data['referrer'] = wp_get_raw_referer();
460 $post_data['user_agent'] = bbp_current_author_ua();
461
462 // Keep all form fields, including third-party fields, as Akismet signals.
463 if ( ! empty( $_POST ) && is_countable( $_POST ) ) {
464 foreach ( $_POST as $key => $value ) {
465 if ( is_string( $value ) ) {
466 $post_data[ 'POST_' . $key ] = $value;
467 }
468 }
469 }
470
471 // Keep server fields for third-party signals, but omit known credential keys.
472 if ( ! empty( $_SERVER ) && is_countable( $_SERVER ) ) {
473 foreach ( $_SERVER as $key => $value ) {
474 if ( is_string( $value ) && ! $this->is_sensitive_server_key( $key ) ) {
475 $post_data[ $key ] = $value;
476 } else {
477 $post_data[ $key ] = '';
478 }
479 }
480 }
481
482 // Encode post data
483 if ( ! empty( $post_data ) && is_countable( $post_data ) ) {
484 foreach ( $post_data as $key => $data ) {
485 $query_string .= $key . '=' . urlencode( wp_unslash( $data ) ) . '&';
486 }
487 }
488
489 // Only accepts spam|ham
490 if ( ! in_array( $spam, array( 'spam', 'ham' ), true ) ) {
491 $spam = 'spam';
492 }
493
494 // Setup the API route
495 if ( 'check' === $check ) {
496 $path = '/1.1/comment-check';
497 } elseif ( 'submit' === $check ) {
498 $path = '/1.1/submit-' . $spam;
499 }
500
501 // Send data to Akismet
502 if ( ! apply_filters( 'bbp_bypass_check_for_spam', false, $post_data ) ) {
503 $response = $this->http_post( $query_string, $akismet_api_host, $path, $akismet_api_port );
504 }
505
506 // Set the result headers
507 $post_data['bbp_akismet_result_headers'] = ! empty( $response[0] )
508 ? $response[0] // raw
509 : esc_html__( 'No response', 'bbpress' );
510
511 // Set the result
512 $post_data['bbp_akismet_result'] = ! empty( $response[1] )
513 ? $response[1] // raw
514 : esc_html__( 'No response', 'bbpress' );
515
516 // Return the post data, with the results of the external Akismet request
517 return $post_data;
518 }
519
520 /**
521 * Check whether a server key may contain credentials.
522 *
523 * @since 2.6.19 bbPress (r7639)
524 *
525 * @param string $key Server key.
526 * @return bool Whether the key should be omitted.
527 */
528 private function is_sensitive_server_key( $key = '' ) {
529 $sensitive = (bool) preg_match( '/(?:^|_)(?:AUTH(?:ORIZATION)?|COOKIE[0-9]*|PASS(?:WORD|WD)?|PRIVATE|SECRET|TOKEN|NONCE|CREDENTIALS?|KEY)(?:_|$)/i', $key );
530
531 /**
532 * Filters whether a server field is omitted from Akismet requests.
533 *
534 * @since 2.6.19 bbPress (r7639)
535 *
536 * @param bool $sensitive Whether the field may contain credentials.
537 * @param string $key Server key.
538 */
539 return (bool) apply_filters( 'bbp_akismet_is_sensitive_server_key', $sensitive, $key );
540 }
541
542 /**
543 * Update post meta after a spam check
544 *
545 * @since 2.0.0 bbPress (r3308)
546 *
547 * @param int $post_id
548 * @param object $_post
549 *
550 * @global object $this->last_post
551 */
552 public function update_post_meta( $post_id = 0, $_post = false ) {
553
554 // Define local variable(s)
555 $as_submitted = false;
556
557 // Setup some variables
558 $post_id = (int) $post_id;
559
560 // Ensure we have a post object
561 if ( empty( $_post ) ) {
562 $_post = get_post( $post_id );
563 }
564
565 // Set up Akismet last post data
566 if ( ! empty( $this->last_post['bbp_post_as_submitted'] ) ) {
567 $as_submitted = $this->last_post['bbp_post_as_submitted'];
568 }
569
570 // wp_insert_post() might be called in other contexts. Ensure this is
571 // the same topic/reply as was checked by BBP_Akismet::check_post()
572 if ( is_object( $_post ) && ! empty( $this->last_post ) && is_array( $as_submitted ) ) {
573
574 // Get user data
575 $userdata = get_userdata( $_post->post_author );
576 if ( ! empty( $userdata ) ) {
577 $name = $userdata->display_name;
578 $email = $userdata->user_email;
579 } else {
580 $anonymous_data = bbp_filter_anonymous_post_data();
581 $name = ! empty( $anonymous_data['bbp_anonymous_name'] ) ? $anonymous_data['bbp_anonymous_name'] : '';
582 $email = ! empty( $anonymous_data['bbp_anonymous_email'] ) ? $anonymous_data['bbp_anonymous_email'] : '';
583 }
584
585 // More checks
586 if (
587
588 // Post matches
589 ( intval( $as_submitted['comment_post_ID'] ) === intval( $_post->post_parent ) )
590
591 &&
592
593 // Name matches
594 ( $as_submitted['comment_author'] === $name )
595
596 &&
597
598 // Email matches
599 ( $as_submitted['comment_author_email'] === $email )
600 ) {
601
602 // Delete old content daily
603 if ( ! wp_next_scheduled( 'akismet_scheduled_delete' ) ) {
604 wp_schedule_event( time(), 'daily', 'akismet_scheduled_delete' );
605 }
606
607 // Normal result: true
608 if ( ! empty( $this->last_post['bbp_akismet_result'] ) && ( 'true' === $this->last_post['bbp_akismet_result'] ) ) {
609
610 // Leave a trail so other's know what we did
611 update_post_meta( $post_id, '_bbp_akismet_result', 'true' );
612 $this->update_post_history(
613 $post_id,
614 esc_html__( 'Akismet caught this post as spam', 'bbpress' ),
615 'check-spam'
616 );
617
618 // If post_status isn't the spam status, as expected, leave a note
619 if ( bbp_get_spam_status_id() !== $_post->post_status ) {
620 $this->update_post_history(
621 $post_id,
622 sprintf(
623 /* translators: %s: Post status */
624 esc_html__( 'Post status was changed to %s', 'bbpress' ),
625 $_post->post_status
626 ),
627 'status-changed-' . $_post->post_status
628 );
629 }
630
631 // Normal result: false
632 } elseif ( ! empty( $this->last_post['bbp_akismet_result'] ) && ( 'false' === $this->last_post['bbp_akismet_result'] ) ) {
633
634 // Leave a trail so other's know what we did
635 update_post_meta( $post_id, '_bbp_akismet_result', 'false' );
636 $this->update_post_history(
637 $post_id,
638 esc_html__( 'Akismet cleared this post as not spam', 'bbpress' ),
639 'check-ham'
640 );
641
642 // If post_status is the spam status, which isn't expected, leave a note
643 if ( bbp_get_spam_status_id() === $_post->post_status ) {
644 $this->update_post_history(
645 $post_id,
646 sprintf(
647 /* translators: %s: Post status */
648 esc_html__( 'Post status was changed to %s', 'bbpress' ),
649 $_post->post_status
650 ),
651 'status-changed-' . $_post->post_status
652 );
653 }
654
655 // Abnormal result: error
656 } else {
657 // Leave a trail so other's know what we did
658 update_post_meta( $post_id, '_bbp_akismet_error', time() );
659 $this->update_post_history(
660 $post_id,
661 sprintf(
662 /* translators: %s: Akismet response */
663 esc_html__( 'Akismet was unable to check this post (response: %s), will automatically retry again later.', 'bbpress' ),
664 $this->last_post['bbp_akismet_result']
665 ),
666 'check-error'
667 );
668 }
669
670 // Record the complete original data as submitted for checking
671 if ( isset( $this->last_post['bbp_post_as_submitted'] ) ) {
672 update_post_meta(
673 $post_id,
674 '_bbp_akismet_as_submitted',
675 $this->last_post['bbp_post_as_submitted']
676 );
677 }
678 }
679 }
680 }
681
682 /**
683 * Update Akismet history of a Post
684 *
685 * @since 2.0.0 bbPress (r3308)
686 *
687 * @param int $post_id
688 * @param string $message
689 * @param string $event
690 */
691 private function update_post_history( $post_id = 0, $message = null, $event = null ) {
692
693 // Define local variable(s)
694 $user = '';
695
696 // Get the current user
697 $current_user = wp_get_current_user();
698
699 // Get the user's login name if possible
700 if ( is_object( $current_user ) && isset( $current_user->user_login ) ) {
701 $user = $current_user->user_login;
702 }
703
704 // This used to be akismet_microtime() but it was removed in 3.0
705 $mtime = explode( ' ', microtime() );
706 $message_time = $mtime[1] + $mtime[0];
707
708 // Setup the event to be saved
709 $event = array(
710 'time' => $message_time,
711 'message' => $message,
712 'event' => $event,
713 'user' => $user,
714 );
715
716 // Save the event data
717 add_post_meta( $post_id, '_bbp_akismet_history', $event );
718 }
719
720 /**
721 * Get the Akismet history of a Post
722 *
723 * @since 2.0.0 bbPress (r3308)
724 *
725 * @param int $post_id
726 *
727 * @return array Array of Akismet history
728 */
729 public function get_post_history( $post_id = 0 ) {
730
731 // Retrieve any previous history
732 $history = get_post_meta( $post_id, '_bbp_akismet_history' );
733
734 // Sort it by the time recorded
735 usort( $history, 'akismet_cmp_time' );
736
737 return $history;
738 }
739
740 /**
741 * Handle any terms submitted with a post flagged as spam
742 *
743 * @since 2.0.0 bbPress (r3308)
744 *
745 * @param string $terms Comma-separated list of terms
746 * @param int $topic_id
747 * @param int $reply_id
748 *
749 * @return array Array of existing topic terms
750 */
751 public function filter_post_terms( $terms = '', $topic_id = 0, $reply_id = 0 ) {
752
753 // Validate the reply_id and topic_id
754 $reply_id = bbp_get_reply_id( $reply_id );
755 $topic_id = bbp_get_topic_id( $topic_id );
756
757 // Get any pre-existing terms
758 $existing_terms = bbp_get_topic_tag_names( $topic_id );
759
760 // Save the terms for later in case the reply gets hammed
761 if ( ! empty( $terms ) ) {
762 update_post_meta( $reply_id, '_bbp_akismet_spam_terms', $terms );
763 }
764
765 // Keep the topic tags the same for now
766 return $existing_terms;
767 }
768
769 /**
770 * Submit data to Akismet service with unique bbPress User Agent
771 *
772 * This code is directly taken from the akismet_http_post() function and
773 * documented to bbPress 2.0 standard.
774 *
775 * @since 2.0.0 bbPress (r3467)
776 *
777 * @param string $request The request we are sending
778 * @param string $host The host to send our request to
779 * @param string $path The path from the host
780 * @param string $port The port to use
781 * @param string $ip Optional Override $host with an IP address
782 * @return mixed WP_Error on error, array on success, empty on failure
783 */
784 private function http_post( $request, $host, $path, $port = 80, $ip = '' ) {
785
786 // Preload required variables
787 $bbp_version = bbp_get_version();
788 $ak_version = constant( 'AKISMET_VERSION' );
789 $http_host = $host;
790 $blog_charset = get_option( 'blog_charset' );
791
792 // User Agent & Content Type
793 $akismet_ua = "bbPress/{$bbp_version} | Akismet/{$ak_version}";
794 $content_type = 'application/x-www-form-urlencoded; charset=' . $blog_charset;
795
796 // Use specific IP (if provided)
797 if ( ! empty( $ip ) && long2ip( ip2long( $ip ) ) ) {
798 $http_host = $ip;
799 }
800
801 // Setup the arguments
802 $http_args = array(
803 'httpversion' => '1.0',
804 'timeout' => 15,
805 'body' => $request,
806 'headers' => array(
807 'Content-Type' => $content_type,
808 'Host' => $host,
809 'User-Agent' => $akismet_ua
810 )
811 );
812
813 // Return the response
814 return $this->get_response( $http_host . $path, $http_args );
815 }
816
817 /**
818 * Handles the repeated calls to wp_remote_post(), including SSL support.
819 *
820 * @since 2.6.7 bbPress (r7195)
821 *
822 * @param string $host_and_path Scheme-less URL
823 * @param array $http_args Array of arguments for wp_remote_post()
824 * @return array
825 */
826 private function get_response( $host_and_path = '', $http_args = array() ) {
827
828 // Default variables
829 $akismet_url = $http_akismet_url = 'http://' . $host_and_path;
830 $is_ssl = $ssl_failed = false;
831 $now = time();
832
833 // Check if SSL requests were disabled fewer than 24 hours ago
834 $ssl_disabled_time = get_option( 'akismet_ssl_disabled' );
835
836 // Clean-up if 24 hours have passed
837 if ( ! empty( $ssl_disabled_time ) && ( $ssl_disabled_time < ( $now - DAY_IN_SECONDS ) ) ) {
838 delete_option( 'akismet_ssl_disabled' );
839 $ssl_disabled_time = false;
840 }
841
842 // Maybe HTTPS if not disabled
843 if ( empty( $ssl_disabled_time ) ) {
844 $is_ssl = wp_http_supports( array( 'ssl' ) );
845
846 // Use SSL
847 if ( ! empty( $is_ssl ) ) {
848 $akismet_url = set_url_scheme( $akismet_url, 'https' );
849 }
850 }
851
852 // Initial remote request
853 $response = wp_remote_post( $akismet_url, $http_args );
854
855 // Initial request produced an error, so retry...
856 if ( ! empty( $is_ssl ) && is_wp_error( $response ) ) {
857
858 // Intermittent connection problems may cause the first HTTPS
859 // request to fail and subsequent HTTP requests to succeed randomly.
860 // Retry the HTTPS request once before disabling SSL for a time.
861 $response = wp_remote_post( $akismet_url, $http_args );
862
863 // SSL request failed twice, so try again without it
864 if ( is_wp_error( $response ) ) {
865 $response = wp_remote_post( $http_akismet_url, $http_args );
866 $ssl_failed = true;
867 }
868 }
869
870 // Bail if errored
871 if ( is_wp_error( $response ) ) {
872 return array( '', '' );
873 }
874
875 // Maybe disable SSL for future requests
876 if ( ! empty( $ssl_failed ) ) {
877 update_option( 'akismet_ssl_disabled', $now );
878 }
879
880 // No errors so return response
881 return array(
882 $response['headers'],
883 $response['body']
884 );
885 }
886
887 /**
888 * Return a user's roles on this site (including super_admin)
889 *
890 * @since 2.3.0 bbPress (r4813)
891 *
892 * @param int $user_id
893 *
894 * @return boolean
895 */
896 private function get_user_roles( $user_id = 0 ) {
897
898 // Default return value
899 $roles = array();
900
901 // Bail if cannot query the user
902 if ( ! class_exists( 'WP_User' ) || empty( $user_id ) ) {
903 return false;
904 }
905
906 // User ID
907 $user = new WP_User( $user_id );
908 if ( isset( $user->roles ) ) {
909 $roles = (array) $user->roles;
910 }
911
912 // Super admin
913 if ( is_multisite() && is_super_admin( $user_id ) ) {
914 $roles[] = 'super_admin';
915 }
916
917 return implode( ',', $roles );
918 }
919
920 /** Admin *****************************************************************/
921
922 /**
923 * Add Aksimet History meta-boxes to topics and replies
924 *
925 * @since 2.4.0 bbPress (r5050)
926 */
927 public function add_metaboxes() {
928
929 // Topics
930 add_meta_box(
931 'bbp_akismet_topic_history',
932 __( 'Akismet History', 'bbpress' ),
933 array( $this, 'history_metabox' ),
934 bbp_get_topic_post_type(),
935 'normal',
936 'core'
937 );
938
939 // Replies
940 add_meta_box(
941 'bbp_akismet_reply_history',
942 __( 'Akismet History', 'bbpress' ),
943 array( $this, 'history_metabox' ),
944 bbp_get_reply_post_type(),
945 'normal',
946 'core'
947 );
948 }
949
950 /**
951 * Output for Akismet History meta-box
952 *
953 * @since 2.4.0 bbPress (r5050)
954 */
955 public function history_metabox() {
956
957 // Post ID
958 $history = $this->get_post_history( get_the_ID() ); ?>
959
960 <div class="akismet-history" style="margin: 13px 0;">
961
962 <?php if ( ! empty( $history ) ) : ?>
963
964 <table>
965 <tbody>
966
967 <?php foreach ( $history as $row ) : ?>
968
969 <tr>
970 <td style="color: #999; text-align: right; white-space: nowrap;">
971 <span title="<?php echo esc_attr( date( 'D d M Y @ h:i:m a', $row['time'] ) . ' GMT' ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date ?>">
972 <?php bbp_time_since( $row['time'], false, true ); ?>
973 </span>
974 </td>
975 <td style="padding-left: 5px;">
976 <?php echo esc_html( $row['message'] ); ?>
977 </td>
978 </tr>
979
980 <?php endforeach; ?>
981 </tbody>
982 </table>
983
984 <?php else : ?>
985
986 <p><?php esc_html_e( 'No recorded history. Akismet has not checked this post.', 'bbpress' ); ?></p>
987
988 <?php endif; ?>
989
990 </div>
991
992 <?php
993 }
994
995 /**
996 * Get the number of rows to delete in a single clean-up query.
997 *
998 * @since 2.6.9 bbPress (r7227)
999 *
1000 * @param string $filter The name of the filter to run.
1001 * @return int
1002 */
1003 public function get_delete_limit( $filter = '' ) {
1004
1005 // Default filter
1006 if ( empty( $filter ) ) {
1007 $filter = '_bbp_akismet_delete_spam_limit';
1008 }
1009
1010 /**
1011 * Determines how many rows will be deleted in each batch.
1012 *
1013 * @param int The number of rows. Default 1000.
1014 */
1015 $delete_limit = (int) apply_filters( $filter, 1000 );
1016
1017 // Validate and return the deletion limit
1018 return max( 1, $delete_limit );
1019 }
1020
1021 /**
1022 * Get the interval (in days) for spam to remain in the queue.
1023 *
1024 * @since 2.6.9 bbPress (r7227)
1025 *
1026 * @param string $filter The name of the filter to run.
1027 * @return int
1028 */
1029 public function get_delete_interval( $filter = '' ) {
1030
1031 // Default filter
1032 if ( empty( $filter ) ) {
1033 $filter = '_bbp_akismet_delete_spam_interval';
1034 }
1035
1036 /**
1037 * Determines how many days a piece of spam will be left in the Spam
1038 * queue before being deleted.
1039 *
1040 * @param int The number of days. Default 15.
1041 */
1042 $delete_interval = (int) apply_filters( $filter, 15 );
1043
1044 // Validate and return the deletion interval
1045 return max( 1, $delete_interval );
1046 }
1047
1048 /**
1049 * Deletes old spam topics & replies from the queue after 15 days
1050 * (determined by `_bbp_akismet_delete_spam_interval` filter)
1051 * since they are not useful in the long term.
1052 *
1053 * @since 2.6.7 bbPress (r7203)
1054 *
1055 * @global wpdb $wpdb
1056 */
1057 public function delete_old_spam() {
1058 global $wpdb;
1059
1060 // Get the deletion limit & interval
1061 $delete_limit = $this->get_delete_limit( '_bbp_akismet_delete_spam_limit' );
1062 $delete_interval = $this->get_delete_interval( '_bbp_akismet_delete_spam_interval' );
1063
1064 // Setup the query
1065 $sql = "SELECT id FROM {$wpdb->posts} WHERE post_type IN ('topic', 'reply') AND post_status = 'spam' AND DATE_SUB(NOW(), INTERVAL %d DAY) > post_date_gmt LIMIT %d";
1066
1067 // Query loop of topic & reply IDs
1068 while ( $spam_ids = $wpdb->get_col( $wpdb->prepare( $sql, $delete_interval, $delete_limit ) ) ) { // phpcs:ignore
1069
1070 // Exit loop if no spam IDs
1071 if ( empty( $spam_ids ) ) {
1072 break;
1073 }
1074
1075 // Reset queries
1076 $wpdb->queries = array();
1077
1078 // Loop through each of the topic/reply IDs
1079 foreach ( $spam_ids as $spam_id ) {
1080
1081 /**
1082 * Perform a single action on the single topic/reply ID for
1083 * simpler batch processing.
1084 *
1085 * Maybe we should run the bbp_delete_topic or bbp_delete_reply
1086 * actions here, too?
1087 *
1088 * @param string The current function.
1089 * @param int The current topic/reply ID.
1090 */
1091 do_action( '_bbp_akismet_batch_delete', __FUNCTION__, $spam_id );
1092 }
1093
1094 // phpcs:disable
1095
1096 // Prepared as strings since id is an unsigned BIGINT, and using %
1097 // will constrain the value to the maximum signed BIGINT.
1098 $format_string = implode( ', ', array_fill( 0, count( $spam_ids ), '%s' ) );
1099
1100 // Run the delete queries
1101 $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->posts} WHERE ID IN ( {$format_string} )", $spam_ids ) );
1102 $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->postmeta} WHERE post_id IN ( {$format_string} )", $spam_ids ) );
1103
1104 // Clean the post cache for these topics & replies
1105 clean_post_cache( $spam_ids );
1106
1107 /**
1108 * Single action that encompasses all topic/reply IDs after the
1109 * delete queries have been run.
1110 *
1111 * @param int Count of topic/reply IDs
1112 * @param array Array of topic/reply IDs
1113 */
1114 do_action( '_bbp_akismet_delete_spam_count', count( $spam_ids ), $spam_ids );
1115 }
1116
1117 /**
1118 * Determines whether tables should be optimized.
1119 *
1120 * @param int Random number between 1 and 5000.
1121 */
1122 $optimize = (int) apply_filters( '_bbp_akismet_optimize_tables', mt_rand( 1, 5000 ), array( $wpdb->posts, $wpdb->postmeta ) );
1123
1124 // Lucky number 11
1125 if ( 11 === $optimize ) {
1126 $wpdb->query( "OPTIMIZE TABLE {$wpdb->posts}" );
1127 $wpdb->query( "OPTIMIZE TABLE {$wpdb->postmeta}" );
1128 }
1129 }
1130
1131 /**
1132 * Deletes `_bbp_akismet_as_submitted` meta keys after 15 days
1133 * (determined by `_bbp_akismet_delete_spam_meta_interval` filter)
1134 * since they are large and not useful in the long term.
1135 *
1136 * @since 2.6.7 bbPress (r7203)
1137 *
1138 * @global wpdb $wpdb
1139 */
1140 public function delete_old_spam_meta() {
1141 global $wpdb;
1142
1143 // Get the deletion limit & interval
1144 $delete_limit = $this->get_delete_limit( '_bbp_akismet_delete_spam_meta_limit' );
1145 $delete_interval = $this->get_delete_interval( '_bbp_akismet_delete_spam_meta_interval' );
1146
1147 // Setup the query
1148 $sql = "SELECT m.post_id FROM {$wpdb->postmeta} as m INNER JOIN {$wpdb->posts} as p ON m.post_id = p.ID WHERE m.meta_key = '_bbp_akismet_as_submitted' AND DATE_SUB(NOW(), INTERVAL %d DAY) > p.post_date_gmt LIMIT %d";
1149
1150 // Query loop of topic & reply IDs
1151 while ( $spam_ids = $wpdb->get_col( $wpdb->prepare( $sql, $delete_interval, $delete_limit ) ) ) { // phpcs:ignore
1152
1153 // Exit loop if no spam IDs
1154 if ( empty( $spam_ids ) ) {
1155 break;
1156 }
1157
1158 // Reset queries
1159 $wpdb->queries = array();
1160
1161 // Loop through each of the topic/reply IDs
1162 foreach ( $spam_ids as $spam_id ) {
1163
1164 // Delete the as_submitted meta data
1165 delete_post_meta( $spam_id, '_bbp_akismet_as_submitted' );
1166
1167 /**
1168 * Perform a single action on the single topic/reply ID for
1169 * simpler batch processing.
1170 *
1171 * @param string The current function.
1172 * @param int The current topic/reply ID.
1173 */
1174 do_action( '_bbp_akismet_batch_delete', __FUNCTION__, $spam_id );
1175 }
1176
1177 /**
1178 * Single action that encompasses all topic/reply IDs after the
1179 * delete queries have been run.
1180 *
1181 * @param int Count of topic/reply IDs
1182 * @param array Array of topic/reply IDs
1183 */
1184 do_action( '_bbp_akismet_delete_spam_meta_count', count( $spam_ids ), $spam_ids );
1185 }
1186
1187 // Maybe optimize
1188 $this->maybe_optimize_postmeta();
1189 }
1190
1191 /**
1192 * Clears post meta that no longer has corresponding posts in the database
1193 * (determined by `_bbp_akismet_delete_spam_orphaned_limit` filter)
1194 * since it is not useful in the long term.
1195 *
1196 * @since 2.6.7 bbPress (r7203)
1197 *
1198 * @global wpdb $wpdb
1199 */
1200 public function delete_orphaned_spam_meta() {
1201 global $wpdb;
1202
1203 // Get the deletion limit
1204 $delete_limit = $this->get_delete_limit( '_bbp_akismet_delete_spam_orphaned_limit' );
1205
1206 // Default last meta ID
1207 $last_meta_id = 0;
1208
1209 // Start time (float)
1210 $start_time = isset( $_SERVER['REQUEST_TIME_FLOAT'] )
1211 ? (float) $_SERVER['REQUEST_TIME_FLOAT']
1212 : microtime( true );
1213
1214 // Maximum time
1215 $max_exec_time = (float) max( ini_get( 'max_execution_time' ) - 5, 3 );
1216
1217 // Setup the query
1218 $sql = 'SELECT m.meta_id, m.post_id, m.meta_key'
1219 . " FROM {$wpdb->postmeta} as m"
1220 . " LEFT JOIN {$wpdb->posts} as p"
1221 . ' ON m.post_id = p.ID'
1222 . ' WHERE p.ID IS NULL'
1223 . ' AND m.meta_id > %d'
1224 . " AND m.meta_key LIKE 'akismet\\_%'"
1225 . ' ORDER BY m.meta_id'
1226 . ' LIMIT %d';
1227
1228 // Query loop of topic & reply IDs
1229 while ( $spam_meta_results = $wpdb->get_results( $wpdb->prepare( $sql, $last_meta_id, $delete_limit ) ) ) { // phpcs:ignore
1230
1231 // Exit loop if no spam IDs
1232 if ( empty( $spam_meta_results ) ) {
1233 break;
1234 }
1235
1236 // Reset queries
1237 $wpdb->queries = array();
1238
1239 // Reset deleted meta count
1240 $spam_meta_deleted = array();
1241
1242 // Loop through each of the metas
1243 foreach ( $spam_meta_results as $spam_meta ) {
1244
1245 // Skip if not an Akismet key
1246 if ( 'akismet_' !== substr( $spam_meta->meta_key, 0, 8 ) ) {
1247 continue;
1248 }
1249
1250 // Delete the meta
1251 delete_post_meta( $spam_meta->post_id, $spam_meta->meta_key );
1252
1253 /**
1254 * Perform a single action on the single topic/reply ID for
1255 * simpler batch processing.
1256 *
1257 * @param string The current function.
1258 * @param int The current topic/reply ID.
1259 */
1260 do_action( '_bbp_akismet_batch_delete', __FUNCTION__, $spam_meta );
1261
1262 // Stash the meta ID being deleted
1263 $spam_meta_deleted[] = $last_meta_id = $spam_meta->meta_id;
1264 }
1265
1266 /**
1267 * Single action that encompasses all topic/reply IDs after the
1268 * delete queries have been run.
1269 *
1270 * @param int Count of spam meta IDs
1271 * @param array Array of spam meta IDs
1272 */
1273 do_action( '_bbp_akismet_delete_spam_meta_count', count( $spam_meta_deleted ), $spam_meta_deleted );
1274
1275 // Break if getting close to max_execution_time.
1276 if ( ( microtime( true ) - $start_time ) > $max_exec_time ) {
1277 break;
1278 }
1279 }
1280
1281 // Maybe optimize
1282 $this->maybe_optimize_postmeta();
1283 }
1284
1285 /**
1286 * Maybe OPTIMIZE the _postmeta database table.
1287 *
1288 * @since 2.6.7 bbPress (r7203)
1289 *
1290 * @global wpdb $wpdb
1291 */
1292 private function maybe_optimize_postmeta() {
1293 global $wpdb;
1294
1295 /**
1296 * Determines whether tables should be optimized.
1297 *
1298 * @param int Random number between 1 and 5000.
1299 */
1300 $optimize = (int) apply_filters( '_bbp_akismet_optimize_table', mt_rand( 1, 5000 ), $wpdb->postmeta );
1301
1302 // Lucky number 11
1303 if ( 11 === $optimize ) {
1304 $wpdb->query( "OPTIMIZE TABLE {$wpdb->postmeta}" );
1305 }
1306 }
1307 }
1308 endif;
1309