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