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