diff options
Diffstat (limited to 'plugins/akismet/class.akismet.php')
-rw-r--r-- | plugins/akismet/class.akismet.php | 477 |
1 files changed, 371 insertions, 106 deletions
diff --git a/plugins/akismet/class.akismet.php b/plugins/akismet/class.akismet.php index 01753014..1681d0e1 100644 --- a/plugins/akismet/class.akismet.php +++ b/plugins/akismet/class.akismet.php @@ -5,12 +5,19 @@ class Akismet { const API_PORT = 80; const MAX_DELAY_BEFORE_MODERATION_EMAIL = 86400; // One day in seconds + public static $limit_notices = array( + 10501 => 'FIRST_MONTH_OVER_LIMIT', + 10502 => 'SECOND_MONTH_OVER_LIMIT', + 10504 => 'THIRD_MONTH_APPROACHING_LIMIT', + 10508 => 'THIRD_MONTH_OVER_LIMIT', + 10516 => 'FOUR_PLUS_MONTHS_OVER_LIMIT', + ); + private static $last_comment = ''; private static $initiated = false; private static $prevent_moderation_email_for_these_comments = array(); private static $last_comment_result = null; 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' => '' ); - private static $is_rest_api_call = false; public static function init() { if ( ! self::$initiated ) { @@ -34,11 +41,7 @@ class Akismet { add_action( 'akismet_schedule_cron_recheck', array( 'Akismet', 'cron_recheck' ) ); add_action( 'comment_form', array( 'Akismet', 'add_comment_nonce' ), 1 ); - - add_action( 'admin_head-edit-comments.php', array( 'Akismet', 'load_form_js' ) ); - add_action( 'comment_form', array( 'Akismet', 'load_form_js' ) ); - add_action( 'comment_form', array( 'Akismet', 'inject_ak_js' ) ); - add_filter( 'script_loader_tag', array( 'Akismet', 'set_form_js_async' ), 10, 3 ); + add_action( 'comment_form', array( 'Akismet', 'output_custom_form_fields' ) ); add_filter( 'comment_moderation_recipients', array( 'Akismet', 'disable_moderation_emails_if_unreachable' ), 1000, 2 ); add_filter( 'pre_comment_approved', array( 'Akismet', 'last_comment_status' ), 10, 2 ); @@ -47,9 +50,20 @@ 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' ) ); - + // Jetpack compatibility add_filter( 'jetpack_options_whitelist', array( 'Akismet', 'add_to_jetpack_options_whitelist' ) ); + add_filter( 'jetpack_contact_form_html', array( 'Akismet', 'inject_custom_form_fields' ) ); + add_filter( 'jetpack_contact_form_akismet_values', array( 'Akismet', 'prepare_custom_form_values' ) ); + + // Gravity Forms + add_filter( 'gform_get_form_filter', array( 'Akismet', 'inject_custom_form_fields' ) ); + add_filter( 'gform_akismet_fields', array( 'Akismet', 'prepare_custom_form_values' ) ); + + // Contact Form 7 + add_filter( 'wpcf7_form_elements', array( 'Akismet', 'append_custom_form_fields' ) ); + add_filter( 'wpcf7_akismet_parameters', array( 'Akismet', 'prepare_custom_form_values' ) ); + add_action( 'update_option_wordpress_api_key', array( 'Akismet', 'updated_option' ), 10, 2 ); add_action( 'add_option_wordpress_api_key', array( 'Akismet', 'added_option' ), 10, 2 ); @@ -131,12 +145,23 @@ class Akismet { } public static function rest_auto_check_comment( $commentdata ) { - self::$is_rest_api_call = true; - - return self::auto_check_comment( $commentdata ); + return self::auto_check_comment( $commentdata, 'rest_api' ); } - public static function auto_check_comment( $commentdata ) { + /** + * Check a comment for spam. + * + * @param array $commentdata + * @param string $context What kind of request triggered this comment check? Possible values are 'default', 'rest_api', and 'xml-rpc'. + * @return array|WP_Error Either the $commentdata array with additional entries related to its spam status + * or a WP_Error, if it's a REST API request and the comment should be discarded. + */ + public static function auto_check_comment( $commentdata, $context = 'default' ) { + // If no key is configured, then there's no point in doing any of this. + if ( ! self::get_api_key() ) { + return $commentdata; + } + self::$last_comment_result = null; $comment = $commentdata; @@ -202,7 +227,15 @@ class Akismet { do_action( 'akismet_comment_check_response', $response ); $commentdata['comment_as_submitted'] = array_intersect_key( $comment, self::$comment_as_submitted_allowed_keys ); - $commentdata['akismet_result'] = $response[1]; + + // Also include any form fields we inject into the comment form, like ak_js + foreach ( $_POST as $key => $value ) { + if ( is_string( $value ) && strpos( $key, 'ak_' ) === 0 ) { + $commentdata['comment_as_submitted'][ 'POST_' . $key ] = $value; + } + } + + $commentdata['akismet_result'] = $response[1]; if ( isset( $response[0]['x-akismet-pro-tip'] ) ) $commentdata['akismet_pro_tip'] = $response[0]['x-akismet-pro-tip']; @@ -227,17 +260,19 @@ class Akismet { update_option( 'akismet_spam_count', get_option( 'akismet_spam_count' ) + $incr ); } - if ( self::$is_rest_api_call ) { + if ( 'rest_api' === $context ) { return new WP_Error( 'akismet_rest_comment_discarded', __( 'Comment discarded.', 'akismet' ) ); - } - else { + } else if ( 'xml-rpc' === $context ) { + // If this is a pingback that we're pre-checking, the discard behavior is the same as the normal spam response behavior. + return $commentdata; + } else { // Redirect back to the previous page, or failing that, the post permalink, or failing that, the homepage of the blog. $redirect_to = isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : ( $post ? get_permalink( $post ) : home_url() ); wp_safe_redirect( esc_url_raw( $redirect_to ) ); die(); } } - else if ( self::$is_rest_api_call ) { + else if ( 'rest_api' === $context ) { // The way the REST API structures its calls, we can set the comment_approved value right away. $commentdata['comment_approved'] = 'spam'; } @@ -296,48 +331,56 @@ class Akismet { // as was checked by auto_check_comment if ( is_object( $comment ) && !empty( self::$last_comment ) && is_array( self::$last_comment ) ) { if ( self::matches_last_comment( $comment ) ) { - - load_plugin_textdomain( 'akismet' ); - - // normal result: true or false - if ( self::$last_comment['akismet_result'] == 'true' ) { - update_comment_meta( $comment->comment_ID, 'akismet_result', 'true' ); - self::update_comment_history( $comment->comment_ID, '', 'check-spam' ); - if ( $comment->comment_approved != 'spam' ) - self::update_comment_history( - $comment->comment_ID, - '', - 'status-changed-'.$comment->comment_approved - ); - } - 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' ); - // 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 - self::update_comment_history( $comment->comment_ID, '', 'status-changed-'.$comment->comment_approved ); - } - } // abnormal result: error - else { - update_comment_meta( $comment->comment_ID, 'akismet_error', time() ); + load_plugin_textdomain( 'akismet' ); + + // normal result: true or false + if ( self::$last_comment['akismet_result'] == 'true' ) { + update_comment_meta( $comment->comment_ID, 'akismet_result', 'true' ); + self::update_comment_history( $comment->comment_ID, '', 'check-spam' ); + if ( $comment->comment_approved != 'spam' ) { self::update_comment_history( $comment->comment_ID, '', - 'check-error', - array( 'response' => substr( self::$last_comment['akismet_result'], 0, 50 ) ) + 'status-changed-' . $comment->comment_approved ); } + } 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' ); + // 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 ( function_exists( 'wp_check_comment_disallowed_list' ) ) { + 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 ) ) { + self::update_comment_history( $comment->comment_ID, '', 'wp-disallowed' ); + } else { + self::update_comment_history( $comment->comment_ID, '', 'status-changed-' . $comment->comment_approved ); + } + } 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 ) ) { + self::update_comment_history( $comment->comment_ID, '', 'wp-blacklisted' ); + } else { + self::update_comment_history( $comment->comment_ID, '', 'status-changed-' . $comment->comment_approved ); + } + } + } else { + // abnormal result: error + update_comment_meta( $comment->comment_ID, 'akismet_error', time() ); + self::update_comment_history( + $comment->comment_ID, + '', + 'check-error', + array( 'response' => substr( self::$last_comment['akismet_result'], 0, 50 ) ) + ); + } - // record the complete original data as submitted for checking - if ( isset( self::$last_comment['comment_as_submitted'] ) ) - update_comment_meta( $comment->comment_ID, 'akismet_as_submitted', self::$last_comment['comment_as_submitted'] ); + // record the complete original data as submitted for checking + if ( isset( self::$last_comment['comment_as_submitted'] ) ) { + update_comment_meta( $comment->comment_ID, 'akismet_as_submitted', self::$last_comment['comment_as_submitted'] ); + } - if ( isset( self::$last_comment['akismet_pro_tip'] ) ) - update_comment_meta( $comment->comment_ID, 'akismet_pro_tip', self::$last_comment['akismet_pro_tip'] ); + if ( isset( self::$last_comment['akismet_pro_tip'] ) ) { + update_comment_meta( $comment->comment_ID, 'akismet_pro_tip', self::$last_comment['akismet_pro_tip'] ); + } } } } @@ -380,6 +423,10 @@ class Akismet { clean_comment_cache( $comment_ids ); do_action( 'akismet_delete_comment_batch', count( $comment_ids ) ); + + foreach ( $comment_ids as $comment_id ) { + do_action( 'deleted_comment', $comment_id ); + } } if ( apply_filters( 'akismet_optimize_table', ( mt_rand(1, 5000) == 11), $wpdb->comments ) ) // lucky number @@ -469,6 +516,44 @@ class Akismet { // get the full comment history for a given comment, as an array in reverse chronological order public static function get_comment_history( $comment_id ) { $history = get_comment_meta( $comment_id, 'akismet_history', false ); + if ( empty( $history ) || empty( $history[ 0 ] ) ) { + return false; + } + + /* + // To see all variants when testing. + $history[] = array( 'time' => 445856401, 'message' => 'Old versions of Akismet stored the message as a literal string in the commentmeta.', 'event' => null ); + $history[] = array( 'time' => 445856402, 'event' => 'recheck-spam' ); + $history[] = array( 'time' => 445856403, 'event' => 'check-spam' ); + $history[] = array( 'time' => 445856404, 'event' => 'recheck-ham' ); + $history[] = array( 'time' => 445856405, 'event' => 'check-ham' ); + $history[] = array( 'time' => 445856406, 'event' => 'wp-blacklisted' ); + $history[] = array( 'time' => 445856406, 'event' => 'wp-disallowed' ); + $history[] = array( 'time' => 445856407, 'event' => 'report-spam' ); + $history[] = array( 'time' => 445856408, 'event' => 'report-spam', 'user' => 'sam' ); + $history[] = array( 'message' => 'sam reported this comment as spam (hardcoded message).', 'time' => 445856400, 'event' => 'report-spam', 'user' => 'sam' ); + $history[] = array( 'time' => 445856409, 'event' => 'report-ham', 'user' => 'sam' ); + $history[] = array( 'message' => 'sam reported this comment as ham (hardcoded message).', 'time' => 445856400, 'event' => 'report-ham', 'user' => 'sam' ); // + $history[] = array( 'time' => 445856410, 'event' => 'cron-retry-spam' ); + $history[] = array( 'time' => 445856411, 'event' => 'cron-retry-ham' ); + $history[] = array( 'time' => 445856412, 'event' => 'check-error' ); // + $history[] = array( 'time' => 445856413, 'event' => 'check-error', 'meta' => array( 'response' => 'The server was taking a nap.' ) ); + $history[] = array( 'time' => 445856414, 'event' => 'recheck-error' ); // Should not generate a message. + $history[] = array( 'time' => 445856415, 'event' => 'recheck-error', 'meta' => array( 'response' => 'The server was taking a nap.' ) ); + $history[] = array( 'time' => 445856416, 'event' => 'status-changedtrash' ); + $history[] = array( 'time' => 445856417, 'event' => 'status-changedspam' ); + $history[] = array( 'time' => 445856418, 'event' => 'status-changedhold' ); + $history[] = array( 'time' => 445856419, 'event' => 'status-changedapprove' ); + $history[] = array( 'time' => 445856420, 'event' => 'status-changed-trash' ); + $history[] = array( 'time' => 445856421, 'event' => 'status-changed-spam' ); + $history[] = array( 'time' => 445856422, 'event' => 'status-changed-hold' ); + $history[] = array( 'time' => 445856423, 'event' => 'status-changed-approve' ); + $history[] = array( 'time' => 445856424, 'event' => 'status-trash', 'user' => 'sam' ); + $history[] = array( 'time' => 445856425, 'event' => 'status-spam', 'user' => 'sam' ); + $history[] = array( 'time' => 445856426, 'event' => 'status-hold', 'user' => 'sam' ); + $history[] = array( 'time' => 445856427, 'event' => 'status-approve', 'user' => 'sam' ); + */ + usort( $history, array( 'Akismet', '_cmp_time' ) ); return $history; } @@ -506,6 +591,10 @@ class Akismet { public static function check_db_comment( $id, $recheck_reason = 'recheck_queue' ) { global $wpdb; + if ( ! self::get_api_key() ) { + return new WP_Error( 'akismet-not-configured', __( 'Akismet is not configured. Please enter an API key.', 'akismet' ) ); + } + $c = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->comments} WHERE comment_ID = %d", $id ), ARRAY_A ); if ( ! $c ) { @@ -653,6 +742,13 @@ class Akismet { if ( 'spam' != $comment->comment_approved ) return; + self::update_comment_history( $comment_id, '', 'report-spam' ); + + // If the user hasn't configured Akismet, there's nothing else to do at this point. + if ( ! self::get_api_key() ) { + return; + } + // use the original version stored in comment_meta if available $as_submitted = self::sanitize_comment_as_submitted( get_comment_meta( $comment_id, 'akismet_as_submitted', true ) ); @@ -685,9 +781,10 @@ class Akismet { } $response = Akismet::http_post( Akismet::build_query( $comment ), 'submit-spam' ); + + update_comment_meta( $comment_id, 'akismet_user_result', 'true' ); + if ( $comment->reporter ) { - self::update_comment_history( $comment_id, '', 'report-spam' ); - update_comment_meta( $comment_id, 'akismet_user_result', 'true' ); update_comment_meta( $comment_id, 'akismet_user', $comment->reporter ); } @@ -703,6 +800,13 @@ class Akismet { if ( !$comment ) // it was deleted return; + self::update_comment_history( $comment_id, '', 'report-ham' ); + + // If the user hasn't configured Akismet, there's nothing else to do at this point. + if ( ! self::get_api_key() ) { + return; + } + // use the original version stored in comment_meta if available $as_submitted = self::sanitize_comment_as_submitted( get_comment_meta( $comment_id, 'akismet_as_submitted', true ) ); @@ -735,9 +839,10 @@ class Akismet { } $response = self::http_post( Akismet::build_query( $comment ), 'submit-ham' ); + + update_comment_meta( $comment_id, 'akismet_user_result', 'false' ); + if ( $comment->reporter ) { - self::update_comment_history( $comment_id, '', 'report-ham' ); - update_comment_meta( $comment_id, 'akismet_user_result', 'false' ); update_comment_meta( $comment_id, 'akismet_user', $comment->reporter ); } @@ -860,6 +965,11 @@ class Akismet { * has not been set and that Akismet should just choose the default behavior for that * situation. */ + + if ( ! self::get_api_key() ) { + return; + } + $akismet_comment_nonce_option = apply_filters( 'akismet_comment_nonce', get_option( 'akismet_comment_nonce' ) ); if ( $akismet_comment_nonce_option == 'true' || $akismet_comment_nonce_option == '' ) { @@ -879,7 +989,7 @@ class Akismet { if ( is_user_logged_in() ) return false; - return ( get_option( 'akismet_strictness' ) === '1' ); + return ( get_option( 'akismet_strictness' ) === '1' ); } public static function get_ip_address() { @@ -1029,10 +1139,12 @@ class Akismet { if ( ! empty( self::$prevent_moderation_email_for_these_comments ) && ! empty( $emails ) ) { $comment = get_comment( $comment_id ); - foreach ( self::$prevent_moderation_email_for_these_comments as $possible_match ) { - if ( self::comments_match( $possible_match, $comment ) ) { - update_comment_meta( $comment_id, 'akismet_delayed_moderation_email', true ); - return array(); + if ( $comment ) { + foreach ( self::$prevent_moderation_email_for_these_comments as $possible_match ) { + if ( self::comments_match( $possible_match, $comment ) ) { + update_comment_meta( $comment_id, 'akismet_delayed_moderation_email', true ); + return array(); + } } } } @@ -1163,51 +1275,106 @@ class Akismet { // given a response from an API call like check_key_status(), update the alert code options if an alert is present. public static function update_alert( $response ) { - $code = $msg = null; - if ( isset( $response[0]['x-akismet-alert-code'] ) ) { - $code = $response[0]['x-akismet-alert-code']; - $msg = $response[0]['x-akismet-alert-msg']; - } + $alert_option_prefix = 'akismet_alert_'; + $alert_header_prefix = 'x-akismet-alert-'; + $alert_header_names = array( + 'code', + 'msg', + 'api-calls', + 'usage-limit', + 'upgrade-plan', + 'upgrade-url', + 'upgrade-type', + ); - // only call update_option() if the value has changed - if ( $code != get_option( 'akismet_alert_code' ) ) { - if ( ! $code ) { - delete_option( 'akismet_alert_code' ); - delete_option( 'akismet_alert_msg' ); + foreach ( $alert_header_names as $alert_header_name ) { + $value = null; + if ( isset( $response[0][ $alert_header_prefix . $alert_header_name ] ) ) { + $value = $response[0][ $alert_header_prefix . $alert_header_name ]; } - else { - update_option( 'akismet_alert_code', $code ); - update_option( 'akismet_alert_msg', $msg ); + + $option_name = $alert_option_prefix . str_replace( '-', '_', $alert_header_name ); + if ( $value != get_option( $option_name ) ) { + if ( ! $value ) { + delete_option( $option_name ); + } else { + update_option( $option_name, $value ); + } } } } public static function load_form_js() { - if ( function_exists( 'is_amp_endpoint' ) && is_amp_endpoint() ) { - return; + /* deprecated */ + } + + public static function set_form_js_async( $tag, $handle, $src ) { + /* deprecated */ + return $tag; + } + + public static function get_akismet_form_fields() { + $fields = ''; + + $prefix = 'ak_'; + + // Contact Form 7 uses _wpcf7 as a prefix to know which fields to exclude from comment_content. + if ( 'wpcf7_form_elements' === current_filter() ) { + $prefix = '_wpcf7_ak_'; + } + + $fields .= '<p style="display: none !important;">'; + $fields .= '<label>Δ<textarea name="' . $prefix . 'hp_textarea" cols="45" rows="8" maxlength="100"></textarea></label>'; + + if ( ! function_exists( 'amp_is_request' ) || ! amp_is_request() ) { + $fields .= '<input type="hidden" id="ak_js" name="' . $prefix . 'js" value="' . mt_rand( 0, 250 ) . '"/>'; + $fields .= '<script>document.getElementById( "ak_js" ).setAttribute( "value", ( new Date() ).getTime() );</script>'; } - wp_register_script( 'akismet-form', plugin_dir_url( __FILE__ ) . '_inc/form.js', array(), AKISMET_VERSION, true ); - wp_enqueue_script( 'akismet-form' ); + $fields .= '</p>'; + + return $fields; } - + + public static function output_custom_form_fields( $post_id ) { + // phpcs:ignore WordPress.Security.EscapeOutput + echo self::get_akismet_form_fields(); + } + + public static function inject_custom_form_fields( $html ) { + $html = str_replace( '</form>', self::get_akismet_form_fields() . '</form>', $html ); + + return $html; + } + + public static function append_custom_form_fields( $html ) { + $html .= self::get_akismet_form_fields(); + + return $html; + } + /** - * Mark form.js as async. Because nothing depends on it, it can run at any time - * after it's loaded, and the browser won't have to wait for it to load to continue - * parsing the rest of the page. + * Ensure that any Akismet-added form fields are included in the comment-check call. + * + * @param array $form + * @return array $form */ - public static function set_form_js_async( $tag, $handle, $src ) { - if ( 'akismet-form' !== $handle ) { - return $tag; + public static function prepare_custom_form_values( $form ) { + $prefix = 'ak_'; + + // Contact Form 7 uses _wpcf7 as a prefix to know which fields to exclude from comment_content. + if ( 'wpcf7_akismet_parameters' === current_filter() ) { + $prefix = '_wpcf7_ak_'; } - - return preg_replace( '/^<script /i', '<script async="async" ', $tag ); - } - - public static function inject_ak_js( $fields ) { - echo '<p style="display: none;">'; - echo '<input type="hidden" id="ak_js" name="ak_js" value="' . mt_rand( 0, 250 ) . '"/>'; - echo '</p>'; + + // phpcs:ignore WordPress.Security.NonceVerification.Missing + foreach ( $_POST as $key => $val ) { + if ( 0 === strpos( $key, $prefix ) ) { + $form[ 'POST_ak_' . substr( $key, strlen( $prefix ) ) ] = $val; + } + } + + return $form; } private static function bail_on_activation( $message, $deactivate = true ) { @@ -1277,7 +1444,7 @@ p { $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/'); Akismet::bail_on_activation( $message ); - } else { + } elseif ( ! empty( $_SERVER['SCRIPT_NAME'] ) && false !== strpos( $_SERVER['SCRIPT_NAME'], '/wp-admin/plugins.php' ) ) { add_option( 'Activated_Akismet', true ); } } @@ -1333,16 +1500,98 @@ p { if ( $method !== 'pingback.ping' ) return; + // 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. + // This ticket should hopefully fix that: https://core.trac.wordpress.org/ticket/52524 + // Until that happens, when it's a system.multicall, pre_check_pingback will be called once for every internal pingback call. + // Keep track of how many times this function has been called so we know which call to reference in the XML. + static $call_count = 0; + + $call_count++; + global $wp_xmlrpc_server; - + if ( !is_object( $wp_xmlrpc_server ) ) return false; - - // Lame: tightly coupled with the IXR class. - $args = $wp_xmlrpc_server->message->params; - - if ( !empty( $args[1] ) ) { - $post_id = url_to_postid( $args[1] ); + + $is_multicall = false; + $multicall_count = 0; + + if ( 'system.multicall' === $wp_xmlrpc_server->message->methodName ) { + $is_multicall = true; + + if ( 0 === $call_count ) { + // Only pass along the number of entries in the multicall the first time we see it. + $multicall_count = count( $wp_xmlrpc_server->message->params ); + } + + /* + * $wp_xmlrpc_server->message looks like this: + * + ( + [message] => + [messageType] => methodCall + [faultCode] => + [faultString] => + [methodName] => system.multicall + [params] => Array + ( + [0] => Array + ( + [methodName] => pingback.ping + [params] => Array + ( + [0] => http://www.example.net/?p=1 // Site that created the pingback. + [1] => https://www.example.com/?p=1 // Post being pingback'd on this site. + ) + ) + [1] => Array + ( + [methodName] => pingback.ping + [params] => Array + ( + [0] => http://www.example.net/?p=1 // Site that created the pingback. + [1] => https://www.example.com/?p=2 // Post being pingback'd on this site. + ) + ) + ) + ) + */ + + // Use the params from the nth pingback.ping call in the multicall. + $pingback_calls_found = 0; + + foreach ( $wp_xmlrpc_server->message->params as $xmlrpc_action ) { + if ( 'pingback.ping' === $xmlrpc_action['methodName'] ) { + $pingback_calls_found++; + } + + if ( $call_count === $pingback_calls_found ) { + $pingback_args = $xmlrpc_action['params']; + break; + } + } + } else { + /* + * $wp_xmlrpc_server->message looks like this: + * + ( + [message] => + [messageType] => methodCall + [faultCode] => + [faultString] => + [methodName] => pingback.ping + [params] => Array + ( + [0] => http://www.example.net/?p=1 // Site that created the pingback. + [1] => https://www.example.com/?p=2 // Post being pingback'd on this site. + ) + ) + */ + $pingback_args = $wp_xmlrpc_server->message->params; + } + + if ( ! empty( $pingback_args[1] ) ) { + $post_id = url_to_postid( $pingback_args[1] ); // If pingbacks aren't open on this post, we'll still check whether this request is part of a potential DDOS, // but indicate to the server that pingbacks are indeed closed so we don't include this request in the user's stats, @@ -1355,23 +1604,30 @@ p { $pingbacks_closed = true; } + // 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. + $comment = array( - 'comment_author_url' => $args[0], + 'comment_author_url' => $pingback_args[0], 'comment_post_ID' => $post_id, 'comment_author' => '', 'comment_author_email' => '', 'comment_content' => '', 'comment_type' => 'pingback', 'akismet_pre_check' => '1', - 'comment_pingback_target' => $args[1], + 'comment_pingback_target' => $pingback_args[1], 'pingbacks_closed' => $pingbacks_closed ? '1' : '0', + 'is_multicall' => $is_multicall, + 'multicall_count' => $multicall_count, ); - $comment = Akismet::auto_check_comment( $comment ); + $comment = self::auto_check_comment( $comment, 'xml-rpc' ); if ( isset( $comment['akismet_result'] ) && 'true' == $comment['akismet_result'] ) { - // Lame: tightly coupled with the IXR classes. Unfortunately the action provides no context and no way to return anything. + // Sad: tightly coupled with the IXR classes. Unfortunately the action provides no context and no way to return anything. $wp_xmlrpc_server->error( new IXR_Error( 0, 'Invalid discovery target' ) ); + + // Also note that if this was part of a multicall, a spam result will prevent the subsequent calls from being executed. + // This is probably fine, but it raises the bar for what should be acceptable as a false positive. } } } @@ -1390,8 +1646,17 @@ p { $meta_value = (array) $meta_value; foreach ( $meta_value as $key => $value ) { - if ( ! isset( self::$comment_as_submitted_allowed_keys[$key] ) || ! is_scalar( $value ) ) { - unset( $meta_value[$key] ); + if ( ! is_scalar( $value ) ) { + unset( $meta_value[ $key ] ); + } else { + // These can change, so they're not explicitly listed in comment_as_submitted_allowed_keys. + if ( strpos( $key, 'POST_ak_' ) === 0 ) { + continue; + } + + if ( ! isset( self::$comment_as_submitted_allowed_keys[ $key ] ) ) { + unset( $meta_value[ $key ] ); + } } } |