[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-content/plugins/akismet/ -> class.akismet.php (source)

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


Generated : Wed Aug 13 08:20:01 2025 Cross-referenced by PHPXref