[ Index ] |
PHP Cross Reference of WordPress |
[Summary view] [Print] [Text view]
1 <?php 2 3 class Akismet { 4 const API_HOST = 'rest.akismet.com'; 5 const API_PORT = 80; 6 const MAX_DELAY_BEFORE_MODERATION_EMAIL = 86400; // One day in seconds 7 8 public static $limit_notices = array( 9 10501 => 'FIRST_MONTH_OVER_LIMIT', 10 10502 => 'SECOND_MONTH_OVER_LIMIT', 11 10504 => 'THIRD_MONTH_APPROACHING_LIMIT', 12 10508 => 'THIRD_MONTH_OVER_LIMIT', 13 10516 => 'FOUR_PLUS_MONTHS_OVER_LIMIT', 14 ); 15 16 private static $last_comment = ''; 17 private static $initiated = false; 18 private static $prevent_moderation_email_for_these_comments = array(); 19 private static $last_comment_result = null; 20 private static $comment_as_submitted_allowed_keys = array( 'blog' => '', 'blog_charset' => '', 'blog_lang' => '', 'blog_ua' => '', 'comment_agent' => '', 'comment_author' => '', 'comment_author_IP' => '', 'comment_author_email' => '', 'comment_author_url' => '', 'comment_content' => '', 'comment_date_gmt' => '', 'comment_tags' => '', 'comment_type' => '', 'guid' => '', 'is_test' => '', 'permalink' => '', 'reporter' => '', 'site_domain' => '', 'submit_referer' => '', 'submit_uri' => '', 'user_ID' => '', 'user_agent' => '', 'user_id' => '', 'user_ip' => '' ); 21 22 public static function init() { 23 if ( ! self::$initiated ) { 24 self::init_hooks(); 25 } 26 } 27 28 /** 29 * Initializes WordPress hooks 30 */ 31 private static function init_hooks() { 32 self::$initiated = true; 33 34 add_action( 'wp_insert_comment', array( 'Akismet', 'auto_check_update_meta' ), 10, 2 ); 35 add_filter( 'preprocess_comment', array( 'Akismet', 'auto_check_comment' ), 1 ); 36 add_filter( 'rest_pre_insert_comment', array( 'Akismet', 'rest_auto_check_comment' ), 1 ); 37 38 add_action( 'akismet_scheduled_delete', array( 'Akismet', 'delete_old_comments' ) ); 39 add_action( 'akismet_scheduled_delete', array( 'Akismet', 'delete_old_comments_meta' ) ); 40 add_action( 'akismet_scheduled_delete', array( 'Akismet', 'delete_orphaned_commentmeta' ) ); 41 add_action( 'akismet_schedule_cron_recheck', array( 'Akismet', 'cron_recheck' ) ); 42 43 add_action( 'comment_form', array( 'Akismet', 'add_comment_nonce' ), 1 ); 44 add_action( 'comment_form', array( 'Akismet', 'output_custom_form_fields' ) ); 45 46 add_filter( 'comment_moderation_recipients', array( 'Akismet', 'disable_moderation_emails_if_unreachable' ), 1000, 2 ); 47 add_filter( 'pre_comment_approved', array( 'Akismet', 'last_comment_status' ), 10, 2 ); 48 49 add_action( 'transition_comment_status', array( 'Akismet', 'transition_comment_status' ), 10, 3 ); 50 51 // Run this early in the pingback call, before doing a remote fetch of the source uri 52 add_action( 'xmlrpc_call', array( 'Akismet', 'pre_check_pingback' ) ); 53 54 // Jetpack compatibility 55 add_filter( 'jetpack_options_whitelist', array( 'Akismet', 'add_to_jetpack_options_whitelist' ) ); 56 add_filter( 'jetpack_contact_form_html', array( 'Akismet', 'inject_custom_form_fields' ) ); 57 add_filter( 'jetpack_contact_form_akismet_values', array( 'Akismet', 'prepare_custom_form_values' ) ); 58 59 // Gravity Forms 60 add_filter( 'gform_get_form_filter', array( 'Akismet', 'inject_custom_form_fields' ) ); 61 add_filter( 'gform_akismet_fields', array( 'Akismet', 'prepare_custom_form_values' ) ); 62 63 // Contact Form 7 64 add_filter( 'wpcf7_form_elements', array( 'Akismet', 'append_custom_form_fields' ) ); 65 add_filter( 'wpcf7_akismet_parameters', array( 'Akismet', 'prepare_custom_form_values' ) ); 66 67 // Formidable Forms 68 add_filter( 'frm_filter_final_form', array( 'Akismet', 'inject_custom_form_fields' ) ); 69 add_filter( 'frm_akismet_values', array( 'Akismet', 'prepare_custom_form_values' ) ); 70 71 // Fluent Forms 72 add_filter( 'fluentform_form_element_start', array( 'Akismet', 'output_custom_form_fields' ) ); 73 add_filter( 'fluentform_akismet_fields', array( 'Akismet', 'prepare_custom_form_values' ), 10, 2 ); 74 75 add_action( 'update_option_wordpress_api_key', array( 'Akismet', 'updated_option' ), 10, 2 ); 76 add_action( 'add_option_wordpress_api_key', array( 'Akismet', 'added_option' ), 10, 2 ); 77 78 add_action( 'comment_form_after', array( 'Akismet', 'display_comment_form_privacy_notice' ) ); 79 } 80 81 public static function get_api_key() { 82 return apply_filters( 'akismet_get_api_key', defined('WPCOM_API_KEY') ? constant('WPCOM_API_KEY') : get_option('wordpress_api_key') ); 83 } 84 85 public static function check_key_status( $key, $ip = null ) { 86 return self::http_post( Akismet::build_query( array( 'key' => $key, 'blog' => get_option( 'home' ) ) ), 'verify-key', $ip ); 87 } 88 89 public static function verify_key( $key, $ip = null ) { 90 // Shortcut for obviously invalid keys. 91 if ( strlen( $key ) != 12 ) { 92 return 'invalid'; 93 } 94 95 $response = self::check_key_status( $key, $ip ); 96 97 if ( $response[1] != 'valid' && $response[1] != 'invalid' ) 98 return 'failed'; 99 100 return $response[1]; 101 } 102 103 public static function deactivate_key( $key ) { 104 $response = self::http_post( Akismet::build_query( array( 'key' => $key, 'blog' => get_option( 'home' ) ) ), 'deactivate' ); 105 106 if ( $response[1] != 'deactivated' ) 107 return 'failed'; 108 109 return $response[1]; 110 } 111 112 /** 113 * Add the akismet option to the Jetpack options management whitelist. 114 * 115 * @param array $options The list of whitelisted option names. 116 * @return array The updated whitelist 117 */ 118 public static function add_to_jetpack_options_whitelist( $options ) { 119 $options[] = 'wordpress_api_key'; 120 return $options; 121 } 122 123 /** 124 * When the akismet option is updated, run the registration call. 125 * 126 * This should only be run when the option is updated from the Jetpack/WP.com 127 * API call, and only if the new key is different than the old key. 128 * 129 * @param mixed $old_value The old option value. 130 * @param mixed $value The new option value. 131 */ 132 public static function updated_option( $old_value, $value ) { 133 // Not an API call 134 if ( ! class_exists( 'WPCOM_JSON_API_Update_Option_Endpoint' ) ) { 135 return; 136 } 137 // Only run the registration if the old key is different. 138 if ( $old_value !== $value ) { 139 self::verify_key( $value ); 140 } 141 } 142 143 /** 144 * Treat the creation of an API key the same as updating the API key to a new value. 145 * 146 * @param mixed $option_name Will always be "wordpress_api_key", until something else hooks in here. 147 * @param mixed $value The option value. 148 */ 149 public static function added_option( $option_name, $value ) { 150 if ( 'wordpress_api_key' === $option_name ) { 151 return self::updated_option( '', $value ); 152 } 153 } 154 155 public static function rest_auto_check_comment( $commentdata ) { 156 return self::auto_check_comment( $commentdata, 'rest_api' ); 157 } 158 159 /** 160 * Check a comment for spam. 161 * 162 * @param array $commentdata 163 * @param string $context What kind of request triggered this comment check? Possible values are 'default', 'rest_api', and 'xml-rpc'. 164 * @return array|WP_Error Either the $commentdata array with additional entries related to its spam status 165 * or a WP_Error, if it's a REST API request and the comment should be discarded. 166 */ 167 public static function auto_check_comment( $commentdata, $context = 'default' ) { 168 // If no key is configured, then there's no point in doing any of this. 169 if ( ! self::get_api_key() ) { 170 return $commentdata; 171 } 172 173 self::$last_comment_result = null; 174 175 $comment = $commentdata; 176 177 $comment['user_ip'] = self::get_ip_address(); 178 $comment['user_agent'] = self::get_user_agent(); 179 $comment['referrer'] = self::get_referer(); 180 $comment['blog'] = get_option( 'home' ); 181 $comment['blog_lang'] = get_locale(); 182 $comment['blog_charset'] = get_option('blog_charset'); 183 $comment['permalink'] = get_permalink( $comment['comment_post_ID'] ); 184 185 if ( ! empty( $comment['user_ID'] ) ) { 186 $comment['user_role'] = Akismet::get_user_roles( $comment['user_ID'] ); 187 } 188 189 /** See filter documentation in init_hooks(). */ 190 $akismet_nonce_option = apply_filters( 'akismet_comment_nonce', get_option( 'akismet_comment_nonce' ) ); 191 $comment['akismet_comment_nonce'] = 'inactive'; 192 if ( $akismet_nonce_option == 'true' || $akismet_nonce_option == '' ) { 193 $comment['akismet_comment_nonce'] = 'failed'; 194 if ( isset( $_POST['akismet_comment_nonce'] ) && wp_verify_nonce( $_POST['akismet_comment_nonce'], 'akismet_comment_nonce_' . $comment['comment_post_ID'] ) ) 195 $comment['akismet_comment_nonce'] = 'passed'; 196 197 // comment reply in wp-admin 198 if ( isset( $_POST['_ajax_nonce-replyto-comment'] ) && check_ajax_referer( 'replyto-comment', '_ajax_nonce-replyto-comment' ) ) 199 $comment['akismet_comment_nonce'] = 'passed'; 200 201 } 202 203 if ( self::is_test_mode() ) 204 $comment['is_test'] = 'true'; 205 206 foreach( $_POST as $key => $value ) { 207 if ( is_string( $value ) ) 208 $comment["POST_{$key}"] = $value; 209 } 210 211 foreach ( $_SERVER as $key => $value ) { 212 if ( ! is_string( $value ) ) { 213 continue; 214 } 215 216 if ( preg_match( "/^HTTP_COOKIE/", $key ) ) { 217 continue; 218 } 219 220 // Send any potentially useful $_SERVER vars, but avoid sending junk we don't need. 221 if ( preg_match( "/^(HTTP_|REMOTE_ADDR|REQUEST_URI|DOCUMENT_URI)/", $key ) ) { 222 $comment[ "$key" ] = $value; 223 } 224 } 225 226 $post = get_post( $comment['comment_post_ID'] ); 227 228 if ( ! is_null( $post ) ) { 229 // $post can technically be null, although in the past, it's always been an indicator of another plugin interfering. 230 $comment[ 'comment_post_modified_gmt' ] = $post->post_modified_gmt; 231 } 232 233 $response = self::http_post( Akismet::build_query( $comment ), 'comment-check' ); 234 235 do_action( 'akismet_comment_check_response', $response ); 236 237 $commentdata['comment_as_submitted'] = array_intersect_key( $comment, self::$comment_as_submitted_allowed_keys ); 238 239 // Also include any form fields we inject into the comment form, like ak_js 240 foreach ( $_POST as $key => $value ) { 241 if ( is_string( $value ) && strpos( $key, 'ak_' ) === 0 ) { 242 $commentdata['comment_as_submitted'][ 'POST_' . $key ] = $value; 243 } 244 } 245 246 $commentdata['akismet_result'] = $response[1]; 247 248 if ( isset( $response[0]['x-akismet-pro-tip'] ) ) 249 $commentdata['akismet_pro_tip'] = $response[0]['x-akismet-pro-tip']; 250 251 if ( isset( $response[0]['x-akismet-error'] ) ) { 252 // An error occurred that we anticipated (like a suspended key) and want the user to act on. 253 // Send to moderation. 254 self::$last_comment_result = '0'; 255 } 256 else if ( 'true' == $response[1] ) { 257 // akismet_spam_count will be incremented later by comment_is_spam() 258 self::$last_comment_result = 'spam'; 259 260 $discard = ( isset( $commentdata['akismet_pro_tip'] ) && $commentdata['akismet_pro_tip'] === 'discard' && self::allow_discard() ); 261 262 do_action( 'akismet_spam_caught', $discard ); 263 264 if ( $discard ) { 265 // The spam is obvious, so we're bailing out early. 266 // akismet_result_spam() won't be called so bump the counter here 267 if ( $incr = apply_filters( 'akismet_spam_count_incr', 1 ) ) { 268 update_option( 'akismet_spam_count', get_option( 'akismet_spam_count' ) + $incr ); 269 } 270 271 if ( 'rest_api' === $context ) { 272 return new WP_Error( 'akismet_rest_comment_discarded', __( 'Comment discarded.', 'akismet' ) ); 273 } else if ( 'xml-rpc' === $context ) { 274 // If this is a pingback that we're pre-checking, the discard behavior is the same as the normal spam response behavior. 275 return $commentdata; 276 } else { 277 // Redirect back to the previous page, or failing that, the post permalink, or failing that, the homepage of the blog. 278 $redirect_to = isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : ( $post ? get_permalink( $post ) : home_url() ); 279 wp_safe_redirect( esc_url_raw( $redirect_to ) ); 280 die(); 281 } 282 } 283 else if ( 'rest_api' === $context ) { 284 // The way the REST API structures its calls, we can set the comment_approved value right away. 285 $commentdata['comment_approved'] = 'spam'; 286 } 287 } 288 289 // if the response is neither true nor false, hold the comment for moderation and schedule a recheck 290 if ( 'true' != $response[1] && 'false' != $response[1] ) { 291 if ( !current_user_can('moderate_comments') ) { 292 // Comment status should be moderated 293 self::$last_comment_result = '0'; 294 } 295 296 if ( ! wp_next_scheduled( 'akismet_schedule_cron_recheck' ) ) { 297 wp_schedule_single_event( time() + 1200, 'akismet_schedule_cron_recheck' ); 298 do_action( 'akismet_scheduled_recheck', 'invalid-response-' . $response[1] ); 299 } 300 301 self::$prevent_moderation_email_for_these_comments[] = $commentdata; 302 } 303 304 // Delete old comments daily 305 if ( ! wp_next_scheduled( 'akismet_scheduled_delete' ) ) { 306 wp_schedule_event( time(), 'daily', 'akismet_scheduled_delete' ); 307 } 308 309 self::set_last_comment( $commentdata ); 310 self::fix_scheduled_recheck(); 311 312 return $commentdata; 313 } 314 315 public static function get_last_comment() { 316 return self::$last_comment; 317 } 318 319 public static function set_last_comment( $comment ) { 320 if ( is_null( $comment ) ) { 321 self::$last_comment = null; 322 } 323 else { 324 // We filter it here so that it matches the filtered comment data that we'll have to compare against later. 325 // wp_filter_comment expects comment_author_IP 326 self::$last_comment = wp_filter_comment( 327 array_merge( 328 array( 'comment_author_IP' => self::get_ip_address() ), 329 $comment 330 ) 331 ); 332 } 333 } 334 335 // this fires on wp_insert_comment. we can't update comment_meta when auto_check_comment() runs 336 // because we don't know the comment ID at that point. 337 public static function auto_check_update_meta( $id, $comment ) { 338 // wp_insert_comment() might be called in other contexts, so make sure this is the same comment 339 // as was checked by auto_check_comment 340 if ( is_object( $comment ) && !empty( self::$last_comment ) && is_array( self::$last_comment ) ) { 341 if ( self::matches_last_comment( $comment ) ) { 342 load_plugin_textdomain( 'akismet' ); 343 344 // normal result: true or false 345 if ( self::$last_comment['akismet_result'] == 'true' ) { 346 update_comment_meta( $comment->comment_ID, 'akismet_result', 'true' ); 347 self::update_comment_history( $comment->comment_ID, '', 'check-spam' ); 348 if ( $comment->comment_approved != 'spam' ) { 349 self::update_comment_history( 350 $comment->comment_ID, 351 '', 352 'status-changed-' . $comment->comment_approved 353 ); 354 } 355 } elseif ( self::$last_comment['akismet_result'] == 'false' ) { 356 update_comment_meta( $comment->comment_ID, 'akismet_result', 'false' ); 357 self::update_comment_history( $comment->comment_ID, '', 'check-ham' ); 358 // Status could be spam or trash, depending on the WP version and whether this change applies: 359 // https://core.trac.wordpress.org/changeset/34726 360 if ( $comment->comment_approved == 'spam' || $comment->comment_approved == 'trash' ) { 361 if ( function_exists( 'wp_check_comment_disallowed_list' ) ) { 362 if ( wp_check_comment_disallowed_list( $comment->comment_author, $comment->comment_author_email, $comment->comment_author_url, $comment->comment_content, $comment->comment_author_IP, $comment->comment_agent ) ) { 363 self::update_comment_history( $comment->comment_ID, '', 'wp-disallowed' ); 364 } else { 365 self::update_comment_history( $comment->comment_ID, '', 'status-changed-' . $comment->comment_approved ); 366 } 367 } else if ( function_exists( 'wp_blacklist_check' ) && wp_blacklist_check( $comment->comment_author, $comment->comment_author_email, $comment->comment_author_url, $comment->comment_content, $comment->comment_author_IP, $comment->comment_agent ) ) { 368 self::update_comment_history( $comment->comment_ID, '', 'wp-blacklisted' ); 369 } else { 370 self::update_comment_history( $comment->comment_ID, '', 'status-changed-' . $comment->comment_approved ); 371 } 372 } 373 } else { 374 // abnormal result: error 375 update_comment_meta( $comment->comment_ID, 'akismet_error', time() ); 376 self::update_comment_history( 377 $comment->comment_ID, 378 '', 379 'check-error', 380 array( 'response' => substr( self::$last_comment['akismet_result'], 0, 50 ) ) 381 ); 382 } 383 384 // record the complete original data as submitted for checking 385 if ( isset( self::$last_comment['comment_as_submitted'] ) ) { 386 update_comment_meta( $comment->comment_ID, 'akismet_as_submitted', self::$last_comment['comment_as_submitted'] ); 387 } 388 389 if ( isset( self::$last_comment['akismet_pro_tip'] ) ) { 390 update_comment_meta( $comment->comment_ID, 'akismet_pro_tip', self::$last_comment['akismet_pro_tip'] ); 391 } 392 } 393 } 394 } 395 396 public static function delete_old_comments() { 397 global $wpdb; 398 399 /** 400 * Determines how many comments will be deleted in each batch. 401 * 402 * @param int The default, as defined by AKISMET_DELETE_LIMIT. 403 */ 404 $delete_limit = apply_filters( 'akismet_delete_comment_limit', defined( 'AKISMET_DELETE_LIMIT' ) ? AKISMET_DELETE_LIMIT : 10000 ); 405 $delete_limit = max( 1, intval( $delete_limit ) ); 406 407 /** 408 * Determines how many days a comment will be left in the Spam queue before being deleted. 409 * 410 * @param int The default number of days. 411 */ 412 $delete_interval = apply_filters( 'akismet_delete_comment_interval', 15 ); 413 $delete_interval = max( 1, intval( $delete_interval ) ); 414 415 while ( $comment_ids = $wpdb->get_col( $wpdb->prepare( "SELECT comment_id FROM {$wpdb->comments} WHERE DATE_SUB(NOW(), INTERVAL %d DAY) > comment_date_gmt AND comment_approved = 'spam' LIMIT %d", $delete_interval, $delete_limit ) ) ) { 416 if ( empty( $comment_ids ) ) 417 return; 418 419 $wpdb->queries = array(); 420 421 $comments = array(); 422 423 foreach ( $comment_ids as $comment_id ) { 424 $comments[ $comment_id ] = get_comment( $comment_id ); 425 426 do_action( 'delete_comment', $comment_id, $comments[ $comment_id ] ); 427 do_action( 'akismet_batch_delete_count', __FUNCTION__ ); 428 } 429 430 // Prepared as strings since comment_id is an unsigned BIGINT, and using %d will constrain the value to the maximum signed BIGINT. 431 $format_string = implode( ", ", array_fill( 0, count( $comment_ids ), '%s' ) ); 432 433 $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->comments} WHERE comment_id IN ( " . $format_string . " )", $comment_ids ) ); 434 $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->commentmeta} WHERE comment_id IN ( " . $format_string . " )", $comment_ids ) ); 435 436 foreach ( $comment_ids as $comment_id ) { 437 do_action( 'deleted_comment', $comment_id, $comments[ $comment_id ] ); 438 unset( $comments[ $comment_id ] ); 439 } 440 441 clean_comment_cache( $comment_ids ); 442 do_action( 'akismet_delete_comment_batch', count( $comment_ids ) ); 443 } 444 445 if ( apply_filters( 'akismet_optimize_table', ( mt_rand(1, 5000) == 11), $wpdb->comments ) ) // lucky number 446 $wpdb->query("OPTIMIZE TABLE {$wpdb->comments}"); 447 } 448 449 public static function delete_old_comments_meta() { 450 global $wpdb; 451 452 $interval = apply_filters( 'akismet_delete_commentmeta_interval', 15 ); 453 454 # enforce a minimum of 1 day 455 $interval = absint( $interval ); 456 if ( $interval < 1 ) 457 $interval = 1; 458 459 // akismet_as_submitted meta values are large, so expire them 460 // after $interval days regardless of the comment status 461 while ( $comment_ids = $wpdb->get_col( $wpdb->prepare( "SELECT m.comment_id FROM {$wpdb->commentmeta} as m INNER JOIN {$wpdb->comments} as c USING(comment_id) WHERE m.meta_key = 'akismet_as_submitted' AND DATE_SUB(NOW(), INTERVAL %d DAY) > c.comment_date_gmt LIMIT 10000", $interval ) ) ) { 462 if ( empty( $comment_ids ) ) 463 return; 464 465 $wpdb->queries = array(); 466 467 foreach ( $comment_ids as $comment_id ) { 468 delete_comment_meta( $comment_id, 'akismet_as_submitted' ); 469 do_action( 'akismet_batch_delete_count', __FUNCTION__ ); 470 } 471 472 do_action( 'akismet_delete_commentmeta_batch', count( $comment_ids ) ); 473 } 474 475 if ( apply_filters( 'akismet_optimize_table', ( mt_rand(1, 5000) == 11), $wpdb->commentmeta ) ) // lucky number 476 $wpdb->query("OPTIMIZE TABLE {$wpdb->commentmeta}"); 477 } 478 479 // Clear out comments meta that no longer have corresponding comments in the database 480 public static function delete_orphaned_commentmeta() { 481 global $wpdb; 482 483 $last_meta_id = 0; 484 $start_time = isset( $_SERVER['REQUEST_TIME_FLOAT'] ) ? $_SERVER['REQUEST_TIME_FLOAT'] : microtime( true ); 485 $max_exec_time = max( ini_get('max_execution_time') - 5, 3 ); 486 487 while ( $commentmeta_results = $wpdb->get_results( $wpdb->prepare( "SELECT m.meta_id, m.comment_id, m.meta_key FROM {$wpdb->commentmeta} as m LEFT JOIN {$wpdb->comments} as c USING(comment_id) WHERE c.comment_id IS NULL AND m.meta_id > %d ORDER BY m.meta_id LIMIT 1000", $last_meta_id ) ) ) { 488 if ( empty( $commentmeta_results ) ) 489 return; 490 491 $wpdb->queries = array(); 492 493 $commentmeta_deleted = 0; 494 495 foreach ( $commentmeta_results as $commentmeta ) { 496 if ( 'akismet_' == substr( $commentmeta->meta_key, 0, 8 ) ) { 497 delete_comment_meta( $commentmeta->comment_id, $commentmeta->meta_key ); 498 do_action( 'akismet_batch_delete_count', __FUNCTION__ ); 499 $commentmeta_deleted++; 500 } 501 502 $last_meta_id = $commentmeta->meta_id; 503 } 504 505 do_action( 'akismet_delete_commentmeta_batch', $commentmeta_deleted ); 506 507 // If we're getting close to max_execution_time, quit for this round. 508 if ( microtime(true) - $start_time > $max_exec_time ) 509 return; 510 } 511 512 if ( apply_filters( 'akismet_optimize_table', ( mt_rand(1, 5000) == 11), $wpdb->commentmeta ) ) // lucky number 513 $wpdb->query("OPTIMIZE TABLE {$wpdb->commentmeta}"); 514 } 515 516 // how many approved comments does this author have? 517 public static function get_user_comments_approved( $user_id, $comment_author_email, $comment_author, $comment_author_url ) { 518 global $wpdb; 519 520 /** 521 * Which comment types should be ignored when counting a user's approved comments? 522 * 523 * Some plugins add entries to the comments table that are not actual 524 * comments that could have been checked by Akismet. Allow these comments 525 * to be excluded from the "approved comment count" query in order to 526 * avoid artificially inflating the approved comment count. 527 * 528 * @param array $comment_types An array of comment types that won't be considered 529 * when counting a user's approved comments. 530 * 531 * @since 4.2.2 532 */ 533 $excluded_comment_types = apply_filters( 'akismet_excluded_comment_types', array() ); 534 535 $comment_type_where = ''; 536 537 if ( is_array( $excluded_comment_types ) && ! empty( $excluded_comment_types ) ) { 538 $excluded_comment_types = array_unique( $excluded_comment_types ); 539 540 foreach ( $excluded_comment_types as $excluded_comment_type ) { 541 $comment_type_where .= $wpdb->prepare( ' AND comment_type <> %s ', $excluded_comment_type ); 542 } 543 } 544 545 if ( ! empty( $user_id ) ) { 546 return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->comments} WHERE user_id = %d AND comment_approved = 1" . $comment_type_where, $user_id ) ); 547 } 548 549 if ( ! empty( $comment_author_email ) ) { 550 return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->comments} WHERE comment_author_email = %s AND comment_author = %s AND comment_author_url = %s AND comment_approved = 1" . $comment_type_where, $comment_author_email, $comment_author, $comment_author_url ) ); 551 } 552 553 return 0; 554 } 555 556 // get the full comment history for a given comment, as an array in reverse chronological order 557 public static function get_comment_history( $comment_id ) { 558 $history = get_comment_meta( $comment_id, 'akismet_history', false ); 559 if ( empty( $history ) || empty( $history[ 0 ] ) ) { 560 return false; 561 } 562 563 /* 564 // To see all variants when testing. 565 $history[] = array( 'time' => 445856401, 'message' => 'Old versions of Akismet stored the message as a literal string in the commentmeta.', 'event' => null ); 566 $history[] = array( 'time' => 445856402, 'event' => 'recheck-spam' ); 567 $history[] = array( 'time' => 445856403, 'event' => 'check-spam' ); 568 $history[] = array( 'time' => 445856404, 'event' => 'recheck-ham' ); 569 $history[] = array( 'time' => 445856405, 'event' => 'check-ham' ); 570 $history[] = array( 'time' => 445856406, 'event' => 'wp-blacklisted' ); 571 $history[] = array( 'time' => 445856406, 'event' => 'wp-disallowed' ); 572 $history[] = array( 'time' => 445856407, 'event' => 'report-spam' ); 573 $history[] = array( 'time' => 445856408, 'event' => 'report-spam', 'user' => 'sam' ); 574 $history[] = array( 'message' => 'sam reported this comment as spam (hardcoded message).', 'time' => 445856400, 'event' => 'report-spam', 'user' => 'sam' ); 575 $history[] = array( 'time' => 445856409, 'event' => 'report-ham', 'user' => 'sam' ); 576 $history[] = array( 'message' => 'sam reported this comment as ham (hardcoded message).', 'time' => 445856400, 'event' => 'report-ham', 'user' => 'sam' ); // 577 $history[] = array( 'time' => 445856410, 'event' => 'cron-retry-spam' ); 578 $history[] = array( 'time' => 445856411, 'event' => 'cron-retry-ham' ); 579 $history[] = array( 'time' => 445856412, 'event' => 'check-error' ); // 580 $history[] = array( 'time' => 445856413, 'event' => 'check-error', 'meta' => array( 'response' => 'The server was taking a nap.' ) ); 581 $history[] = array( 'time' => 445856414, 'event' => 'recheck-error' ); // Should not generate a message. 582 $history[] = array( 'time' => 445856415, 'event' => 'recheck-error', 'meta' => array( 'response' => 'The server was taking a nap.' ) ); 583 $history[] = array( 'time' => 445856416, 'event' => 'status-changedtrash' ); 584 $history[] = array( 'time' => 445856417, 'event' => 'status-changedspam' ); 585 $history[] = array( 'time' => 445856418, 'event' => 'status-changedhold' ); 586 $history[] = array( 'time' => 445856419, 'event' => 'status-changedapprove' ); 587 $history[] = array( 'time' => 445856420, 'event' => 'status-changed-trash' ); 588 $history[] = array( 'time' => 445856421, 'event' => 'status-changed-spam' ); 589 $history[] = array( 'time' => 445856422, 'event' => 'status-changed-hold' ); 590 $history[] = array( 'time' => 445856423, 'event' => 'status-changed-approve' ); 591 $history[] = array( 'time' => 445856424, 'event' => 'status-trash', 'user' => 'sam' ); 592 $history[] = array( 'time' => 445856425, 'event' => 'status-spam', 'user' => 'sam' ); 593 $history[] = array( 'time' => 445856426, 'event' => 'status-hold', 'user' => 'sam' ); 594 $history[] = array( 'time' => 445856427, 'event' => 'status-approve', 'user' => 'sam' ); 595 */ 596 597 usort( $history, array( 'Akismet', '_cmp_time' ) ); 598 return $history; 599 } 600 601 /** 602 * Log an event for a given comment, storing it in comment_meta. 603 * 604 * @param int $comment_id The ID of the relevant comment. 605 * @param string $message The string description of the event. No longer used. 606 * @param string $event The event code. 607 * @param array $meta Metadata about the history entry. e.g., the user that reported or changed the status of a given comment. 608 */ 609 public static function update_comment_history( $comment_id, $message, $event=null, $meta=null ) { 610 global $current_user; 611 612 $user = ''; 613 614 $event = array( 615 'time' => self::_get_microtime(), 616 'event' => $event, 617 ); 618 619 if ( is_object( $current_user ) && isset( $current_user->user_login ) ) { 620 $event['user'] = $current_user->user_login; 621 } 622 623 if ( ! empty( $meta ) ) { 624 $event['meta'] = $meta; 625 } 626 627 // $unique = false so as to allow multiple values per comment 628 $r = add_comment_meta( $comment_id, 'akismet_history', $event, false ); 629 } 630 631 public static function check_db_comment( $id, $recheck_reason = 'recheck_queue' ) { 632 global $wpdb; 633 634 if ( ! self::get_api_key() ) { 635 return new WP_Error( 'akismet-not-configured', __( 'Akismet is not configured. Please enter an API key.', 'akismet' ) ); 636 } 637 638 $c = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->comments} WHERE comment_ID = %d", $id ), ARRAY_A ); 639 640 if ( ! $c ) { 641 return new WP_Error( 'invalid-comment-id', __( 'Comment not found.', 'akismet' ) ); 642 } 643 644 $c['user_ip'] = $c['comment_author_IP']; 645 $c['user_agent'] = $c['comment_agent']; 646 $c['referrer'] = ''; 647 $c['blog'] = get_option( 'home' ); 648 $c['blog_lang'] = get_locale(); 649 $c['blog_charset'] = get_option('blog_charset'); 650 $c['permalink'] = get_permalink($c['comment_post_ID']); 651 $c['recheck_reason'] = $recheck_reason; 652 653 $c['user_role'] = ''; 654 if ( ! empty( $c['user_ID'] ) ) { 655 $c['user_role'] = Akismet::get_user_roles( $c['user_ID'] ); 656 } 657 658 if ( self::is_test_mode() ) 659 $c['is_test'] = 'true'; 660 661 $response = self::http_post( Akismet::build_query( $c ), 'comment-check' ); 662 663 if ( ! empty( $response[1] ) ) { 664 return $response[1]; 665 } 666 667 return false; 668 } 669 670 public static function recheck_comment( $id, $recheck_reason = 'recheck_queue' ) { 671 add_comment_meta( $id, 'akismet_rechecking', true ); 672 673 $api_response = self::check_db_comment( $id, $recheck_reason ); 674 675 delete_comment_meta( $id, 'akismet_rechecking' ); 676 677 if ( is_wp_error( $api_response ) ) { 678 // Invalid comment ID. 679 } 680 else if ( 'true' === $api_response ) { 681 wp_set_comment_status( $id, 'spam' ); 682 update_comment_meta( $id, 'akismet_result', 'true' ); 683 delete_comment_meta( $id, 'akismet_error' ); 684 delete_comment_meta( $id, 'akismet_delayed_moderation_email' ); 685 Akismet::update_comment_history( $id, '', 'recheck-spam' ); 686 } 687 elseif ( 'false' === $api_response ) { 688 update_comment_meta( $id, 'akismet_result', 'false' ); 689 delete_comment_meta( $id, 'akismet_error' ); 690 delete_comment_meta( $id, 'akismet_delayed_moderation_email' ); 691 Akismet::update_comment_history( $id, '', 'recheck-ham' ); 692 } 693 else { 694 // abnormal result: error 695 update_comment_meta( $id, 'akismet_result', 'error' ); 696 Akismet::update_comment_history( 697 $id, 698 '', 699 'recheck-error', 700 array( 'response' => substr( $api_response, 0, 50 ) ) 701 ); 702 } 703 704 return $api_response; 705 } 706 707 public static function transition_comment_status( $new_status, $old_status, $comment ) { 708 709 if ( $new_status == $old_status ) 710 return; 711 712 if ( 'spam' === $new_status || 'spam' === $old_status ) { 713 // Clear the cache of the "X comments in your spam queue" count on the dashboard. 714 wp_cache_delete( 'akismet_spam_count', 'widget' ); 715 } 716 717 # we don't need to record a history item for deleted comments 718 if ( $new_status == 'delete' ) 719 return; 720 721 if ( !current_user_can( 'edit_post', $comment->comment_post_ID ) && !current_user_can( 'moderate_comments' ) ) 722 return; 723 724 if ( defined('WP_IMPORTING') && WP_IMPORTING == true ) 725 return; 726 727 // if this is present, it means the status has been changed by a re-check, not an explicit user action 728 if ( get_comment_meta( $comment->comment_ID, 'akismet_rechecking' ) ) 729 return; 730 731 // Assumption alert: 732 // We want to submit comments to Akismet only when a moderator explicitly spams or approves it - not if the status 733 // is changed automatically by another plugin. Unfortunately WordPress doesn't provide an unambiguous way to 734 // determine why the transition_comment_status action was triggered. And there are several different ways by which 735 // to spam and unspam comments: bulk actions, ajax, links in moderation emails, the dashboard, and perhaps others. 736 // We'll assume that this is an explicit user action if certain POST/GET variables exist. 737 if ( 738 // status=spam: Marking as spam via the REST API or... 739 // status=unspam: I'm not sure. Maybe this used to be used instead of status=approved? Or the UI for removing from spam but not approving has been since removed?... 740 // status=approved: Unspamming via the REST API (Calypso) or... 741 ( isset( $_POST['status'] ) && in_array( $_POST['status'], array( 'spam', 'unspam', 'approved', ) ) ) 742 // spam=1: Clicking "Spam" underneath a comment in wp-admin and allowing the AJAX request to happen. 743 || ( isset( $_POST['spam'] ) && (int) $_POST['spam'] == 1 ) 744 // unspam=1: Clicking "Not Spam" underneath a comment in wp-admin and allowing the AJAX request to happen. Or, clicking "Undo" after marking something as spam. 745 || ( isset( $_POST['unspam'] ) && (int) $_POST['unspam'] == 1 ) 746 // comment_status=spam/unspam: It's unclear where this is happening. 747 || ( isset( $_POST['comment_status'] ) && in_array( $_POST['comment_status'], array( 'spam', 'unspam' ) ) ) 748 // action=spam: Choosing "Mark as Spam" from the Bulk Actions dropdown in wp-admin (or the "Spam it" link in notification emails). 749 // action=unspam: Choosing "Not Spam" from the Bulk Actions dropdown in wp-admin. 750 // action=spamcomment: Following the "Spam" link below a comment in wp-admin (not allowing AJAX request to happen). 751 // action=unspamcomment: Following the "Not Spam" link below a comment in wp-admin (not allowing AJAX request to happen). 752 || ( isset( $_GET['action'] ) && in_array( $_GET['action'], array( 'spam', 'unspam', 'spamcomment', 'unspamcomment', ) ) ) 753 // action=editedcomment: Editing a comment via wp-admin (and possibly changing its status). 754 || ( isset( $_POST['action'] ) && in_array( $_POST['action'], array( 'editedcomment' ) ) ) 755 // for=jetpack: Moderation via the WordPress app, Calypso, anything powered by the Jetpack connection. 756 || ( isset( $_GET['for'] ) && ( 'jetpack' == $_GET['for'] ) && ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) ) 757 // Certain WordPress.com API requests 758 || ( defined( 'REST_API_REQUEST' ) && REST_API_REQUEST ) 759 // WordPress.org REST API requests 760 || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) 761 ) { 762 if ( $new_status == 'spam' && ( $old_status == 'approved' || $old_status == 'unapproved' || !$old_status ) ) { 763 return self::submit_spam_comment( $comment->comment_ID ); 764 } elseif ( $old_status == 'spam' && ( $new_status == 'approved' || $new_status == 'unapproved' ) ) { 765 return self::submit_nonspam_comment( $comment->comment_ID ); 766 } 767 } 768 769 self::update_comment_history( $comment->comment_ID, '', 'status-' . $new_status ); 770 } 771 772 public static function submit_spam_comment( $comment_id ) { 773 global $wpdb, $current_user, $current_site; 774 775 $comment_id = (int) $comment_id; 776 777 $comment = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->comments} WHERE comment_ID = %d", $comment_id ) ); 778 779 if ( !$comment ) // it was deleted 780 return; 781 782 if ( 'spam' != $comment->comment_approved ) 783 return; 784 785 self::update_comment_history( $comment_id, '', 'report-spam' ); 786 787 // If the user hasn't configured Akismet, there's nothing else to do at this point. 788 if ( ! self::get_api_key() ) { 789 return; 790 } 791 792 // use the original version stored in comment_meta if available 793 $as_submitted = self::sanitize_comment_as_submitted( get_comment_meta( $comment_id, 'akismet_as_submitted', true ) ); 794 795 if ( $as_submitted && is_array( $as_submitted ) && isset( $as_submitted['comment_content'] ) ) 796 $comment = (object) array_merge( (array)$comment, $as_submitted ); 797 798 $comment->blog = get_option( 'home' ); 799 $comment->blog_lang = get_locale(); 800 $comment->blog_charset = get_option('blog_charset'); 801 $comment->permalink = get_permalink($comment->comment_post_ID); 802 803 if ( is_object($current_user) ) 804 $comment->reporter = $current_user->user_login; 805 806 if ( is_object($current_site) ) 807 $comment->site_domain = $current_site->domain; 808 809 $comment->user_role = ''; 810 if ( ! empty( $comment->user_ID ) ) { 811 $comment->user_role = Akismet::get_user_roles( $comment->user_ID ); 812 } 813 814 if ( self::is_test_mode() ) 815 $comment->is_test = 'true'; 816 817 $post = get_post( $comment->comment_post_ID ); 818 819 if ( ! is_null( $post ) ) { 820 $comment->comment_post_modified_gmt = $post->post_modified_gmt; 821 } 822 823 $response = Akismet::http_post( Akismet::build_query( $comment ), 'submit-spam' ); 824 825 update_comment_meta( $comment_id, 'akismet_user_result', 'true' ); 826 827 if ( $comment->reporter ) { 828 update_comment_meta( $comment_id, 'akismet_user', $comment->reporter ); 829 } 830 831 do_action('akismet_submit_spam_comment', $comment_id, $response[1]); 832 } 833 834 public static function submit_nonspam_comment( $comment_id ) { 835 global $wpdb, $current_user, $current_site; 836 837 $comment_id = (int) $comment_id; 838 839 $comment = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->comments} WHERE comment_ID = %d", $comment_id ) ); 840 if ( !$comment ) // it was deleted 841 return; 842 843 self::update_comment_history( $comment_id, '', 'report-ham' ); 844 845 // If the user hasn't configured Akismet, there's nothing else to do at this point. 846 if ( ! self::get_api_key() ) { 847 return; 848 } 849 850 // use the original version stored in comment_meta if available 851 $as_submitted = self::sanitize_comment_as_submitted( get_comment_meta( $comment_id, 'akismet_as_submitted', true ) ); 852 853 if ( $as_submitted && is_array($as_submitted) && isset($as_submitted['comment_content']) ) 854 $comment = (object) array_merge( (array)$comment, $as_submitted ); 855 856 $comment->blog = get_option( 'home' ); 857 $comment->blog_lang = get_locale(); 858 $comment->blog_charset = get_option('blog_charset'); 859 $comment->permalink = get_permalink( $comment->comment_post_ID ); 860 $comment->user_role = ''; 861 862 if ( is_object($current_user) ) 863 $comment->reporter = $current_user->user_login; 864 865 if ( is_object($current_site) ) 866 $comment->site_domain = $current_site->domain; 867 868 if ( ! empty( $comment->user_ID ) ) { 869 $comment->user_role = Akismet::get_user_roles( $comment->user_ID ); 870 } 871 872 if ( Akismet::is_test_mode() ) 873 $comment->is_test = 'true'; 874 875 $post = get_post( $comment->comment_post_ID ); 876 877 if ( ! is_null( $post ) ) { 878 $comment->comment_post_modified_gmt = $post->post_modified_gmt; 879 } 880 881 $response = self::http_post( Akismet::build_query( $comment ), 'submit-ham' ); 882 883 update_comment_meta( $comment_id, 'akismet_user_result', 'false' ); 884 885 if ( $comment->reporter ) { 886 update_comment_meta( $comment_id, 'akismet_user', $comment->reporter ); 887 } 888 889 do_action('akismet_submit_nonspam_comment', $comment_id, $response[1]); 890 } 891 892 public static function cron_recheck() { 893 global $wpdb; 894 895 $api_key = self::get_api_key(); 896 897 $status = self::verify_key( $api_key ); 898 if ( get_option( 'akismet_alert_code' ) || $status == 'invalid' ) { 899 // since there is currently a problem with the key, reschedule a check for 6 hours hence 900 wp_schedule_single_event( time() + 21600, 'akismet_schedule_cron_recheck' ); 901 do_action( 'akismet_scheduled_recheck', 'key-problem-' . get_option( 'akismet_alert_code' ) . '-' . $status ); 902 return false; 903 } 904 905 delete_option('akismet_available_servers'); 906 907 $comment_errors = $wpdb->get_col( "SELECT comment_id FROM {$wpdb->commentmeta} WHERE meta_key = 'akismet_error' LIMIT 100" ); 908 909 load_plugin_textdomain( 'akismet' ); 910 911 foreach ( (array) $comment_errors as $comment_id ) { 912 // if the comment no longer exists, or is too old, remove the meta entry from the queue to avoid getting stuck 913 $comment = get_comment( $comment_id ); 914 915 if ( 916 ! $comment // Comment has been deleted 917 || strtotime( $comment->comment_date_gmt ) < strtotime( "-15 days" ) // Comment is too old. 918 || $comment->comment_approved !== "0" // Comment is no longer in the Pending queue 919 ) { 920 delete_comment_meta( $comment_id, 'akismet_error' ); 921 delete_comment_meta( $comment_id, 'akismet_delayed_moderation_email' ); 922 continue; 923 } 924 925 add_comment_meta( $comment_id, 'akismet_rechecking', true ); 926 $status = self::check_db_comment( $comment_id, 'retry' ); 927 928 $event = ''; 929 if ( $status == 'true' ) { 930 $event = 'cron-retry-spam'; 931 } elseif ( $status == 'false' ) { 932 $event = 'cron-retry-ham'; 933 } 934 935 // If we got back a legit response then update the comment history 936 // other wise just bail now and try again later. No point in 937 // re-trying all the comments once we hit one failure. 938 if ( !empty( $event ) ) { 939 delete_comment_meta( $comment_id, 'akismet_error' ); 940 self::update_comment_history( $comment_id, '', $event ); 941 update_comment_meta( $comment_id, 'akismet_result', $status ); 942 // make sure the comment status is still pending. if it isn't, that means the user has already moved it elsewhere. 943 $comment = get_comment( $comment_id ); 944 if ( $comment && 'unapproved' == wp_get_comment_status( $comment_id ) ) { 945 if ( $status == 'true' ) { 946 wp_spam_comment( $comment_id ); 947 } elseif ( $status == 'false' ) { 948 // comment is good, but it's still in the pending queue. depending on the moderation settings 949 // we may need to change it to approved. 950 if ( check_comment($comment->comment_author, $comment->comment_author_email, $comment->comment_author_url, $comment->comment_content, $comment->comment_author_IP, $comment->comment_agent, $comment->comment_type) ) 951 wp_set_comment_status( $comment_id, 1 ); 952 else if ( get_comment_meta( $comment_id, 'akismet_delayed_moderation_email', true ) ) 953 wp_notify_moderator( $comment_id ); 954 } 955 } 956 957 delete_comment_meta( $comment_id, 'akismet_delayed_moderation_email' ); 958 } else { 959 // If this comment has been pending moderation for longer than MAX_DELAY_BEFORE_MODERATION_EMAIL, 960 // send a moderation email now. 961 if ( ( intval( gmdate( 'U' ) ) - strtotime( $comment->comment_date_gmt ) ) < self::MAX_DELAY_BEFORE_MODERATION_EMAIL ) { 962 delete_comment_meta( $comment_id, 'akismet_delayed_moderation_email' ); 963 wp_notify_moderator( $comment_id ); 964 } 965 966 delete_comment_meta( $comment_id, 'akismet_rechecking' ); 967 wp_schedule_single_event( time() + 1200, 'akismet_schedule_cron_recheck' ); 968 do_action( 'akismet_scheduled_recheck', 'check-db-comment-' . $status ); 969 return; 970 } 971 delete_comment_meta( $comment_id, 'akismet_rechecking' ); 972 } 973 974 $remaining = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->commentmeta} WHERE meta_key = 'akismet_error'" ); 975 if ( $remaining && !wp_next_scheduled('akismet_schedule_cron_recheck') ) { 976 wp_schedule_single_event( time() + 1200, 'akismet_schedule_cron_recheck' ); 977 do_action( 'akismet_scheduled_recheck', 'remaining' ); 978 } 979 } 980 981 public static function fix_scheduled_recheck() { 982 $future_check = wp_next_scheduled( 'akismet_schedule_cron_recheck' ); 983 if ( !$future_check ) { 984 return; 985 } 986 987 if ( get_option( 'akismet_alert_code' ) > 0 ) { 988 return; 989 } 990 991 $check_range = time() + 1200; 992 if ( $future_check > $check_range ) { 993 wp_clear_scheduled_hook( 'akismet_schedule_cron_recheck' ); 994 wp_schedule_single_event( time() + 300, 'akismet_schedule_cron_recheck' ); 995 do_action( 'akismet_scheduled_recheck', 'fix-scheduled-recheck' ); 996 } 997 } 998 999 public static function add_comment_nonce( $post_id ) { 1000 /** 1001 * To disable the Akismet comment nonce, add a filter for the 'akismet_comment_nonce' tag 1002 * and return any string value that is not 'true' or '' (empty string). 1003 * 1004 * Don't return boolean false, because that implies that the 'akismet_comment_nonce' option 1005 * has not been set and that Akismet should just choose the default behavior for that 1006 * situation. 1007 */ 1008 1009 if ( ! self::get_api_key() ) { 1010 return; 1011 } 1012 1013 $akismet_comment_nonce_option = apply_filters( 'akismet_comment_nonce', get_option( 'akismet_comment_nonce' ) ); 1014 1015 if ( $akismet_comment_nonce_option == 'true' || $akismet_comment_nonce_option == '' ) { 1016 echo '<p style="display: none;">'; 1017 wp_nonce_field( 'akismet_comment_nonce_' . $post_id, 'akismet_comment_nonce', FALSE ); 1018 echo '</p>'; 1019 } 1020 } 1021 1022 public static function is_test_mode() { 1023 return defined('AKISMET_TEST_MODE') && AKISMET_TEST_MODE; 1024 } 1025 1026 public static function allow_discard() { 1027 if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) 1028 return false; 1029 if ( is_user_logged_in() ) 1030 return false; 1031 1032 return ( get_option( 'akismet_strictness' ) === '1' ); 1033 } 1034 1035 public static function get_ip_address() { 1036 return isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : null; 1037 } 1038 1039 /** 1040 * Do these two comments, without checking the comment_ID, "match"? 1041 * 1042 * @param mixed $comment1 A comment object or array. 1043 * @param mixed $comment2 A comment object or array. 1044 * @return bool Whether the two comments should be treated as the same comment. 1045 */ 1046 private static function comments_match( $comment1, $comment2 ) { 1047 $comment1 = (array) $comment1; 1048 $comment2 = (array) $comment2; 1049 1050 // Set default values for these strings that we check in order to simplify 1051 // the checks and avoid PHP warnings. 1052 if ( ! isset( $comment1['comment_author'] ) ) { 1053 $comment1['comment_author'] = ''; 1054 } 1055 1056 if ( ! isset( $comment2['comment_author'] ) ) { 1057 $comment2['comment_author'] = ''; 1058 } 1059 1060 if ( ! isset( $comment1['comment_author_email'] ) ) { 1061 $comment1['comment_author_email'] = ''; 1062 } 1063 1064 if ( ! isset( $comment2['comment_author_email'] ) ) { 1065 $comment2['comment_author_email'] = ''; 1066 } 1067 1068 $comments_match = ( 1069 isset( $comment1['comment_post_ID'], $comment2['comment_post_ID'] ) 1070 && intval( $comment1['comment_post_ID'] ) == intval( $comment2['comment_post_ID'] ) 1071 && ( 1072 // The comment author length max is 255 characters, limited by the TINYTEXT column type. 1073 // If the comment author includes multibyte characters right around the 255-byte mark, they 1074 // may be stripped when the author is saved in the DB, so a 300+ char author may turn into 1075 // a 253-char author when it's saved, not 255 exactly. The longest possible character is 1076 // theoretically 6 bytes, so we'll only look at the first 248 bytes to be safe. 1077 substr( $comment1['comment_author'], 0, 248 ) == substr( $comment2['comment_author'], 0, 248 ) 1078 || substr( stripslashes( $comment1['comment_author'] ), 0, 248 ) == substr( $comment2['comment_author'], 0, 248 ) 1079 || substr( $comment1['comment_author'], 0, 248 ) == substr( stripslashes( $comment2['comment_author'] ), 0, 248 ) 1080 // Certain long comment author names will be truncated to nothing, depending on their encoding. 1081 || ( ! $comment1['comment_author'] && strlen( $comment2['comment_author'] ) > 248 ) 1082 || ( ! $comment2['comment_author'] && strlen( $comment1['comment_author'] ) > 248 ) 1083 ) 1084 && ( 1085 // The email max length is 100 characters, limited by the VARCHAR(100) column type. 1086 // Same argument as above for only looking at the first 93 characters. 1087 substr( $comment1['comment_author_email'], 0, 93 ) == substr( $comment2['comment_author_email'], 0, 93 ) 1088 || substr( stripslashes( $comment1['comment_author_email'] ), 0, 93 ) == substr( $comment2['comment_author_email'], 0, 93 ) 1089 || substr( $comment1['comment_author_email'], 0, 93 ) == substr( stripslashes( $comment2['comment_author_email'] ), 0, 93 ) 1090 // Very long emails can be truncated and then stripped if the [0:100] substring isn't a valid address. 1091 || ( ! $comment1['comment_author_email'] && strlen( $comment2['comment_author_email'] ) > 100 ) 1092 || ( ! $comment2['comment_author_email'] && strlen( $comment1['comment_author_email'] ) > 100 ) 1093 ) 1094 ); 1095 1096 return $comments_match; 1097 } 1098 1099 // Does the supplied comment match the details of the one most recently stored in self::$last_comment? 1100 public static function matches_last_comment( $comment ) { 1101 return self::comments_match( self::$last_comment, $comment ); 1102 } 1103 1104 private static function get_user_agent() { 1105 return isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : null; 1106 } 1107 1108 private static function get_referer() { 1109 return isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : null; 1110 } 1111 1112 // return a comma-separated list of role names for the given user 1113 public static function get_user_roles( $user_id ) { 1114 $roles = false; 1115 1116 if ( !class_exists('WP_User') ) 1117 return false; 1118 1119 if ( $user_id > 0 ) { 1120 $comment_user = new WP_User( $user_id ); 1121 if ( isset( $comment_user->roles ) ) 1122 $roles = join( ',', $comment_user->roles ); 1123 } 1124 1125 if ( is_multisite() && is_super_admin( $user_id ) ) { 1126 if ( empty( $roles ) ) { 1127 $roles = 'super_admin'; 1128 } else { 1129 $comment_user->roles[] = 'super_admin'; 1130 $roles = join( ',', $comment_user->roles ); 1131 } 1132 } 1133 1134 return $roles; 1135 } 1136 1137 // filter handler used to return a spam result to pre_comment_approved 1138 public static function last_comment_status( $approved, $comment ) { 1139 if ( is_null( self::$last_comment_result ) ) { 1140 // We didn't have reason to store the result of the last check. 1141 return $approved; 1142 } 1143 1144 // Only do this if it's the correct comment 1145 if ( ! self::matches_last_comment( $comment ) ) { 1146 self::log( "comment_is_spam mismatched comment, returning unaltered $approved" ); 1147 return $approved; 1148 } 1149 1150 if ( 'trash' === $approved ) { 1151 // If the last comment we checked has had its approval set to 'trash', 1152 // then it failed the comment blacklist check. Let that blacklist override 1153 // the spam check, since users have the (valid) expectation that when 1154 // they fill out their blacklists, comments that match it will always 1155 // end up in the trash. 1156 return $approved; 1157 } 1158 1159 // bump the counter here instead of when the filter is added to reduce the possibility of overcounting 1160 if ( $incr = apply_filters('akismet_spam_count_incr', 1) ) 1161 update_option( 'akismet_spam_count', get_option('akismet_spam_count') + $incr ); 1162 1163 return self::$last_comment_result; 1164 } 1165 1166 /** 1167 * If Akismet is temporarily unreachable, we don't want to "spam" the blogger with 1168 * moderation emails for comments that will be automatically cleared or spammed on 1169 * the next retry. 1170 * 1171 * For comments that will be rechecked later, empty the list of email addresses that 1172 * the moderation email would be sent to. 1173 * 1174 * @param array $emails An array of email addresses that the moderation email will be sent to. 1175 * @param int $comment_id The ID of the relevant comment. 1176 * @return array An array of email addresses that the moderation email will be sent to. 1177 */ 1178 public static function disable_moderation_emails_if_unreachable( $emails, $comment_id ) { 1179 if ( ! empty( self::$prevent_moderation_email_for_these_comments ) && ! empty( $emails ) ) { 1180 $comment = get_comment( $comment_id ); 1181 1182 if ( $comment ) { 1183 foreach ( self::$prevent_moderation_email_for_these_comments as $possible_match ) { 1184 if ( self::comments_match( $possible_match, $comment ) ) { 1185 update_comment_meta( $comment_id, 'akismet_delayed_moderation_email', true ); 1186 return array(); 1187 } 1188 } 1189 } 1190 } 1191 1192 return $emails; 1193 } 1194 1195 public static function _cmp_time( $a, $b ) { 1196 return $a['time'] > $b['time'] ? -1 : 1; 1197 } 1198 1199 public static function _get_microtime() { 1200 $mtime = explode( ' ', microtime() ); 1201 return $mtime[1] + $mtime[0]; 1202 } 1203 1204 /** 1205 * Make a POST request to the Akismet API. 1206 * 1207 * @param string $request The body of the request. 1208 * @param string $path The path for the request. 1209 * @param string $ip The specific IP address to hit. 1210 * @return array A two-member array consisting of the headers and the response body, both empty in the case of a failure. 1211 */ 1212 public static function http_post( $request, $path, $ip=null ) { 1213 1214 $akismet_ua = sprintf( 'WordPress/%s | Akismet/%s', $GLOBALS['wp_version'], constant( 'AKISMET_VERSION' ) ); 1215 $akismet_ua = apply_filters( 'akismet_ua', $akismet_ua ); 1216 1217 $content_length = strlen( $request ); 1218 1219 $api_key = self::get_api_key(); 1220 $host = self::API_HOST; 1221 1222 if ( !empty( $api_key ) ) 1223 $host = $api_key.'.'.$host; 1224 1225 $http_host = $host; 1226 // use a specific IP if provided 1227 // needed by Akismet_Admin::check_server_connectivity() 1228 if ( $ip && long2ip( ip2long( $ip ) ) ) { 1229 $http_host = $ip; 1230 } 1231 1232 $http_args = array( 1233 'body' => $request, 1234 'headers' => array( 1235 'Content-Type' => 'application/x-www-form-urlencoded; charset=' . get_option( 'blog_charset' ), 1236 'Host' => $host, 1237 'User-Agent' => $akismet_ua, 1238 ), 1239 'httpversion' => '1.0', 1240 'timeout' => 15 1241 ); 1242 1243 $akismet_url = $http_akismet_url = "http://{$http_host}/1.1/{$path}"; 1244 1245 /** 1246 * Try SSL first; if that fails, try without it and don't try it again for a while. 1247 */ 1248 1249 $ssl = $ssl_failed = false; 1250 1251 // Check if SSL requests were disabled fewer than X hours ago. 1252 $ssl_disabled = get_option( 'akismet_ssl_disabled' ); 1253 1254 if ( $ssl_disabled && $ssl_disabled < ( time() - 60 * 60 * 24 ) ) { // 24 hours 1255 $ssl_disabled = false; 1256 delete_option( 'akismet_ssl_disabled' ); 1257 } 1258 else if ( $ssl_disabled ) { 1259 do_action( 'akismet_ssl_disabled' ); 1260 } 1261 1262 if ( ! $ssl_disabled && ( $ssl = wp_http_supports( array( 'ssl' ) ) ) ) { 1263 $akismet_url = set_url_scheme( $akismet_url, 'https' ); 1264 1265 do_action( 'akismet_https_request_pre' ); 1266 } 1267 1268 $response = wp_remote_post( $akismet_url, $http_args ); 1269 1270 Akismet::log( compact( 'akismet_url', 'http_args', 'response' ) ); 1271 1272 if ( $ssl && is_wp_error( $response ) ) { 1273 do_action( 'akismet_https_request_failure', $response ); 1274 1275 // Intermittent connection problems may cause the first HTTPS 1276 // request to fail and subsequent HTTP requests to succeed randomly. 1277 // Retry the HTTPS request once before disabling SSL for a time. 1278 $response = wp_remote_post( $akismet_url, $http_args ); 1279 1280 Akismet::log( compact( 'akismet_url', 'http_args', 'response' ) ); 1281 1282 if ( is_wp_error( $response ) ) { 1283 $ssl_failed = true; 1284 1285 do_action( 'akismet_https_request_failure', $response ); 1286 1287 do_action( 'akismet_http_request_pre' ); 1288 1289 // Try the request again without SSL. 1290 $response = wp_remote_post( $http_akismet_url, $http_args ); 1291 1292 Akismet::log( compact( 'http_akismet_url', 'http_args', 'response' ) ); 1293 } 1294 } 1295 1296 if ( is_wp_error( $response ) ) { 1297 do_action( 'akismet_request_failure', $response ); 1298 1299 return array( '', '' ); 1300 } 1301 1302 if ( $ssl_failed ) { 1303 // The request failed when using SSL but succeeded without it. Disable SSL for future requests. 1304 update_option( 'akismet_ssl_disabled', time() ); 1305 1306 do_action( 'akismet_https_disabled' ); 1307 } 1308 1309 $simplified_response = array( $response['headers'], $response['body'] ); 1310 1311 self::update_alert( $simplified_response ); 1312 1313 return $simplified_response; 1314 } 1315 1316 // given a response from an API call like check_key_status(), update the alert code options if an alert is present. 1317 public static function update_alert( $response ) { 1318 $alert_option_prefix = 'akismet_alert_'; 1319 $alert_header_prefix = 'x-akismet-alert-'; 1320 $alert_header_names = array( 1321 'code', 1322 'msg', 1323 'api-calls', 1324 'usage-limit', 1325 'upgrade-plan', 1326 'upgrade-url', 1327 'upgrade-type', 1328 ); 1329 1330 foreach ( $alert_header_names as $alert_header_name ) { 1331 $value = null; 1332 if ( isset( $response[0][ $alert_header_prefix . $alert_header_name ] ) ) { 1333 $value = $response[0][ $alert_header_prefix . $alert_header_name ]; 1334 } 1335 1336 $option_name = $alert_option_prefix . str_replace( '-', '_', $alert_header_name ); 1337 if ( $value != get_option( $option_name ) ) { 1338 if ( ! $value ) { 1339 delete_option( $option_name ); 1340 } else { 1341 update_option( $option_name, $value ); 1342 } 1343 } 1344 } 1345 } 1346 1347 public static function load_form_js() { 1348 /* deprecated */ 1349 } 1350 1351 public static function set_form_js_async( $tag, $handle, $src ) { 1352 /* deprecated */ 1353 return $tag; 1354 } 1355 1356 public static function get_akismet_form_fields() { 1357 $fields = ''; 1358 1359 $prefix = 'ak_'; 1360 1361 // Contact Form 7 uses _wpcf7 as a prefix to know which fields to exclude from comment_content. 1362 if ( 'wpcf7_form_elements' === current_filter() ) { 1363 $prefix = '_wpcf7_ak_'; 1364 } 1365 1366 $fields .= '<p style="display: none !important;">'; 1367 $fields .= '<label>Δ<textarea name="' . $prefix . 'hp_textarea" cols="45" rows="8" maxlength="100"></textarea></label>'; 1368 1369 if ( ! function_exists( 'amp_is_request' ) || ! amp_is_request() ) { 1370 // Keep track of how many ak_js fields are in this page so that we don't re-use 1371 // the same ID. 1372 static $field_count = 0; 1373 1374 $field_count++; 1375 1376 $fields .= '<input type="hidden" id="ak_js_' . $field_count . '" name="' . $prefix . 'js" value="' . mt_rand( 0, 250 ) . '"/>'; 1377 $fields .= '<script>document.getElementById( "ak_js_' . $field_count . '" ).setAttribute( "value", ( new Date() ).getTime() );</script>'; 1378 } 1379 1380 $fields .= '</p>'; 1381 1382 return $fields; 1383 } 1384 1385 public static function output_custom_form_fields( $post_id ) { 1386 // phpcs:ignore WordPress.Security.EscapeOutput 1387 echo self::get_akismet_form_fields(); 1388 } 1389 1390 public static function inject_custom_form_fields( $html ) { 1391 $html = str_replace( '</form>', self::get_akismet_form_fields() . '</form>', $html ); 1392 1393 return $html; 1394 } 1395 1396 public static function append_custom_form_fields( $html ) { 1397 $html .= self::get_akismet_form_fields(); 1398 1399 return $html; 1400 } 1401 1402 /** 1403 * Ensure that any Akismet-added form fields are included in the comment-check call. 1404 * 1405 * @param array $form 1406 * @param array $data Some plugins will supply the POST data via the filter, since they don't 1407 * read it directly from $_POST. 1408 * @return array $form 1409 */ 1410 public static function prepare_custom_form_values( $form, $data = null ) { 1411 if ( is_null( $data ) ) { 1412 // phpcs:ignore WordPress.Security.NonceVerification.Missing 1413 $data = $_POST; 1414 } 1415 1416 $prefix = 'ak_'; 1417 1418 // Contact Form 7 uses _wpcf7 as a prefix to know which fields to exclude from comment_content. 1419 if ( 'wpcf7_akismet_parameters' === current_filter() ) { 1420 $prefix = '_wpcf7_ak_'; 1421 } 1422 1423 foreach ( $data as $key => $val ) { 1424 if ( 0 === strpos( $key, $prefix ) ) { 1425 $form[ 'POST_ak_' . substr( $key, strlen( $prefix ) ) ] = $val; 1426 } 1427 } 1428 1429 return $form; 1430 } 1431 1432 private static function bail_on_activation( $message, $deactivate = true ) { 1433 ?> 1434 <!doctype html> 1435 <html> 1436 <head> 1437 <meta charset="<?php bloginfo( 'charset' ); ?>" /> 1438 <style> 1439 * { 1440 text-align: center; 1441 margin: 0; 1442 padding: 0; 1443 font-family: "Lucida Grande",Verdana,Arial,"Bitstream Vera Sans",sans-serif; 1444 } 1445 p { 1446 margin-top: 1em; 1447 font-size: 18px; 1448 } 1449 </style> 1450 </head> 1451 <body> 1452 <p><?php echo esc_html( $message ); ?></p> 1453 </body> 1454 </html> 1455 <?php 1456 if ( $deactivate ) { 1457 $plugins = get_option( 'active_plugins' ); 1458 $akismet = plugin_basename( AKISMET__PLUGIN_DIR . 'akismet.php' ); 1459 $update = false; 1460 foreach ( $plugins as $i => $plugin ) { 1461 if ( $plugin === $akismet ) { 1462 $plugins[$i] = false; 1463 $update = true; 1464 } 1465 } 1466 1467 if ( $update ) { 1468 update_option( 'active_plugins', array_filter( $plugins ) ); 1469 } 1470 } 1471 exit; 1472 } 1473 1474 public static function view( $name, array $args = array() ) { 1475 $args = apply_filters( 'akismet_view_arguments', $args, $name ); 1476 1477 foreach ( $args AS $key => $val ) { 1478 $$key = $val; 1479 } 1480 1481 load_plugin_textdomain( 'akismet' ); 1482 1483 $file = AKISMET__PLUGIN_DIR . 'views/'. $name . '.php'; 1484 1485 include( $file ); 1486 } 1487 1488 /** 1489 * Attached to activate_{ plugin_basename( __FILES__ ) } by register_activation_hook() 1490 * @static 1491 */ 1492 public static function plugin_activation() { 1493 if ( version_compare( $GLOBALS['wp_version'], AKISMET__MINIMUM_WP_VERSION, '<' ) ) { 1494 load_plugin_textdomain( 'akismet' ); 1495 1496 $message = '<strong>'.sprintf(esc_html__( 'Akismet %s requires WordPress %s or higher.' , 'akismet'), AKISMET_VERSION, AKISMET__MINIMUM_WP_VERSION ).'</strong> '.sprintf(__('Please <a href="%1$s">upgrade WordPress</a> to a current version, or <a href="%2$s">downgrade to version 2.4 of the Akismet plugin</a>.', 'akismet'), 'https://codex.wordpress.org/Upgrading_WordPress', 'https://wordpress.org/extend/plugins/akismet/download/'); 1497 1498 Akismet::bail_on_activation( $message ); 1499 } elseif ( ! empty( $_SERVER['SCRIPT_NAME'] ) && false !== strpos( $_SERVER['SCRIPT_NAME'], '/wp-admin/plugins.php' ) ) { 1500 add_option( 'Activated_Akismet', true ); 1501 } 1502 } 1503 1504 /** 1505 * Removes all connection options 1506 * @static 1507 */ 1508 public static function plugin_deactivation( ) { 1509 self::deactivate_key( self::get_api_key() ); 1510 1511 // Remove any scheduled cron jobs. 1512 $akismet_cron_events = array( 1513 'akismet_schedule_cron_recheck', 1514 'akismet_scheduled_delete', 1515 ); 1516 1517 foreach ( $akismet_cron_events as $akismet_cron_event ) { 1518 $timestamp = wp_next_scheduled( $akismet_cron_event ); 1519 1520 if ( $timestamp ) { 1521 wp_unschedule_event( $timestamp, $akismet_cron_event ); 1522 } 1523 } 1524 } 1525 1526 /** 1527 * Essentially a copy of WP's build_query but one that doesn't expect pre-urlencoded values. 1528 * 1529 * @param array $args An array of key => value pairs 1530 * @return string A string ready for use as a URL query string. 1531 */ 1532 public static function build_query( $args ) { 1533 return _http_build_query( $args, '', '&' ); 1534 } 1535 1536 /** 1537 * Log debugging info to the error log. 1538 * 1539 * Enabled when WP_DEBUG_LOG is enabled (and WP_DEBUG, since according to 1540 * core, "WP_DEBUG_DISPLAY and WP_DEBUG_LOG perform no function unless 1541 * WP_DEBUG is true), but can be disabled via the akismet_debug_log filter. 1542 * 1543 * @param mixed $akismet_debug The data to log. 1544 */ 1545 public static function log( $akismet_debug ) { 1546 if ( apply_filters( 'akismet_debug_log', defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG && defined( 'AKISMET_DEBUG' ) && AKISMET_DEBUG ) ) { 1547 error_log( print_r( compact( 'akismet_debug' ), true ) ); 1548 } 1549 } 1550 1551 public static function pre_check_pingback( $method ) { 1552 if ( $method !== 'pingback.ping' ) 1553 return; 1554 1555 // A lot of this code is tightly coupled with the IXR class because the xmlrpc_call action doesn't pass along any information besides the method name. 1556 // This ticket should hopefully fix that: https://core.trac.wordpress.org/ticket/52524 1557 // Until that happens, when it's a system.multicall, pre_check_pingback will be called once for every internal pingback call. 1558 // Keep track of how many times this function has been called so we know which call to reference in the XML. 1559 static $call_count = 0; 1560 1561 $call_count++; 1562 1563 global $wp_xmlrpc_server; 1564 1565 if ( !is_object( $wp_xmlrpc_server ) ) 1566 return false; 1567 1568 $is_multicall = false; 1569 $multicall_count = 0; 1570 1571 if ( 'system.multicall' === $wp_xmlrpc_server->message->methodName ) { 1572 $is_multicall = true; 1573 1574 if ( 0 === $call_count ) { 1575 // Only pass along the number of entries in the multicall the first time we see it. 1576 $multicall_count = count( $wp_xmlrpc_server->message->params ); 1577 } 1578 1579 /* 1580 * $wp_xmlrpc_server->message looks like this: 1581 * 1582 ( 1583 [message] => 1584 [messageType] => methodCall 1585 [faultCode] => 1586 [faultString] => 1587 [methodName] => system.multicall 1588 [params] => Array 1589 ( 1590 [0] => Array 1591 ( 1592 [methodName] => pingback.ping 1593 [params] => Array 1594 ( 1595 [0] => http://www.example.net/?p=1 // Site that created the pingback. 1596 [1] => https://www.example.com/?p=1 // Post being pingback'd on this site. 1597 ) 1598 ) 1599 [1] => Array 1600 ( 1601 [methodName] => pingback.ping 1602 [params] => Array 1603 ( 1604 [0] => http://www.example.net/?p=1 // Site that created the pingback. 1605 [1] => https://www.example.com/?p=2 // Post being pingback'd on this site. 1606 ) 1607 ) 1608 ) 1609 ) 1610 */ 1611 1612 // Use the params from the nth pingback.ping call in the multicall. 1613 $pingback_calls_found = 0; 1614 1615 foreach ( $wp_xmlrpc_server->message->params as $xmlrpc_action ) { 1616 if ( 'pingback.ping' === $xmlrpc_action['methodName'] ) { 1617 $pingback_calls_found++; 1618 } 1619 1620 if ( $call_count === $pingback_calls_found ) { 1621 $pingback_args = $xmlrpc_action['params']; 1622 break; 1623 } 1624 } 1625 } else { 1626 /* 1627 * $wp_xmlrpc_server->message looks like this: 1628 * 1629 ( 1630 [message] => 1631 [messageType] => methodCall 1632 [faultCode] => 1633 [faultString] => 1634 [methodName] => pingback.ping 1635 [params] => Array 1636 ( 1637 [0] => http://www.example.net/?p=1 // Site that created the pingback. 1638 [1] => https://www.example.com/?p=2 // Post being pingback'd on this site. 1639 ) 1640 ) 1641 */ 1642 $pingback_args = $wp_xmlrpc_server->message->params; 1643 } 1644 1645 if ( ! empty( $pingback_args[1] ) ) { 1646 $post_id = url_to_postid( $pingback_args[1] ); 1647 1648 // If pingbacks aren't open on this post, we'll still check whether this request is part of a potential DDOS, 1649 // but indicate to the server that pingbacks are indeed closed so we don't include this request in the user's stats, 1650 // since the user has already done their part by disabling pingbacks. 1651 $pingbacks_closed = false; 1652 1653 $post = get_post( $post_id ); 1654 1655 if ( ! $post || ! pings_open( $post ) ) { 1656 $pingbacks_closed = true; 1657 } 1658 1659 // Note: If is_multicall is true and multicall_count=0, then we know this is at least the 2nd pingback we've processed in this multicall. 1660 1661 $comment = array( 1662 'comment_author_url' => $pingback_args[0], 1663 'comment_post_ID' => $post_id, 1664 'comment_author' => '', 1665 'comment_author_email' => '', 1666 'comment_content' => '', 1667 'comment_type' => 'pingback', 1668 'akismet_pre_check' => '1', 1669 'comment_pingback_target' => $pingback_args[1], 1670 'pingbacks_closed' => $pingbacks_closed ? '1' : '0', 1671 'is_multicall' => $is_multicall, 1672 'multicall_count' => $multicall_count, 1673 ); 1674 1675 $comment = self::auto_check_comment( $comment, 'xml-rpc' ); 1676 1677 if ( isset( $comment['akismet_result'] ) && 'true' == $comment['akismet_result'] ) { 1678 // Sad: tightly coupled with the IXR classes. Unfortunately the action provides no context and no way to return anything. 1679 $wp_xmlrpc_server->error( new IXR_Error( 0, 'Invalid discovery target' ) ); 1680 1681 // Also note that if this was part of a multicall, a spam result will prevent the subsequent calls from being executed. 1682 // This is probably fine, but it raises the bar for what should be acceptable as a false positive. 1683 } 1684 } 1685 } 1686 1687 /** 1688 * Ensure that we are loading expected scalar values from akismet_as_submitted commentmeta. 1689 * 1690 * @param mixed $meta_value 1691 * @return mixed 1692 */ 1693 private static function sanitize_comment_as_submitted( $meta_value ) { 1694 if ( empty( $meta_value ) ) { 1695 return $meta_value; 1696 } 1697 1698 $meta_value = (array) $meta_value; 1699 1700 foreach ( $meta_value as $key => $value ) { 1701 if ( ! is_scalar( $value ) ) { 1702 unset( $meta_value[ $key ] ); 1703 } else { 1704 // These can change, so they're not explicitly listed in comment_as_submitted_allowed_keys. 1705 if ( strpos( $key, 'POST_ak_' ) === 0 ) { 1706 continue; 1707 } 1708 1709 if ( ! isset( self::$comment_as_submitted_allowed_keys[ $key ] ) ) { 1710 unset( $meta_value[ $key ] ); 1711 } 1712 } 1713 } 1714 1715 return $meta_value; 1716 } 1717 1718 public static function predefined_api_key() { 1719 if ( defined( 'WPCOM_API_KEY' ) ) { 1720 return true; 1721 } 1722 1723 return apply_filters( 'akismet_predefined_api_key', false ); 1724 } 1725 1726 /** 1727 * Controls the display of a privacy related notice underneath the comment form using the `akismet_comment_form_privacy_notice` option and filter respectively. 1728 * Default is top not display the notice, leaving the choice to site admins, or integrators. 1729 */ 1730 public static function display_comment_form_privacy_notice() { 1731 if ( 'display' !== apply_filters( 'akismet_comment_form_privacy_notice', get_option( 'akismet_comment_form_privacy_notice', 'hide' ) ) ) { 1732 return; 1733 } 1734 echo apply_filters( 1735 'akismet_comment_form_privacy_notice_markup', 1736 '<p class="akismet_comment_form_privacy_notice">' . sprintf( 1737 __( 'This site uses Akismet to reduce spam. <a href="%s" target="_blank" rel="nofollow noopener">Learn how your comment data is processed</a>.', 'akismet' ), 1738 'https://akismet.com/privacy/' 1739 ) . '</p>' 1740 ); 1741 } 1742 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Tue Dec 24 01:00:02 2024 | Cross-referenced by PHPXref 0.7.1 |