angelovcom.net

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

class.akismet.php (64804B)


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