X-Git-Url: https://scripts.mit.edu/gitweb/autoinstalls/wordpress.git/blobdiff_plain/53f4633144ed68c8b8fb5861f992b5489894a940..3d39054f012aefe514b3f5509e32f09fc4feda44:/wp-content/plugins/akismet/class.akismet.php diff --git a/wp-content/plugins/akismet/class.akismet.php b/wp-content/plugins/akismet/class.akismet.php index 7a637386..0d312e98 100644 --- a/wp-content/plugins/akismet/class.akismet.php +++ b/wp-content/plugins/akismet/class.akismet.php @@ -29,6 +29,14 @@ class Akismet { add_action( 'akismet_scheduled_delete', array( 'Akismet', 'delete_old_comments_meta' ) ); add_action( 'akismet_schedule_cron_recheck', array( 'Akismet', 'cron_recheck' ) ); + /** + * To disable the Akismet comment nonce, add a filter for the 'akismet_comment_nonce' tag + * and return any string value that is not 'true' or '' (empty string). + * + * Don't return boolean false, because that implies that the 'akismet_comment_nonce' option + * has not been set and that Akismet should just choose the default behavior for that + * situation. + */ $akismet_comment_nonce_option = apply_filters( 'akismet_comment_nonce', get_option( 'akismet_comment_nonce' ) ); if ( $akismet_comment_nonce_option == 'true' || $akismet_comment_nonce_option == '' ) @@ -45,12 +53,10 @@ class Akismet { // Run this early in the pingback call, before doing a remote fetch of the source uri add_action( 'xmlrpc_call', array( 'Akismet', 'pre_check_pingback' ) ); - - if ( '3.0.5' == $GLOBALS['wp_version'] ) { - remove_filter( 'comment_text', 'wp_kses_data' ); - if ( is_admin() ) - add_filter( 'comment_text', 'wp_kses_post' ); - } + + // Jetpack compatibility + add_filter( 'jetpack_options_whitelist', array( 'Akismet', 'add_to_jetpack_options_whitelist' ) ); + add_action( 'update_option_wordpress_api_key', array( 'Akismet', 'updated_option' ), 10, 2 ); } public static function get_api_key() { @@ -58,7 +64,7 @@ class Akismet { } public static function check_key_status( $key, $ip = null ) { - return self::http_post( Akismet::build_query( array( 'key' => $key, 'blog' => get_option('home') ) ), 'verify-key', $ip ); + return self::http_post( Akismet::build_query( array( 'key' => $key, 'blog' => get_option( 'home' ) ) ), 'verify-key', $ip ); } public static function verify_key( $key, $ip = null ) { @@ -71,7 +77,7 @@ class Akismet { } public static function deactivate_key( $key ) { - $response = self::http_post( Akismet::build_query( array( 'key' => $key, 'blog' => get_option('home') ) ), 'deactivate' ); + $response = self::http_post( Akismet::build_query( array( 'key' => $key, 'blog' => get_option( 'home' ) ) ), 'deactivate' ); if ( $response[1] != 'deactivated' ) return 'failed'; @@ -79,6 +85,37 @@ class Akismet { return $response[1]; } + /** + * Add the akismet option to the Jetpack options management whitelist. + * + * @param array $options The list of whitelisted option names. + * @return array The updated whitelist + */ + public static function add_to_jetpack_options_whitelist( $options ) { + $options[] = 'wordpress_api_key'; + return $options; + } + + /** + * When the akismet option is updated, run the registration call. + * + * This should only be run when the option is updated from the Jetpack/WP.com + * API call, and only if the new key is different than the old key. + * + * @param mixed $old_value The old option value. + * @param mixed $value The new option value. + */ + public static function updated_option( $old_value, $value ) { + // Not an API call + if ( ! class_exists( 'WPCOM_JSON_API_Update_Option_Endpoint' ) ) { + return; + } + // Only run the registration if the old key is different. + if ( $old_value !== $value ) { + self::verify_key( $value ); + } + } + public static function auto_check_comment( $commentdata ) { self::$last_comment_result = null; @@ -87,7 +124,7 @@ class Akismet { $comment['user_ip'] = self::get_ip_address(); $comment['user_agent'] = self::get_user_agent(); $comment['referrer'] = self::get_referer(); - $comment['blog'] = get_option('home'); + $comment['blog'] = get_option( 'home' ); $comment['blog_lang'] = get_locale(); $comment['blog_charset'] = get_option('blog_charset'); $comment['permalink'] = get_permalink( $comment['comment_post_ID'] ); @@ -95,6 +132,7 @@ class Akismet { if ( !empty( $comment['user_ID'] ) ) $comment['user_role'] = Akismet::get_user_roles( $comment['user_ID'] ); + /** See filter documentation in init_hooks(). */ $akismet_nonce_option = apply_filters( 'akismet_comment_nonce', get_option( 'akismet_comment_nonce' ) ); $comment['akismet_comment_nonce'] = 'inactive'; if ( $akismet_nonce_option == 'true' || $akismet_nonce_option == '' ) { @@ -116,13 +154,19 @@ class Akismet { $comment["POST_{$key}"] = $value; } - $ignore = array( 'HTTP_COOKIE', 'HTTP_COOKIE2', 'PHP_AUTH_PW' ); - foreach ( $_SERVER as $key => $value ) { - if ( !in_array( $key, $ignore ) && is_string($value) ) - $comment["$key"] = $value; - else - $comment["$key"] = ''; + if ( ! is_string( $value ) ) { + continue; + } + + if ( preg_match( "/^HTTP_COOKIE/", $key ) ) { + continue; + } + + // Send any potentially useful $_SERVER vars, but avoid sending junk we don't need. + if ( preg_match( "/^(HTTP_|REMOTE_ADDR|REQUEST_URI|DOCUMENT_URI)/", $key ) ) { + $comment[ "$key" ] = $value; + } } $post = get_post( $comment['comment_post_ID'] ); @@ -245,7 +289,9 @@ class Akismet { elseif ( self::$last_comment['akismet_result'] == 'false' ) { update_comment_meta( $comment->comment_ID, 'akismet_result', 'false' ); self::update_comment_history( $comment->comment_ID, '', 'check-ham' ); - if ( $comment->comment_approved == 'spam' ) { + // Status could be spam or trash, depending on the WP version and whether this change applies: + // https://core.trac.wordpress.org/changeset/34726 + if ( $comment->comment_approved == 'spam' || $comment->comment_approved == 'trash' ) { 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) ) self::update_comment_history( $comment->comment_ID, '', 'wp-blacklisted' ); else @@ -301,10 +347,11 @@ class Akismet { do_action( 'delete_comment', $comment_id ); } - $comma_comment_ids = implode( ', ', array_map('intval', $comment_ids) ); + // Prepared as strings since comment_id is an unsigned BIGINT, and using %d will constrain the value to the maximum signed BIGINT. + $format_string = implode( ", ", array_fill( 0, count( $comment_ids ), '%s' ) ); - $wpdb->query("DELETE FROM {$wpdb->comments} WHERE comment_id IN ( $comma_comment_ids )"); - $wpdb->query("DELETE FROM {$wpdb->commentmeta} WHERE comment_id IN ( $comma_comment_ids )"); + $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->comments} WHERE comment_id IN ( " . $format_string . " )", $comment_ids ) ); + $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->commentmeta} WHERE comment_id IN ( " . $format_string . " )", $comment_ids ) ); clean_comment_cache( $comment_ids ); } @@ -403,27 +450,72 @@ class Akismet { global $wpdb; $c = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->comments} WHERE comment_ID = %d", $id ), ARRAY_A ); - if ( !$c ) - return; + + if ( ! $c ) { + return new WP_Error( 'invalid-comment-id', __( 'Comment not found.', 'akismet' ) ); + } $c['user_ip'] = $c['comment_author_IP']; $c['user_agent'] = $c['comment_agent']; $c['referrer'] = ''; - $c['blog'] = get_option('home'); + $c['blog'] = get_option( 'home' ); $c['blog_lang'] = get_locale(); $c['blog_charset'] = get_option('blog_charset'); $c['permalink'] = get_permalink($c['comment_post_ID']); $c['recheck_reason'] = $recheck_reason; + $c['user_role'] = ''; + if ( isset( $c['user_ID'] ) ) + $c['user_role'] = Akismet::get_user_roles($c['user_ID']); + if ( self::is_test_mode() ) $c['is_test'] = 'true'; $response = self::http_post( Akismet::build_query( $c ), 'comment-check' ); - return ( is_array( $response ) && ! empty( $response[1] ) ) ? $response[1] : false; + if ( ! empty( $response[1] ) ) { + return $response[1]; + } + + return false; } - + public static function recheck_comment( $id, $recheck_reason = 'recheck_queue' ) { + add_comment_meta( $id, 'akismet_rechecking', true ); + + $api_response = self::check_db_comment( $id, $recheck_reason ); + + delete_comment_meta( $id, 'akismet_rechecking' ); + + if ( is_wp_error( $api_response ) ) { + // Invalid comment ID. + } + else if ( 'true' === $api_response ) { + wp_set_comment_status( $id, 'spam' ); + update_comment_meta( $id, 'akismet_result', 'true' ); + delete_comment_meta( $id, 'akismet_error' ); + delete_comment_meta( $id, 'akismet_delayed_moderation_email' ); + Akismet::update_comment_history( $id, '', 'recheck-spam' ); + } + elseif ( 'false' === $api_response ) { + update_comment_meta( $id, 'akismet_result', 'false' ); + delete_comment_meta( $id, 'akismet_error' ); + delete_comment_meta( $id, 'akismet_delayed_moderation_email' ); + Akismet::update_comment_history( $id, '', 'recheck-ham' ); + } + else { + // abnormal result: error + update_comment_meta( $id, 'akismet_result', 'error' ); + Akismet::update_comment_history( + $id, + '', + 'recheck-error', + array( 'response' => substr( $api_response, 0, 50 ) ) + ); + } + + return $api_response; + } public static function transition_comment_status( $new_status, $old_status, $comment ) { @@ -459,8 +551,9 @@ class Akismet { ( isset( $_POST['spam'] ) && (int) $_POST['spam'] == 1 ) || ( isset( $_POST['unspam'] ) && (int) $_POST['unspam'] == 1 ) || ( isset( $_POST['comment_status'] ) && in_array( $_POST['comment_status'], array( 'spam', 'unspam' ) ) ) || - ( isset( $_GET['action'] ) && in_array( $_GET['action'], array( 'spam', 'unspam' ) ) ) || - ( isset( $_POST['action'] ) && in_array( $_POST['action'], array( 'editedcomment' ) ) ) + ( isset( $_GET['action'] ) && in_array( $_GET['action'], array( 'spam', 'unspam', 'spamcomment', 'unspamcomment', ) ) ) || + ( isset( $_POST['action'] ) && in_array( $_POST['action'], array( 'editedcomment' ) ) ) || + ( isset( $_GET['for'] ) && ( 'jetpack' == $_GET['for'] ) ) // Moderation via WP.com notifications/WP app/etc. ) { if ( $new_status == 'spam' && ( $old_status == 'approved' || $old_status == 'unapproved' || !$old_status ) ) { return self::submit_spam_comment( $comment->comment_ID ); @@ -491,7 +584,7 @@ class Akismet { if ( $as_submitted && is_array( $as_submitted ) && isset( $as_submitted['comment_content'] ) ) $comment = (object) array_merge( (array)$comment, $as_submitted ); - $comment->blog = get_bloginfo('url'); + $comment->blog = get_option( 'home' ); $comment->blog_lang = get_locale(); $comment->blog_charset = get_option('blog_charset'); $comment->permalink = get_permalink($comment->comment_post_ID); @@ -537,7 +630,7 @@ class Akismet { if ( $as_submitted && is_array($as_submitted) && isset($as_submitted['comment_content']) ) $comment = (object) array_merge( (array)$comment, $as_submitted ); - $comment->blog = get_bloginfo('url'); + $comment->blog = get_option( 'home' ); $comment->blog_lang = get_locale(); $comment->blog_charset = get_option('blog_charset'); $comment->permalink = get_permalink( $comment->comment_post_ID ); @@ -704,20 +797,35 @@ class Akismet { $comment1 = (array) $comment1; $comment2 = (array) $comment2; - return ( + $comments_match = ( isset( $comment1['comment_post_ID'], $comment2['comment_post_ID'] ) && intval( $comment1['comment_post_ID'] ) == intval( $comment2['comment_post_ID'] ) && ( - $comment1['comment_author'] == $comment2['comment_author'] - || stripslashes( $comment1['comment_author'] ) == $comment2['comment_author'] - || $comment1['comment_author'] == stripslashes( $comment2['comment_author'] ) + // The comment author length max is 255 characters, limited by the TINYTEXT column type. + // If the comment author includes multibyte characters right around the 255-byte mark, they + // may be stripped when the author is saved in the DB, so a 300+ char author may turn into + // a 253-char author when it's saved, not 255 exactly. The longest possible character is + // theoretically 6 bytes, so we'll only look at the first 248 bytes to be safe. + substr( $comment1['comment_author'], 0, 248 ) == substr( $comment2['comment_author'], 0, 248 ) + || substr( stripslashes( $comment1['comment_author'] ), 0, 248 ) == substr( $comment2['comment_author'], 0, 248 ) + || substr( $comment1['comment_author'], 0, 248 ) == substr( stripslashes( $comment2['comment_author'] ), 0, 248 ) + // Certain long comment author names will be truncated to nothing, depending on their encoding. + || ( ! $comment1['comment_author'] && strlen( $comment2['comment_author'] ) > 248 ) + || ( ! $comment2['comment_author'] && strlen( $comment1['comment_author'] ) > 248 ) ) && ( - $comment1['comment_author_email'] == $comment2['comment_author_email'] - || stripslashes( $comment1['comment_author_email'] ) == $comment2['comment_author_email'] - || $comment1['comment_author_email'] == stripslashes( $comment2['comment_author_email'] ) + // The email max length is 100 characters, limited by the VARCHAR(100) column type. + // Same argument as above for only looking at the first 93 characters. + substr( $comment1['comment_author_email'], 0, 93 ) == substr( $comment2['comment_author_email'], 0, 93 ) + || substr( stripslashes( $comment1['comment_author_email'] ), 0, 93 ) == substr( $comment2['comment_author_email'], 0, 93 ) + || substr( $comment1['comment_author_email'], 0, 93 ) == substr( stripslashes( $comment2['comment_author_email'] ), 0, 93 ) + // Very long emails can be truncated and then stripped if the [0:100] substring isn't a valid address. + || ( ! $comment1['comment_author_email'] && strlen( $comment2['comment_author_email'] ) > 100 ) + || ( ! $comment2['comment_author_email'] && strlen( $comment1['comment_author_email'] ) > 100 ) ) ); + + return $comments_match; } // Does the supplied comment match the details of the one most recently stored in self::$last_comment? @@ -763,8 +871,13 @@ class Akismet { // filter handler used to return a spam result to pre_comment_approved public static function last_comment_status( $approved, $comment ) { + if ( is_null( self::$last_comment_result ) ) { + // We didn't have reason to store the result of the last check. + return $approved; + } + // Only do this if it's the correct comment - if ( is_null(self::$last_comment_result) || ! self::matches_last_comment( $comment ) ) { + if ( ! self::matches_last_comment( $comment ) ) { self::log( "comment_is_spam mismatched comment, returning unaltered $approved" ); return $approved; } @@ -948,7 +1061,7 @@ class Akismet { public static function load_form_js() { // WP < 3.3 can't enqueue a script this late in the game and still have it appear in the footer. // Once we drop support for everything pre-3.3, this can change back to a single enqueue call. - wp_register_script( 'akismet-form', AKISMET__PLUGIN_URL . '_inc/form.js', array(), AKISMET_VERSION, true ); + wp_register_script( 'akismet-form', plugin_dir_url( __FILE__ ) . '_inc/form.js', array(), AKISMET_VERSION, true ); add_action( 'wp_footer', array( 'Akismet', 'print_form_js' ) ); add_action( 'admin_footer', array( 'Akismet', 'print_form_js' ) ); } @@ -1026,7 +1139,7 @@ p { if ( version_compare( $GLOBALS['wp_version'], AKISMET__MINIMUM_WP_VERSION, '<' ) ) { load_plugin_textdomain( 'akismet' ); - $message = ''.sprintf(esc_html__( 'Akismet %s requires WordPress %s or higher.' , 'akismet'), AKISMET_VERSION, AKISMET__MINIMUM_WP_VERSION ).' '.sprintf(__('Please upgrade WordPress to a current version, or downgrade to version 2.4 of the Akismet plugin.', 'akismet'), 'https://codex.wordpress.org/Upgrading_WordPress', 'http://wordpress.org/extend/plugins/akismet/download/'); + $message = ''.sprintf(esc_html__( 'Akismet %s requires WordPress %s or higher.' , 'akismet'), AKISMET_VERSION, AKISMET__MINIMUM_WP_VERSION ).' '.sprintf(__('Please upgrade WordPress to a current version, or downgrade to version 2.4 of the Akismet plugin.', 'akismet'), 'https://codex.wordpress.org/Upgrading_WordPress', 'https://wordpress.org/extend/plugins/akismet/download/'); Akismet::bail_on_activation( $message ); } @@ -1053,12 +1166,14 @@ p { /** * Log debugging info to the error log. * - * Enabled when WP_DEBUG_LOG is enabled, but can be disabled via the akismet_debug_log filter. + * Enabled when WP_DEBUG_LOG is enabled (and WP_DEBUG, since according to + * core, "WP_DEBUG_DISPLAY and WP_DEBUG_LOG perform no function unless + * WP_DEBUG is true), but can be disabled via the akismet_debug_log filter. * * @param mixed $akismet_debug The data to log. */ public static function log( $akismet_debug ) { - if ( apply_filters( 'akismet_debug_log', defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) ) { + if ( apply_filters( 'akismet_debug_log', defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) ) { error_log( print_r( compact( 'akismet_debug' ), true ) ); } } @@ -1145,4 +1260,4 @@ p { return $meta_value; } -} \ No newline at end of file +}