home *** CD-ROM | disk | FTP | other *** search
/ HTML Examples / WP.iso / wordpress2 / wp-content / plugins / akismet / class.akismet.php < prev    next >
Encoding:
PHP Script  |  2017-12-12  |  50.3 KB  |  1,352 lines

  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.     private static $last_comment = '';
  9.     private static $initiated = false;
  10.     private static $prevent_moderation_email_for_these_comments = array();
  11.     private static $last_comment_result = null;
  12.     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' => '' );
  13.     private static $is_rest_api_call = false;
  14.     
  15.     public static function init() {
  16.         if ( ! self::$initiated ) {
  17.             self::init_hooks();
  18.         }
  19.     }
  20.  
  21.     /**
  22.      * Initializes WordPress hooks
  23.      */
  24.     private static function init_hooks() {
  25.         self::$initiated = true;
  26.  
  27.         add_action( 'wp_insert_comment', array( 'Akismet', 'auto_check_update_meta' ), 10, 2 );
  28.         add_filter( 'preprocess_comment', array( 'Akismet', 'auto_check_comment' ), 1 );
  29.         add_filter( 'rest_pre_insert_comment', array( 'Akismet', 'rest_auto_check_comment' ), 1 );
  30.  
  31.         add_action( 'akismet_scheduled_delete', array( 'Akismet', 'delete_old_comments' ) );
  32.         add_action( 'akismet_scheduled_delete', array( 'Akismet', 'delete_old_comments_meta' ) );
  33.         add_action( 'akismet_schedule_cron_recheck', array( 'Akismet', 'cron_recheck' ) );
  34.  
  35.         add_action( 'comment_form',  array( 'Akismet',  'add_comment_nonce' ), 1 );
  36.  
  37.         add_action( 'admin_head-edit-comments.php', array( 'Akismet', 'load_form_js' ) );
  38.         add_action( 'comment_form', array( 'Akismet', 'load_form_js' ) );
  39.         add_action( 'comment_form', array( 'Akismet', 'inject_ak_js' ) );
  40.         add_filter( 'script_loader_tag', array( 'Akismet', 'set_form_js_async' ), 10, 3 );
  41.  
  42.         add_filter( 'comment_moderation_recipients', array( 'Akismet', 'disable_moderation_emails_if_unreachable' ), 1000, 2 );
  43.         add_filter( 'pre_comment_approved', array( 'Akismet', 'last_comment_status' ), 10, 2 );
  44.         
  45.         add_action( 'transition_comment_status', array( 'Akismet', 'transition_comment_status' ), 10, 3 );
  46.  
  47.         // Run this early in the pingback call, before doing a remote fetch of the source uri
  48.         add_action( 'xmlrpc_call', array( 'Akismet', 'pre_check_pingback' ) );
  49.         
  50.         // Jetpack compatibility
  51.         add_filter( 'jetpack_options_whitelist', array( 'Akismet', 'add_to_jetpack_options_whitelist' ) );
  52.         add_action( 'update_option_wordpress_api_key', array( 'Akismet', 'updated_option' ), 10, 2 );
  53.     }
  54.  
  55.     public static function get_api_key() {
  56.         return apply_filters( 'akismet_get_api_key', defined('WPCOM_API_KEY') ? constant('WPCOM_API_KEY') : get_option('wordpress_api_key') );
  57.     }
  58.  
  59.     public static function check_key_status( $key, $ip = null ) {
  60.         return self::http_post( Akismet::build_query( array( 'key' => $key, 'blog' => get_option( 'home' ) ) ), 'verify-key', $ip );
  61.     }
  62.  
  63.     public static function verify_key( $key, $ip = null ) {
  64.         $response = self::check_key_status( $key, $ip );
  65.  
  66.         if ( $response[1] != 'valid' && $response[1] != 'invalid' )
  67.             return 'failed';
  68.  
  69.         return $response[1];
  70.     }
  71.  
  72.     public static function deactivate_key( $key ) {
  73.         $response = self::http_post( Akismet::build_query( array( 'key' => $key, 'blog' => get_option( 'home' ) ) ), 'deactivate' );
  74.  
  75.         if ( $response[1] != 'deactivated' )
  76.             return 'failed';
  77.  
  78.         return $response[1];
  79.     }
  80.  
  81.     /**
  82.      * Add the akismet option to the Jetpack options management whitelist.
  83.      *
  84.      * @param array $options The list of whitelisted option names.
  85.      * @return array The updated whitelist
  86.      */
  87.     public static function add_to_jetpack_options_whitelist( $options ) {
  88.         $options[] = 'wordpress_api_key';
  89.         return $options;
  90.     }
  91.  
  92.     /**
  93.      * When the akismet option is updated, run the registration call.
  94.      *
  95.      * This should only be run when the option is updated from the Jetpack/WP.com
  96.      * API call, and only if the new key is different than the old key.
  97.      *
  98.      * @param mixed  $old_value   The old option value.
  99.      * @param mixed  $value       The new option value.
  100.      */
  101.     public static function updated_option( $old_value, $value ) {
  102.         // Not an API call
  103.         if ( ! class_exists( 'WPCOM_JSON_API_Update_Option_Endpoint' ) ) {
  104.             return;
  105.         }
  106.         // Only run the registration if the old key is different.
  107.         if ( $old_value !== $value ) {
  108.             self::verify_key( $value );
  109.         }
  110.     }
  111.     
  112.     public static function rest_auto_check_comment( $commentdata ) {
  113.         self::$is_rest_api_call = true;
  114.         
  115.         return self::auto_check_comment( $commentdata );
  116.     }
  117.  
  118.     public static function auto_check_comment( $commentdata ) {
  119.         self::$last_comment_result = null;
  120.  
  121.         $comment = $commentdata;
  122.  
  123.         $comment['user_ip']      = self::get_ip_address();
  124.         $comment['user_agent']   = self::get_user_agent();
  125.         $comment['referrer']     = self::get_referer();
  126.         $comment['blog']         = get_option( 'home' );
  127.         $comment['blog_lang']    = get_locale();
  128.         $comment['blog_charset'] = get_option('blog_charset');
  129.         $comment['permalink']    = get_permalink( $comment['comment_post_ID'] );
  130.  
  131.         if ( ! empty( $comment['user_ID'] ) ) {
  132.             $comment['user_role'] = Akismet::get_user_roles( $comment['user_ID'] );
  133.         }
  134.  
  135.         /** See filter documentation in init_hooks(). */
  136.         $akismet_nonce_option = apply_filters( 'akismet_comment_nonce', get_option( 'akismet_comment_nonce' ) );
  137.         $comment['akismet_comment_nonce'] = 'inactive';
  138.         if ( $akismet_nonce_option == 'true' || $akismet_nonce_option == '' ) {
  139.             $comment['akismet_comment_nonce'] = 'failed';
  140.             if ( isset( $_POST['akismet_comment_nonce'] ) && wp_verify_nonce( $_POST['akismet_comment_nonce'], 'akismet_comment_nonce_' . $comment['comment_post_ID'] ) )
  141.                 $comment['akismet_comment_nonce'] = 'passed';
  142.  
  143.             // comment reply in wp-admin
  144.             if ( isset( $_POST['_ajax_nonce-replyto-comment'] ) && check_ajax_referer( 'replyto-comment', '_ajax_nonce-replyto-comment' ) )
  145.                 $comment['akismet_comment_nonce'] = 'passed';
  146.  
  147.         }
  148.  
  149.         if ( self::is_test_mode() )
  150.             $comment['is_test'] = 'true';
  151.  
  152.         foreach( $_POST as $key => $value ) {
  153.             if ( is_string( $value ) )
  154.                 $comment["POST_{$key}"] = $value;
  155.         }
  156.  
  157.         foreach ( $_SERVER as $key => $value ) {
  158.             if ( ! is_string( $value ) ) {
  159.                 continue;
  160.             }
  161.  
  162.             if ( preg_match( "/^HTTP_COOKIE/", $key ) ) {
  163.                 continue;
  164.             }
  165.  
  166.             // Send any potentially useful $_SERVER vars, but avoid sending junk we don't need.
  167.             if ( preg_match( "/^(HTTP_|REMOTE_ADDR|REQUEST_URI|DOCUMENT_URI)/", $key ) ) {
  168.                 $comment[ "$key" ] = $value;
  169.             }
  170.         }
  171.  
  172.         $post = get_post( $comment['comment_post_ID'] );
  173.  
  174.         if ( ! is_null( $post ) ) {
  175.             // $post can technically be null, although in the past, it's always been an indicator of another plugin interfering.
  176.             $comment[ 'comment_post_modified_gmt' ] = $post->post_modified_gmt;
  177.         }
  178.  
  179.         $response = self::http_post( Akismet::build_query( $comment ), 'comment-check' );
  180.  
  181.         do_action( 'akismet_comment_check_response', $response );
  182.  
  183.         $commentdata['comment_as_submitted'] = array_intersect_key( $comment, self::$comment_as_submitted_allowed_keys );
  184.         $commentdata['akismet_result']       = $response[1];
  185.  
  186.         if ( isset( $response[0]['x-akismet-pro-tip'] ) )
  187.             $commentdata['akismet_pro_tip'] = $response[0]['x-akismet-pro-tip'];
  188.  
  189.         if ( isset( $response[0]['x-akismet-error'] ) ) {
  190.             // An error occurred that we anticipated (like a suspended key) and want the user to act on.
  191.             // Send to moderation.
  192.             self::$last_comment_result = '0';
  193.         }
  194.         else if ( 'true' == $response[1] ) {
  195.             // akismet_spam_count will be incremented later by comment_is_spam()
  196.             self::$last_comment_result = 'spam';
  197.  
  198.             $discard = ( isset( $commentdata['akismet_pro_tip'] ) && $commentdata['akismet_pro_tip'] === 'discard' && self::allow_discard() );
  199.  
  200.             do_action( 'akismet_spam_caught', $discard );
  201.  
  202.             if ( $discard ) {
  203.                 // The spam is obvious, so we're bailing out early. 
  204.                 // akismet_result_spam() won't be called so bump the counter here
  205.                 if ( $incr = apply_filters( 'akismet_spam_count_incr', 1 ) ) {
  206.                     update_option( 'akismet_spam_count', get_option( 'akismet_spam_count' ) + $incr );
  207.                 }
  208.  
  209.                 if ( self::$is_rest_api_call ) {
  210.                     return new WP_Error( 'akismet_rest_comment_discarded', __( 'Comment discarded.', 'akismet' ) );
  211.                 }
  212.                 else {
  213.                     // Redirect back to the previous page, or failing that, the post permalink, or failing that, the homepage of the blog.
  214.                     $redirect_to = isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : ( $post ? get_permalink( $post ) : home_url() );
  215.                     wp_safe_redirect( esc_url_raw( $redirect_to ) );
  216.                     die();
  217.                 }
  218.             }
  219.             else if ( self::$is_rest_api_call ) {
  220.                 // The way the REST API structures its calls, we can set the comment_approved value right away.
  221.                 $commentdata['comment_approved'] = 'spam';
  222.             }
  223.         }
  224.         
  225.         // if the response is neither true nor false, hold the comment for moderation and schedule a recheck
  226.         if ( 'true' != $response[1] && 'false' != $response[1] ) {
  227.             if ( !current_user_can('moderate_comments') ) {
  228.                 // Comment status should be moderated
  229.                 self::$last_comment_result = '0';
  230.             }
  231.  
  232.             if ( ! wp_next_scheduled( 'akismet_schedule_cron_recheck' ) ) {
  233.                 wp_schedule_single_event( time() + 1200, 'akismet_schedule_cron_recheck' );
  234.                 do_action( 'akismet_scheduled_recheck', 'invalid-response-' . $response[1] );
  235.             }
  236.  
  237.             self::$prevent_moderation_email_for_these_comments[] = $commentdata;
  238.         }
  239.  
  240.         // Delete old comments daily
  241.         if ( ! wp_next_scheduled( 'akismet_scheduled_delete' ) ) {
  242.             wp_schedule_event( time(), 'daily', 'akismet_scheduled_delete' );
  243.         }
  244.  
  245.         self::set_last_comment( $commentdata );
  246.         self::fix_scheduled_recheck();
  247.  
  248.         return $commentdata;
  249.     }
  250.     
  251.     public static function get_last_comment() {
  252.         return self::$last_comment;
  253.     }
  254.     
  255.     public static function set_last_comment( $comment ) {
  256.         if ( is_null( $comment ) ) {
  257.             self::$last_comment = null;
  258.         }
  259.         else {
  260.             // We filter it here so that it matches the filtered comment data that we'll have to compare against later.
  261.             // wp_filter_comment expects comment_author_IP
  262.             self::$last_comment = wp_filter_comment(
  263.                 array_merge(
  264.                     array( 'comment_author_IP' => self::get_ip_address() ),
  265.                     $comment
  266.                 )
  267.             );
  268.         }
  269.     }
  270.  
  271.     // this fires on wp_insert_comment.  we can't update comment_meta when auto_check_comment() runs
  272.     // because we don't know the comment ID at that point.
  273.     public static function auto_check_update_meta( $id, $comment ) {
  274.         // wp_insert_comment() might be called in other contexts, so make sure this is the same comment
  275.         // as was checked by auto_check_comment
  276.         if ( is_object( $comment ) && !empty( self::$last_comment ) && is_array( self::$last_comment ) ) {
  277.             if ( self::matches_last_comment( $comment ) ) {
  278.                     
  279.                     load_plugin_textdomain( 'akismet' );
  280.                     
  281.                     // normal result: true or false
  282.                     if ( self::$last_comment['akismet_result'] == 'true' ) {
  283.                         update_comment_meta( $comment->comment_ID, 'akismet_result', 'true' );
  284.                         self::update_comment_history( $comment->comment_ID, '', 'check-spam' );
  285.                         if ( $comment->comment_approved != 'spam' )
  286.                             self::update_comment_history(
  287.                                 $comment->comment_ID,
  288.                                 '',
  289.                                 'status-changed-'.$comment->comment_approved
  290.                             );
  291.                     }
  292.                     elseif ( self::$last_comment['akismet_result'] == 'false' ) {
  293.                         update_comment_meta( $comment->comment_ID, 'akismet_result', 'false' );
  294.                         self::update_comment_history( $comment->comment_ID, '', 'check-ham' );
  295.                         // Status could be spam or trash, depending on the WP version and whether this change applies:
  296.                         // https://core.trac.wordpress.org/changeset/34726
  297.                         if ( $comment->comment_approved == 'spam' || $comment->comment_approved == 'trash' ) {
  298.                             if ( wp_blacklist_check($comment->comment_author, $comment->comment_author_email, $comment->comment_author_url, $comment->comment_content, $comment->comment_author_IP, $comment->comment_agent) )
  299.                                 self::update_comment_history( $comment->comment_ID, '', 'wp-blacklisted' );
  300.                             else
  301.                                 self::update_comment_history( $comment->comment_ID, '', 'status-changed-'.$comment->comment_approved );
  302.                         }
  303.                     } // abnormal result: error
  304.                     else {
  305.                         update_comment_meta( $comment->comment_ID, 'akismet_error', time() );
  306.                         self::update_comment_history(
  307.                             $comment->comment_ID,
  308.                             '',
  309.                             'check-error',
  310.                             array( 'response' => substr( self::$last_comment['akismet_result'], 0, 50 ) )
  311.                         );
  312.                     }
  313.  
  314.                     // record the complete original data as submitted for checking
  315.                     if ( isset( self::$last_comment['comment_as_submitted'] ) )
  316.                         update_comment_meta( $comment->comment_ID, 'akismet_as_submitted', self::$last_comment['comment_as_submitted'] );
  317.  
  318.                     if ( isset( self::$last_comment['akismet_pro_tip'] ) )
  319.                         update_comment_meta( $comment->comment_ID, 'akismet_pro_tip', self::$last_comment['akismet_pro_tip'] );
  320.             }
  321.         }
  322.     }
  323.  
  324.     public static function delete_old_comments() {
  325.         global $wpdb;
  326.  
  327.         /**
  328.          * Determines how many comments will be deleted in each batch.
  329.          *
  330.          * @param int The default, as defined by AKISMET_DELETE_LIMIT.
  331.          */
  332.         $delete_limit = apply_filters( 'akismet_delete_comment_limit', defined( 'AKISMET_DELETE_LIMIT' ) ? AKISMET_DELETE_LIMIT : 10000 );
  333.         $delete_limit = max( 1, intval( $delete_limit ) );
  334.  
  335.         /**
  336.          * Determines how many days a comment will be left in the Spam queue before being deleted.
  337.          *
  338.          * @param int The default number of days.
  339.          */
  340.         $delete_interval = apply_filters( 'akismet_delete_comment_interval', 15 );
  341.         $delete_interval = max( 1, intval( $delete_interval ) );
  342.  
  343.         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 ) ) ) {
  344.             if ( empty( $comment_ids ) )
  345.                 return;
  346.  
  347.             $wpdb->queries = array();
  348.  
  349.             foreach ( $comment_ids as $comment_id ) {
  350.                 do_action( 'delete_comment', $comment_id );
  351.             }
  352.  
  353.             // Prepared as strings since comment_id is an unsigned BIGINT, and using %d will constrain the value to the maximum signed BIGINT.
  354.             $format_string = implode( ", ", array_fill( 0, count( $comment_ids ), '%s' ) );
  355.  
  356.             $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->comments} WHERE comment_id IN ( " . $format_string . " )", $comment_ids ) );
  357.             $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->commentmeta} WHERE comment_id IN ( " . $format_string . " )", $comment_ids ) );
  358.  
  359.             clean_comment_cache( $comment_ids );
  360.             do_action( 'akismet_delete_comment_batch', count( $comment_ids ) );
  361.         }
  362.  
  363.         if ( apply_filters( 'akismet_optimize_table', ( mt_rand(1, 5000) == 11), $wpdb->comments ) ) // lucky number
  364.             $wpdb->query("OPTIMIZE TABLE {$wpdb->comments}");
  365.     }
  366.  
  367.     public static function delete_old_comments_meta() {
  368.         global $wpdb;
  369.  
  370.         $interval = apply_filters( 'akismet_delete_commentmeta_interval', 15 );
  371.  
  372.         # enfore a minimum of 1 day
  373.         $interval = absint( $interval );
  374.         if ( $interval < 1 )
  375.             $interval = 1;
  376.  
  377.         // akismet_as_submitted meta values are large, so expire them
  378.         // after $interval days regardless of the comment status
  379.         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 ) ) ) {
  380.             if ( empty( $comment_ids ) )
  381.                 return;
  382.  
  383.             $wpdb->queries = array();
  384.  
  385.             foreach ( $comment_ids as $comment_id ) {
  386.                 delete_comment_meta( $comment_id, 'akismet_as_submitted' );
  387.             }
  388.  
  389.             do_action( 'akismet_delete_commentmeta_batch', count( $comment_ids ) );
  390.         }
  391.  
  392.         if ( apply_filters( 'akismet_optimize_table', ( mt_rand(1, 5000) == 11), $wpdb->commentmeta ) ) // lucky number
  393.             $wpdb->query("OPTIMIZE TABLE {$wpdb->commentmeta}");
  394.     }
  395.  
  396.     // how many approved comments does this author have?
  397.     public static function get_user_comments_approved( $user_id, $comment_author_email, $comment_author, $comment_author_url ) {
  398.         global $wpdb;
  399.  
  400.         if ( !empty( $user_id ) )
  401.             return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->comments} WHERE user_id = %d AND comment_approved = 1", $user_id ) );
  402.  
  403.         if ( !empty( $comment_author_email ) )
  404.             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 ) );
  405.  
  406.         return 0;
  407.     }
  408.  
  409.     // get the full comment history for a given comment, as an array in reverse chronological order
  410.     public static function get_comment_history( $comment_id ) {
  411.         $history = get_comment_meta( $comment_id, 'akismet_history', false );
  412.         usort( $history, array( 'Akismet', '_cmp_time' ) );
  413.         return $history;
  414.     }
  415.  
  416.     /**
  417.      * Log an event for a given comment, storing it in comment_meta.
  418.      *
  419.      * @param int $comment_id The ID of the relevant comment.
  420.      * @param string $message The string description of the event. No longer used.
  421.      * @param string $event The event code.
  422.      * @param array $meta Metadata about the history entry. e.g., the user that reported or changed the status of a given comment.
  423.      */
  424.     public static function update_comment_history( $comment_id, $message, $event=null, $meta=null ) {
  425.         global $current_user;
  426.  
  427.         $user = '';
  428.  
  429.         $event = array(
  430.             'time'    => self::_get_microtime(),
  431.             'event'   => $event,
  432.         );
  433.         
  434.         if ( is_object( $current_user ) && isset( $current_user->user_login ) ) {
  435.             $event['user'] = $current_user->user_login;
  436.         }
  437.         
  438.         if ( ! empty( $meta ) ) {
  439.             $event['meta'] = $meta;
  440.         }
  441.  
  442.         // $unique = false so as to allow multiple values per comment
  443.         $r = add_comment_meta( $comment_id, 'akismet_history', $event, false );
  444.     }
  445.  
  446.     public static function check_db_comment( $id, $recheck_reason = 'recheck_queue' ) {
  447.         global $wpdb;
  448.  
  449.         $c = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->comments} WHERE comment_ID = %d", $id ), ARRAY_A );
  450.         
  451.         if ( ! $c ) {
  452.             return new WP_Error( 'invalid-comment-id', __( 'Comment not found.', 'akismet' ) );
  453.         }
  454.  
  455.         $c['user_ip']        = $c['comment_author_IP'];
  456.         $c['user_agent']     = $c['comment_agent'];
  457.         $c['referrer']       = '';
  458.         $c['blog']           = get_option( 'home' );
  459.         $c['blog_lang']      = get_locale();
  460.         $c['blog_charset']   = get_option('blog_charset');
  461.         $c['permalink']      = get_permalink($c['comment_post_ID']);
  462.         $c['recheck_reason'] = $recheck_reason;
  463.  
  464.         $c['user_role'] = '';
  465.         if ( ! empty( $c['user_ID'] ) ) {
  466.             $c['user_role'] = Akismet::get_user_roles( $c['user_ID'] );
  467.         }
  468.  
  469.         if ( self::is_test_mode() )
  470.             $c['is_test'] = 'true';
  471.  
  472.         $response = self::http_post( Akismet::build_query( $c ), 'comment-check' );
  473.  
  474.         if ( ! empty( $response[1] ) ) {
  475.             return $response[1];
  476.         }
  477.  
  478.         return false;
  479.     }
  480.     
  481.     public static function recheck_comment( $id, $recheck_reason = 'recheck_queue' ) {
  482.         add_comment_meta( $id, 'akismet_rechecking', true );
  483.         
  484.         $api_response = self::check_db_comment( $id, $recheck_reason );
  485.  
  486.         delete_comment_meta( $id, 'akismet_rechecking' );
  487.  
  488.         if ( is_wp_error( $api_response ) ) {
  489.             // Invalid comment ID.
  490.         }
  491.         else if ( 'true' === $api_response ) {
  492.             wp_set_comment_status( $id, 'spam' );
  493.             update_comment_meta( $id, 'akismet_result', 'true' );
  494.             delete_comment_meta( $id, 'akismet_error' );
  495.             delete_comment_meta( $id, 'akismet_delayed_moderation_email' );
  496.             Akismet::update_comment_history( $id, '', 'recheck-spam' );
  497.         }
  498.         elseif ( 'false' === $api_response ) {
  499.             update_comment_meta( $id, 'akismet_result', 'false' );
  500.             delete_comment_meta( $id, 'akismet_error' );
  501.             delete_comment_meta( $id, 'akismet_delayed_moderation_email' );
  502.             Akismet::update_comment_history( $id, '', 'recheck-ham' );
  503.         }
  504.         else {
  505.             // abnormal result: error
  506.             update_comment_meta( $id, 'akismet_result', 'error' );
  507.             Akismet::update_comment_history(
  508.                 $id,
  509.                 '',
  510.                 'recheck-error',
  511.                 array( 'response' => substr( $api_response, 0, 50 ) )
  512.             );
  513.         }
  514.  
  515.         return $api_response;
  516.     }
  517.  
  518.     public static function transition_comment_status( $new_status, $old_status, $comment ) {
  519.         
  520.         if ( $new_status == $old_status )
  521.             return;
  522.  
  523.         # we don't need to record a history item for deleted comments
  524.         if ( $new_status == 'delete' )
  525.             return;
  526.         
  527.         if ( !current_user_can( 'edit_post', $comment->comment_post_ID ) && !current_user_can( 'moderate_comments' ) )
  528.             return;
  529.  
  530.         if ( defined('WP_IMPORTING') && WP_IMPORTING == true )
  531.             return;
  532.             
  533.         // if this is present, it means the status has been changed by a re-check, not an explicit user action
  534.         if ( get_comment_meta( $comment->comment_ID, 'akismet_rechecking' ) )
  535.             return;
  536.         
  537.         // Assumption alert:
  538.         // We want to submit comments to Akismet only when a moderator explicitly spams or approves it - not if the status
  539.         // is changed automatically by another plugin.  Unfortunately WordPress doesn't provide an unambiguous way to
  540.         // determine why the transition_comment_status action was triggered.  And there are several different ways by which
  541.         // to spam and unspam comments: bulk actions, ajax, links in moderation emails, the dashboard, and perhaps others.
  542.         // We'll assume that this is an explicit user action if certain POST/GET variables exist.
  543.         if (
  544.              // status=spam: Marking as spam via the REST API or...
  545.              // 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?...
  546.              // status=approved: Unspamming via the REST API (Calypso) or...
  547.              ( isset( $_POST['status'] ) && in_array( $_POST['status'], array( 'spam', 'unspam', 'approved', ) ) )
  548.              // spam=1: Clicking "Spam" underneath a comment in wp-admin and allowing the AJAX request to happen.
  549.              || ( isset( $_POST['spam'] ) && (int) $_POST['spam'] == 1 )
  550.              // 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.
  551.              || ( isset( $_POST['unspam'] ) && (int) $_POST['unspam'] == 1 )
  552.              // comment_status=spam/unspam: It's unclear where this is happening.
  553.              || ( isset( $_POST['comment_status'] )  && in_array( $_POST['comment_status'], array( 'spam', 'unspam' ) ) )
  554.              // action=spam: Choosing "Mark as Spam" from the Bulk Actions dropdown in wp-admin (or the "Spam it" link in notification emails).
  555.              // action=unspam: Choosing "Not Spam" from the Bulk Actions dropdown in wp-admin.
  556.              // action=spamcomment: Following the "Spam" link below a comment in wp-admin (not allowing AJAX request to happen).
  557.              // action=unspamcomment: Following the "Not Spam" link below a comment in wp-admin (not allowing AJAX request to happen).
  558.              || ( isset( $_GET['action'] ) && in_array( $_GET['action'], array( 'spam', 'unspam', 'spamcomment', 'unspamcomment', ) ) )
  559.              // action=editedcomment: Editing a comment via wp-admin (and possibly changing its status).
  560.              || ( isset( $_POST['action'] ) && in_array( $_POST['action'], array( 'editedcomment' ) ) )
  561.              // for=jetpack: Moderation via the WordPress app, Calypso, anything powered by the Jetpack connection.
  562.              || ( isset( $_GET['for'] ) && ( 'jetpack' == $_GET['for'] ) && ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) ) 
  563.              // Certain WordPress.com API requests
  564.              || ( defined( 'REST_API_REQUEST' ) && REST_API_REQUEST )
  565.              // WordPress.org REST API requests
  566.              || ( defined( 'REST_REQUEST' ) && REST_REQUEST )
  567.          ) {
  568.             if ( $new_status == 'spam' && ( $old_status == 'approved' || $old_status == 'unapproved' || !$old_status ) ) {
  569.                 return self::submit_spam_comment( $comment->comment_ID );
  570.             } elseif ( $old_status == 'spam' && ( $new_status == 'approved' || $new_status == 'unapproved' ) ) {
  571.                 return self::submit_nonspam_comment( $comment->comment_ID );
  572.             }
  573.         }
  574.  
  575.         self::update_comment_history( $comment->comment_ID, '', 'status-' . $new_status );
  576.     }
  577.     
  578.     public static function submit_spam_comment( $comment_id ) {
  579.         global $wpdb, $current_user, $current_site;
  580.  
  581.         $comment_id = (int) $comment_id;
  582.  
  583.         $comment = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->comments} WHERE comment_ID = %d", $comment_id ) );
  584.  
  585.         if ( !$comment ) // it was deleted
  586.             return;
  587.  
  588.         if ( 'spam' != $comment->comment_approved )
  589.             return;
  590.  
  591.         // use the original version stored in comment_meta if available
  592.         $as_submitted = self::sanitize_comment_as_submitted( get_comment_meta( $comment_id, 'akismet_as_submitted', true ) );
  593.  
  594.         if ( $as_submitted && is_array( $as_submitted ) && isset( $as_submitted['comment_content'] ) )
  595.             $comment = (object) array_merge( (array)$comment, $as_submitted );
  596.  
  597.         $comment->blog         = get_option( 'home' );
  598.         $comment->blog_lang    = get_locale();
  599.         $comment->blog_charset = get_option('blog_charset');
  600.         $comment->permalink    = get_permalink($comment->comment_post_ID);
  601.  
  602.         if ( is_object($current_user) )
  603.             $comment->reporter = $current_user->user_login;
  604.  
  605.         if ( is_object($current_site) )
  606.             $comment->site_domain = $current_site->domain;
  607.  
  608.         $comment->user_role = '';
  609.         if ( ! empty( $comment->user_ID ) ) {
  610.             $comment->user_role = Akismet::get_user_roles( $comment->user_ID );
  611.         }
  612.  
  613.         if ( self::is_test_mode() )
  614.             $comment->is_test = 'true';
  615.  
  616.         $post = get_post( $comment->comment_post_ID );
  617.  
  618.         if ( ! is_null( $post ) ) {
  619.             $comment->comment_post_modified_gmt = $post->post_modified_gmt;
  620.         }
  621.  
  622.         $response = Akismet::http_post( Akismet::build_query( $comment ), 'submit-spam' );
  623.         if ( $comment->reporter ) {
  624.             self::update_comment_history( $comment_id, '', 'report-spam' );
  625.             update_comment_meta( $comment_id, 'akismet_user_result', 'true' );
  626.             update_comment_meta( $comment_id, 'akismet_user', $comment->reporter );
  627.         }
  628.  
  629.         do_action('akismet_submit_spam_comment', $comment_id, $response[1]);
  630.     }
  631.  
  632.     public static function submit_nonspam_comment( $comment_id ) {
  633.         global $wpdb, $current_user, $current_site;
  634.  
  635.         $comment_id = (int) $comment_id;
  636.  
  637.         $comment = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->comments} WHERE comment_ID = %d", $comment_id ) );
  638.         if ( !$comment ) // it was deleted
  639.             return;
  640.  
  641.         // use the original version stored in comment_meta if available
  642.         $as_submitted = self::sanitize_comment_as_submitted( get_comment_meta( $comment_id, 'akismet_as_submitted', true ) );
  643.  
  644.         if ( $as_submitted && is_array($as_submitted) && isset($as_submitted['comment_content']) )
  645.             $comment = (object) array_merge( (array)$comment, $as_submitted );
  646.  
  647.         $comment->blog         = get_option( 'home' );
  648.         $comment->blog_lang    = get_locale();
  649.         $comment->blog_charset = get_option('blog_charset');
  650.         $comment->permalink    = get_permalink( $comment->comment_post_ID );
  651.         $comment->user_role    = '';
  652.  
  653.         if ( is_object($current_user) )
  654.             $comment->reporter = $current_user->user_login;
  655.  
  656.         if ( is_object($current_site) )
  657.             $comment->site_domain = $current_site->domain;
  658.  
  659.         if ( ! empty( $comment->user_ID ) ) {
  660.             $comment->user_role = Akismet::get_user_roles( $comment->user_ID );
  661.         }
  662.  
  663.         if ( Akismet::is_test_mode() )
  664.             $comment->is_test = 'true';
  665.  
  666.         $post = get_post( $comment->comment_post_ID );
  667.  
  668.         if ( ! is_null( $post ) ) {
  669.             $comment->comment_post_modified_gmt = $post->post_modified_gmt;
  670.         }
  671.  
  672.         $response = self::http_post( Akismet::build_query( $comment ), 'submit-ham' );
  673.         if ( $comment->reporter ) {
  674.             self::update_comment_history( $comment_id, '', 'report-ham' );
  675.             update_comment_meta( $comment_id, 'akismet_user_result', 'false' );
  676.             update_comment_meta( $comment_id, 'akismet_user', $comment->reporter );
  677.         }
  678.  
  679.         do_action('akismet_submit_nonspam_comment', $comment_id, $response[1]);
  680.     }
  681.  
  682.     public static function cron_recheck() {
  683.         global $wpdb;
  684.  
  685.         $api_key = self::get_api_key();
  686.  
  687.         $status = self::verify_key( $api_key );
  688.         if ( get_option( 'akismet_alert_code' ) || $status == 'invalid' ) {
  689.             // since there is currently a problem with the key, reschedule a check for 6 hours hence
  690.             wp_schedule_single_event( time() + 21600, 'akismet_schedule_cron_recheck' );
  691.             do_action( 'akismet_scheduled_recheck', 'key-problem-' . get_option( 'akismet_alert_code' ) . '-' . $status );
  692.             return false;
  693.         }
  694.  
  695.         delete_option('akismet_available_servers');
  696.  
  697.         $comment_errors = $wpdb->get_col( "SELECT comment_id FROM {$wpdb->commentmeta} WHERE meta_key = 'akismet_error'    LIMIT 100" );
  698.         
  699.         load_plugin_textdomain( 'akismet' );
  700.  
  701.         foreach ( (array) $comment_errors as $comment_id ) {
  702.             // if the comment no longer exists, or is too old, remove the meta entry from the queue to avoid getting stuck
  703.             $comment = get_comment( $comment_id );
  704.  
  705.             if (
  706.                 ! $comment // Comment has been deleted
  707.                 || strtotime( $comment->comment_date_gmt ) < strtotime( "-15 days" ) // Comment is too old.
  708.                 || $comment->comment_approved !== "0" // Comment is no longer in the Pending queue
  709.                 ) {
  710.                 echo "Deleting";
  711.                 delete_comment_meta( $comment_id, 'akismet_error' );
  712.                 delete_comment_meta( $comment_id, 'akismet_delayed_moderation_email' );
  713.                 continue;
  714.             }
  715.  
  716.             add_comment_meta( $comment_id, 'akismet_rechecking', true );
  717.             $status = self::check_db_comment( $comment_id, 'retry' );
  718.  
  719.             $event = '';
  720.             if ( $status == 'true' ) {
  721.                 $event = 'cron-retry-spam';
  722.             } elseif ( $status == 'false' ) {
  723.                 $event = 'cron-retry-ham';
  724.             }
  725.  
  726.             // If we got back a legit response then update the comment history
  727.             // other wise just bail now and try again later.  No point in
  728.             // re-trying all the comments once we hit one failure.
  729.             if ( !empty( $event ) ) {
  730.                 delete_comment_meta( $comment_id, 'akismet_error' );
  731.                 self::update_comment_history( $comment_id, '', $event );
  732.                 update_comment_meta( $comment_id, 'akismet_result', $status );
  733.                 // make sure the comment status is still pending.  if it isn't, that means the user has already moved it elsewhere.
  734.                 $comment = get_comment( $comment_id );
  735.                 if ( $comment && 'unapproved' == wp_get_comment_status( $comment_id ) ) {
  736.                     if ( $status == 'true' ) {
  737.                         wp_spam_comment( $comment_id );
  738.                     } elseif ( $status == 'false' ) {
  739.                         // comment is good, but it's still in the pending queue.  depending on the moderation settings
  740.                         // we may need to change it to approved.
  741.                         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) )
  742.                             wp_set_comment_status( $comment_id, 1 );
  743.                         else if ( get_comment_meta( $comment_id, 'akismet_delayed_moderation_email', true ) )
  744.                             wp_notify_moderator( $comment_id );
  745.                     }
  746.                 }
  747.                 
  748.                 delete_comment_meta( $comment_id, 'akismet_delayed_moderation_email' );
  749.             } else {
  750.                 // If this comment has been pending moderation for longer than MAX_DELAY_BEFORE_MODERATION_EMAIL,
  751.                 // send a moderation email now.
  752.                 if ( ( intval( gmdate( 'U' ) ) - strtotime( $comment->comment_date_gmt ) ) < self::MAX_DELAY_BEFORE_MODERATION_EMAIL ) {
  753.                     delete_comment_meta( $comment_id, 'akismet_delayed_moderation_email' );
  754.                     wp_notify_moderator( $comment_id );
  755.                 }
  756.  
  757.                 delete_comment_meta( $comment_id, 'akismet_rechecking' );
  758.                 wp_schedule_single_event( time() + 1200, 'akismet_schedule_cron_recheck' );
  759.                 do_action( 'akismet_scheduled_recheck', 'check-db-comment-' . $status );
  760.                 return;
  761.             }
  762.             delete_comment_meta( $comment_id, 'akismet_rechecking' );
  763.         }
  764.  
  765.         $remaining = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->commentmeta} WHERE meta_key = 'akismet_error'" );
  766.         if ( $remaining && !wp_next_scheduled('akismet_schedule_cron_recheck') ) {
  767.             wp_schedule_single_event( time() + 1200, 'akismet_schedule_cron_recheck' );
  768.             do_action( 'akismet_scheduled_recheck', 'remaining' );
  769.         }
  770.     }
  771.  
  772.     public static function fix_scheduled_recheck() {
  773.         $future_check = wp_next_scheduled( 'akismet_schedule_cron_recheck' );
  774.         if ( !$future_check ) {
  775.             return;
  776.         }
  777.  
  778.         if ( get_option( 'akismet_alert_code' ) > 0 ) {
  779.             return;
  780.         }
  781.  
  782.         $check_range = time() + 1200;
  783.         if ( $future_check > $check_range ) {
  784.             wp_clear_scheduled_hook( 'akismet_schedule_cron_recheck' );
  785.             wp_schedule_single_event( time() + 300, 'akismet_schedule_cron_recheck' );
  786.             do_action( 'akismet_scheduled_recheck', 'fix-scheduled-recheck' );
  787.         }
  788.     }
  789.  
  790.     public static function add_comment_nonce( $post_id ) {
  791.         /**
  792.          * To disable the Akismet comment nonce, add a filter for the 'akismet_comment_nonce' tag
  793.          * and return any string value that is not 'true' or '' (empty string).
  794.          *
  795.          * Don't return boolean false, because that implies that the 'akismet_comment_nonce' option
  796.          * has not been set and that Akismet should just choose the default behavior for that
  797.          * situation.
  798.          */
  799.         $akismet_comment_nonce_option = apply_filters( 'akismet_comment_nonce', get_option( 'akismet_comment_nonce' ) );
  800.  
  801.         if ( $akismet_comment_nonce_option == 'true' || $akismet_comment_nonce_option == '' ) {
  802.             echo '<p style="display: none;">';
  803.             wp_nonce_field( 'akismet_comment_nonce_' . $post_id, 'akismet_comment_nonce', FALSE );
  804.             echo '</p>';
  805.         }
  806.     }
  807.  
  808.     public static function is_test_mode() {
  809.         return defined('AKISMET_TEST_MODE') && AKISMET_TEST_MODE;
  810.     }
  811.     
  812.     public static function allow_discard() {
  813.         if ( defined( 'DOING_AJAX' ) && DOING_AJAX )
  814.             return false;
  815.         if ( is_user_logged_in() )
  816.             return false;
  817.     
  818.         return ( get_option( 'akismet_strictness' ) === '1'  );
  819.     }
  820.  
  821.     public static function get_ip_address() {
  822.         return isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : null;
  823.     }
  824.     
  825.     /**
  826.      * Do these two comments, without checking the comment_ID, "match"?
  827.      *
  828.      * @param mixed $comment1 A comment object or array.
  829.      * @param mixed $comment2 A comment object or array.
  830.      * @return bool Whether the two comments should be treated as the same comment.
  831.      */
  832.     private static function comments_match( $comment1, $comment2 ) {
  833.         $comment1 = (array) $comment1;
  834.         $comment2 = (array) $comment2;
  835.  
  836.         // Set default values for these strings that we check in order to simplify
  837.         // the checks and avoid PHP warnings.
  838.         if ( ! isset( $comment1['comment_author'] ) ) {
  839.             $comment1['comment_author'] = '';
  840.         }
  841.  
  842.         if ( ! isset( $comment2['comment_author'] ) ) {
  843.             $comment2['comment_author'] = '';
  844.         }
  845.  
  846.         if ( ! isset( $comment1['comment_author_email'] ) ) {
  847.             $comment1['comment_author_email'] = '';
  848.         }
  849.  
  850.         if ( ! isset( $comment2['comment_author_email'] ) ) {
  851.             $comment2['comment_author_email'] = '';
  852.         }
  853.  
  854.         $comments_match = (
  855.                isset( $comment1['comment_post_ID'], $comment2['comment_post_ID'] )
  856.             && intval( $comment1['comment_post_ID'] ) == intval( $comment2['comment_post_ID'] )
  857.             && (
  858.                 // The comment author length max is 255 characters, limited by the TINYTEXT column type.
  859.                 // If the comment author includes multibyte characters right around the 255-byte mark, they
  860.                 // may be stripped when the author is saved in the DB, so a 300+ char author may turn into
  861.                 // a 253-char author when it's saved, not 255 exactly.  The longest possible character is
  862.                 // theoretically 6 bytes, so we'll only look at the first 248 bytes to be safe.
  863.                 substr( $comment1['comment_author'], 0, 248 ) == substr( $comment2['comment_author'], 0, 248 )
  864.                 || substr( stripslashes( $comment1['comment_author'] ), 0, 248 ) == substr( $comment2['comment_author'], 0, 248 )
  865.                 || substr( $comment1['comment_author'], 0, 248 ) == substr( stripslashes( $comment2['comment_author'] ), 0, 248 )
  866.                 // Certain long comment author names will be truncated to nothing, depending on their encoding.
  867.                 || ( ! $comment1['comment_author'] && strlen( $comment2['comment_author'] ) > 248 )
  868.                 || ( ! $comment2['comment_author'] && strlen( $comment1['comment_author'] ) > 248 )
  869.                 )
  870.             && (
  871.                 // The email max length is 100 characters, limited by the VARCHAR(100) column type.
  872.                 // Same argument as above for only looking at the first 93 characters.
  873.                 substr( $comment1['comment_author_email'], 0, 93 ) == substr( $comment2['comment_author_email'], 0, 93 )
  874.                 || substr( stripslashes( $comment1['comment_author_email'] ), 0, 93 ) == substr( $comment2['comment_author_email'], 0, 93 )
  875.                 || substr( $comment1['comment_author_email'], 0, 93 ) == substr( stripslashes( $comment2['comment_author_email'] ), 0, 93 )
  876.                 // Very long emails can be truncated and then stripped if the [0:100] substring isn't a valid address.
  877.                 || ( ! $comment1['comment_author_email'] && strlen( $comment2['comment_author_email'] ) > 100 )
  878.                 || ( ! $comment2['comment_author_email'] && strlen( $comment1['comment_author_email'] ) > 100 )
  879.             )
  880.         );
  881.  
  882.         return $comments_match;
  883.     }
  884.     
  885.     // Does the supplied comment match the details of the one most recently stored in self::$last_comment?
  886.     public static function matches_last_comment( $comment ) {
  887.         return self::comments_match( self::$last_comment, $comment );
  888.     }
  889.  
  890.     private static function get_user_agent() {
  891.         return isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : null;
  892.     }
  893.  
  894.     private static function get_referer() {
  895.         return isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : null;
  896.     }
  897.  
  898.     // return a comma-separated list of role names for the given user
  899.     public static function get_user_roles( $user_id ) {
  900.         $roles = false;
  901.  
  902.         if ( !class_exists('WP_User') )
  903.             return false;
  904.  
  905.         if ( $user_id > 0 ) {
  906.             $comment_user = new WP_User( $user_id );
  907.             if ( isset( $comment_user->roles ) )
  908.                 $roles = join( ',', $comment_user->roles );
  909.         }
  910.  
  911.         if ( is_multisite() && is_super_admin( $user_id ) ) {
  912.             if ( empty( $roles ) ) {
  913.                 $roles = 'super_admin';
  914.             } else {
  915.                 $comment_user->roles[] = 'super_admin';
  916.                 $roles = join( ',', $comment_user->roles );
  917.             }
  918.         }
  919.  
  920.         return $roles;
  921.     }
  922.  
  923.     // filter handler used to return a spam result to pre_comment_approved
  924.     public static function last_comment_status( $approved, $comment ) {
  925.         if ( is_null( self::$last_comment_result ) ) {
  926.             // We didn't have reason to store the result of the last check.
  927.             return $approved;
  928.         }
  929.  
  930.         // Only do this if it's the correct comment
  931.         if ( ! self::matches_last_comment( $comment ) ) {
  932.             self::log( "comment_is_spam mismatched comment, returning unaltered $approved" );
  933.             return $approved;
  934.         }
  935.  
  936.         if ( 'trash' === $approved ) {
  937.             // If the last comment we checked has had its approval set to 'trash',
  938.             // then it failed the comment blacklist check. Let that blacklist override
  939.             // the spam check, since users have the (valid) expectation that when
  940.             // they fill out their blacklists, comments that match it will always
  941.             // end up in the trash.
  942.             return $approved;
  943.         }
  944.  
  945.         // bump the counter here instead of when the filter is added to reduce the possibility of overcounting
  946.         if ( $incr = apply_filters('akismet_spam_count_incr', 1) )
  947.             update_option( 'akismet_spam_count', get_option('akismet_spam_count') + $incr );
  948.  
  949.         return self::$last_comment_result;
  950.     }
  951.     
  952.     /**
  953.      * If Akismet is temporarily unreachable, we don't want to "spam" the blogger with
  954.      * moderation emails for comments that will be automatically cleared or spammed on
  955.      * the next retry.
  956.      *
  957.      * For comments that will be rechecked later, empty the list of email addresses that
  958.      * the moderation email would be sent to.
  959.      *
  960.      * @param array $emails An array of email addresses that the moderation email will be sent to.
  961.      * @param int $comment_id The ID of the relevant comment.
  962.      * @return array An array of email addresses that the moderation email will be sent to.
  963.      */
  964.     public static function disable_moderation_emails_if_unreachable( $emails, $comment_id ) {
  965.         if ( ! empty( self::$prevent_moderation_email_for_these_comments ) && ! empty( $emails ) ) {
  966.             $comment = get_comment( $comment_id );
  967.  
  968.             foreach ( self::$prevent_moderation_email_for_these_comments as $possible_match ) {
  969.                 if ( self::comments_match( $possible_match, $comment ) ) {
  970.                     update_comment_meta( $comment_id, 'akismet_delayed_moderation_email', true );
  971.                     return array();
  972.                 }
  973.             }
  974.         }
  975.  
  976.         return $emails;
  977.     }
  978.  
  979.     public static function _cmp_time( $a, $b ) {
  980.         return $a['time'] > $b['time'] ? -1 : 1;
  981.     }
  982.  
  983.     public static function _get_microtime() {
  984.         $mtime = explode( ' ', microtime() );
  985.         return $mtime[1] + $mtime[0];
  986.     }
  987.  
  988.     /**
  989.      * Make a POST request to the Akismet API.
  990.      *
  991.      * @param string $request The body of the request.
  992.      * @param string $path The path for the request.
  993.      * @param string $ip The specific IP address to hit.
  994.      * @return array A two-member array consisting of the headers and the response body, both empty in the case of a failure.
  995.      */
  996.     public static function http_post( $request, $path, $ip=null ) {
  997.  
  998.         $akismet_ua = sprintf( 'WordPress/%s | Akismet/%s', $GLOBALS['wp_version'], constant( 'AKISMET_VERSION' ) );
  999.         $akismet_ua = apply_filters( 'akismet_ua', $akismet_ua );
  1000.  
  1001.         $content_length = strlen( $request );
  1002.  
  1003.         $api_key   = self::get_api_key();
  1004.         $host      = self::API_HOST;
  1005.  
  1006.         if ( !empty( $api_key ) )
  1007.             $host = $api_key.'.'.$host;
  1008.  
  1009.         $http_host = $host;
  1010.         // use a specific IP if provided
  1011.         // needed by Akismet_Admin::check_server_connectivity()
  1012.         if ( $ip && long2ip( ip2long( $ip ) ) ) {
  1013.             $http_host = $ip;
  1014.         }
  1015.  
  1016.         $http_args = array(
  1017.             'body' => $request,
  1018.             'headers' => array(
  1019.                 'Content-Type' => 'application/x-www-form-urlencoded; charset=' . get_option( 'blog_charset' ),
  1020.                 'Host' => $host,
  1021.                 'User-Agent' => $akismet_ua,
  1022.             ),
  1023.             'httpversion' => '1.0',
  1024.             'timeout' => 15
  1025.         );
  1026.  
  1027.         $akismet_url = $http_akismet_url = "http://{$http_host}/1.1/{$path}";
  1028.  
  1029.         /**
  1030.          * Try SSL first; if that fails, try without it and don't try it again for a while.
  1031.          */
  1032.  
  1033.         $ssl = $ssl_failed = false;
  1034.  
  1035.         // Check if SSL requests were disabled fewer than X hours ago.
  1036.         $ssl_disabled = get_option( 'akismet_ssl_disabled' );
  1037.  
  1038.         if ( $ssl_disabled && $ssl_disabled < ( time() - 60 * 60 * 24 ) ) { // 24 hours
  1039.             $ssl_disabled = false;
  1040.             delete_option( 'akismet_ssl_disabled' );
  1041.         }
  1042.         else if ( $ssl_disabled ) {
  1043.             do_action( 'akismet_ssl_disabled' );
  1044.         }
  1045.  
  1046.         if ( ! $ssl_disabled && ( $ssl = wp_http_supports( array( 'ssl' ) ) ) ) {
  1047.             $akismet_url = set_url_scheme( $akismet_url, 'https' );
  1048.  
  1049.             do_action( 'akismet_https_request_pre' );
  1050.         }
  1051.  
  1052.         $response = wp_remote_post( $akismet_url, $http_args );
  1053.  
  1054.         Akismet::log( compact( 'akismet_url', 'http_args', 'response' ) );
  1055.  
  1056.         if ( $ssl && is_wp_error( $response ) ) {
  1057.             do_action( 'akismet_https_request_failure', $response );
  1058.  
  1059.             // Intermittent connection problems may cause the first HTTPS
  1060.             // request to fail and subsequent HTTP requests to succeed randomly.
  1061.             // Retry the HTTPS request once before disabling SSL for a time.
  1062.             $response = wp_remote_post( $akismet_url, $http_args );
  1063.             
  1064.             Akismet::log( compact( 'akismet_url', 'http_args', 'response' ) );
  1065.  
  1066.             if ( is_wp_error( $response ) ) {
  1067.                 $ssl_failed = true;
  1068.  
  1069.                 do_action( 'akismet_https_request_failure', $response );
  1070.  
  1071.                 do_action( 'akismet_http_request_pre' );
  1072.  
  1073.                 // Try the request again without SSL.
  1074.                 $response = wp_remote_post( $http_akismet_url, $http_args );
  1075.  
  1076.                 Akismet::log( compact( 'http_akismet_url', 'http_args', 'response' ) );
  1077.             }
  1078.         }
  1079.  
  1080.         if ( is_wp_error( $response ) ) {
  1081.             do_action( 'akismet_request_failure', $response );
  1082.  
  1083.             return array( '', '' );
  1084.         }
  1085.  
  1086.         if ( $ssl_failed ) {
  1087.             // The request failed when using SSL but succeeded without it. Disable SSL for future requests.
  1088.             update_option( 'akismet_ssl_disabled', time() );
  1089.             
  1090.             do_action( 'akismet_https_disabled' );
  1091.         }
  1092.         
  1093.         $simplified_response = array( $response['headers'], $response['body'] );
  1094.         
  1095.         self::update_alert( $simplified_response );
  1096.  
  1097.         return $simplified_response;
  1098.     }
  1099.  
  1100.     // given a response from an API call like check_key_status(), update the alert code options if an alert is present.
  1101.     public static function update_alert( $response ) {
  1102.         $code = $msg = null;
  1103.         if ( isset( $response[0]['x-akismet-alert-code'] ) ) {
  1104.             $code = $response[0]['x-akismet-alert-code'];
  1105.             $msg  = $response[0]['x-akismet-alert-msg'];
  1106.         }
  1107.  
  1108.         // only call update_option() if the value has changed
  1109.         if ( $code != get_option( 'akismet_alert_code' ) ) {
  1110.             if ( ! $code ) {
  1111.                 delete_option( 'akismet_alert_code' );
  1112.                 delete_option( 'akismet_alert_msg' );
  1113.             }
  1114.             else {
  1115.                 update_option( 'akismet_alert_code', $code );
  1116.                 update_option( 'akismet_alert_msg', $msg );
  1117.             }
  1118.         }
  1119.     }
  1120.  
  1121.     public static function load_form_js() {
  1122.         wp_register_script( 'akismet-form', plugin_dir_url( __FILE__ ) . '_inc/form.js', array(), AKISMET_VERSION, true );
  1123.         wp_enqueue_script( 'akismet-form' );
  1124.     }
  1125.     
  1126.     /**
  1127.      * Mark form.js as async. Because nothing depends on it, it can run at any time
  1128.      * after it's loaded, and the browser won't have to wait for it to load to continue
  1129.      * parsing the rest of the page.
  1130.      */
  1131.     public static function set_form_js_async( $tag, $handle, $src ) {
  1132.         if ( 'akismet-form' !== $handle ) {
  1133.             return $tag;
  1134.         }
  1135.         
  1136.         return preg_replace( '/^<script /i', '<script async="async" ', $tag );
  1137.     }
  1138.     
  1139.     public static function inject_ak_js( $fields ) {
  1140.         echo '<p style="display: none;">';
  1141.         echo '<input type="hidden" id="ak_js" name="ak_js" value="' . mt_rand( 0, 250 ) . '"/>';
  1142.         echo '</p>';
  1143.     }
  1144.  
  1145.     private static function bail_on_activation( $message, $deactivate = true ) {
  1146. ?>
  1147. <!doctype html>
  1148. <html>
  1149. <head>
  1150. <meta charset="<?php bloginfo( 'charset' ); ?>">
  1151. <style>
  1152. * {
  1153.     text-align: center;
  1154.     margin: 0;
  1155.     padding: 0;
  1156.     font-family: "Lucida Grande",Verdana,Arial,"Bitstream Vera Sans",sans-serif;
  1157. }
  1158. p {
  1159.     margin-top: 1em;
  1160.     font-size: 18px;
  1161. }
  1162. </style>
  1163. <body>
  1164. <p><?php echo esc_html( $message ); ?></p>
  1165. </body>
  1166. </html>
  1167. <?php
  1168.         if ( $deactivate ) {
  1169.             $plugins = get_option( 'active_plugins' );
  1170.             $akismet = plugin_basename( AKISMET__PLUGIN_DIR . 'akismet.php' );
  1171.             $update  = false;
  1172.             foreach ( $plugins as $i => $plugin ) {
  1173.                 if ( $plugin === $akismet ) {
  1174.                     $plugins[$i] = false;
  1175.                     $update = true;
  1176.                 }
  1177.             }
  1178.  
  1179.             if ( $update ) {
  1180.                 update_option( 'active_plugins', array_filter( $plugins ) );
  1181.             }
  1182.         }
  1183.         exit;
  1184.     }
  1185.  
  1186.     public static function view( $name, array $args = array() ) {
  1187.         $args = apply_filters( 'akismet_view_arguments', $args, $name );
  1188.         
  1189.         foreach ( $args AS $key => $val ) {
  1190.             $$key = $val;
  1191.         }
  1192.         
  1193.         load_plugin_textdomain( 'akismet' );
  1194.  
  1195.         $file = AKISMET__PLUGIN_DIR . 'views/'. $name . '.php';
  1196.  
  1197.         include( $file );
  1198.     }
  1199.  
  1200.     /**
  1201.      * Attached to activate_{ plugin_basename( __FILES__ ) } by register_activation_hook()
  1202.      * @static
  1203.      */
  1204.     public static function plugin_activation() {
  1205.         if ( version_compare( $GLOBALS['wp_version'], AKISMET__MINIMUM_WP_VERSION, '<' ) ) {
  1206.             load_plugin_textdomain( 'akismet' );
  1207.             
  1208.             $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/');
  1209.  
  1210.             Akismet::bail_on_activation( $message );
  1211.         }
  1212.     }
  1213.  
  1214.     /**
  1215.      * Removes all connection options
  1216.      * @static
  1217.      */
  1218.     public static function plugin_deactivation( ) {
  1219.         self::deactivate_key( self::get_api_key() );
  1220.         
  1221.         // Remove any scheduled cron jobs.
  1222.         $akismet_cron_events = array(
  1223.             'akismet_schedule_cron_recheck',
  1224.             'akismet_scheduled_delete',
  1225.         );
  1226.         
  1227.         foreach ( $akismet_cron_events as $akismet_cron_event ) {
  1228.             $timestamp = wp_next_scheduled( $akismet_cron_event );
  1229.             
  1230.             if ( $timestamp ) {
  1231.                 wp_unschedule_event( $timestamp, $akismet_cron_event );
  1232.             }
  1233.         }
  1234.     }
  1235.     
  1236.     /**
  1237.      * Essentially a copy of WP's build_query but one that doesn't expect pre-urlencoded values.
  1238.      *
  1239.      * @param array $args An array of key => value pairs
  1240.      * @return string A string ready for use as a URL query string.
  1241.      */
  1242.     public static function build_query( $args ) {
  1243.         return _http_build_query( $args, '', '&' );
  1244.     }
  1245.  
  1246.     /**
  1247.      * Log debugging info to the error log.
  1248.      *
  1249.      * Enabled when WP_DEBUG_LOG is enabled (and WP_DEBUG, since according to
  1250.      * core, "WP_DEBUG_DISPLAY and WP_DEBUG_LOG perform no function unless
  1251.      * WP_DEBUG is true), but can be disabled via the akismet_debug_log filter.
  1252.      *
  1253.      * @param mixed $akismet_debug The data to log.
  1254.      */
  1255.     public static function log( $akismet_debug ) {
  1256.         if ( apply_filters( 'akismet_debug_log', defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG && defined( 'AKISMET_DEBUG' ) && AKISMET_DEBUG ) ) {
  1257.             error_log( print_r( compact( 'akismet_debug' ), true ) );
  1258.         }
  1259.     }
  1260.  
  1261.     public static function pre_check_pingback( $method ) {
  1262.         if ( $method !== 'pingback.ping' )
  1263.             return;
  1264.  
  1265.         global $wp_xmlrpc_server;
  1266.     
  1267.         if ( !is_object( $wp_xmlrpc_server ) )
  1268.             return false;
  1269.     
  1270.         // Lame: tightly coupled with the IXR class.
  1271.         $args = $wp_xmlrpc_server->message->params;
  1272.     
  1273.         if ( !empty( $args[1] ) ) {
  1274.             $post_id = url_to_postid( $args[1] );
  1275.  
  1276.             // If this gets through the pre-check, make sure we properly identify the outbound request as a pingback verification
  1277.             Akismet::pingback_forwarded_for( null, $args[0] );
  1278.             add_filter( 'http_request_args', array( 'Akismet', 'pingback_forwarded_for' ), 10, 2 );
  1279.  
  1280.             $comment = array(
  1281.                 'comment_author_url' => $args[0],
  1282.                 'comment_post_ID' => $post_id,
  1283.                 'comment_author' => '',
  1284.                 'comment_author_email' => '',
  1285.                 'comment_content' => '',
  1286.                 'comment_type' => 'pingback',
  1287.                 'akismet_pre_check' => '1',
  1288.                 'comment_pingback_target' => $args[1],
  1289.             );
  1290.  
  1291.             $comment = Akismet::auto_check_comment( $comment );
  1292.  
  1293.             if ( isset( $comment['akismet_result'] ) && 'true' == $comment['akismet_result'] ) {
  1294.                 // Lame: tightly coupled with the IXR classes. Unfortunately the action provides no context and no way to return anything.
  1295.                 $wp_xmlrpc_server->error( new IXR_Error( 0, 'Invalid discovery target' ) );
  1296.             }
  1297.         }
  1298.     }
  1299.     
  1300.     public static function pingback_forwarded_for( $r, $url ) {
  1301.         static $urls = array();
  1302.     
  1303.         // Call this with $r == null to prime the callback to add headers on a specific URL
  1304.         if ( is_null( $r ) && !in_array( $url, $urls ) ) {
  1305.             $urls[] = $url;
  1306.         }
  1307.  
  1308.         // Add X-Pingback-Forwarded-For header, but only for requests to a specific URL (the apparent pingback source)
  1309.         if ( is_array( $r ) && is_array( $r['headers'] ) && !isset( $r['headers']['X-Pingback-Forwarded-For'] ) && in_array( $url, $urls ) ) {
  1310.             $remote_ip = preg_replace( '/[^a-fx0-9:.,]/i', '', $_SERVER['REMOTE_ADDR'] );
  1311.         
  1312.             // Note: this assumes REMOTE_ADDR is correct, and it may not be if a reverse proxy or CDN is in use
  1313.             $r['headers']['X-Pingback-Forwarded-For'] = $remote_ip;
  1314.  
  1315.             // Also identify the request as a pingback verification in the UA string so it appears in logs
  1316.             $r['user-agent'] .= '; verifying pingback from ' . $remote_ip;
  1317.         }
  1318.  
  1319.         return $r;
  1320.     }
  1321.     
  1322.     /**
  1323.      * Ensure that we are loading expected scalar values from akismet_as_submitted commentmeta.
  1324.      *
  1325.      * @param mixed $meta_value
  1326.      * @return mixed
  1327.      */
  1328.     private static function sanitize_comment_as_submitted( $meta_value ) {
  1329.         if ( empty( $meta_value ) ) {
  1330.             return $meta_value;
  1331.         }
  1332.  
  1333.         $meta_value = (array) $meta_value;
  1334.  
  1335.         foreach ( $meta_value as $key => $value ) {
  1336.             if ( ! isset( self::$comment_as_submitted_allowed_keys[$key] ) || ! is_scalar( $value ) ) {
  1337.                 unset( $meta_value[$key] );
  1338.             }
  1339.         }
  1340.  
  1341.         return $meta_value;
  1342.     }
  1343.     
  1344.     public static function predefined_api_key() {
  1345.         if ( defined( 'WPCOM_API_KEY' ) ) {
  1346.             return true;
  1347.         }
  1348.         
  1349.         return apply_filters( 'akismet_predefined_api_key', false );
  1350.     }
  1351. }
  1352.