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


Generated : Tue Jan 6 08:20:02 2026 Cross-referenced by PHPXref