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