From 0bdfa2ab1e2bc0d52daf1a8559d8f7ec9023cb8d Mon Sep 17 00:00:00 2001 From: Boone Gorges Date: Tue, 11 Oct 2016 03:43:28 +0000 Subject: [PATCH] Comments: Abstract `die()` calls from comment submission routine. Since 4.4, comment submission has been mostly abstracted into a function, rather than being processed inline in wp-comments-post.php. This change made it easier to write automated tests against the bulk of the comment submission process. `wp_allow_comment()` remained untestable, however: when a comment failed one of its checks (flooding, duplicates, etc), `die()` or `wp_die()` would be called directly. This shortcoming posed problems for any application attempting to use WP's comment verification functions in an abstract way - from PHPUnit to the REST API. The current changeset introduces a new parameter, `$avoid_die`, to the `wp_new_comment()` stack. When set to `true`, `wp_new_comment()` and `wp_allow_comment()` will return `WP_Error` objects when a comment check fails. When set to `false` - the default, for backward compatibility - a failed check will result in a `die()` or `wp_die()`, as appropriate. Prior to this changeset, default comment flood checks took place in the function `check_comment_flood_db()`, which was hooked to the 'check_comment_flood' action. This design allowed the default comment flood routine to be bypassed or replaced using `remove_action()`. In order to maintain backward compatibility with this usage, while simultaneously converting the comment flood logic into something that returns a value rather than calling `die()` directly, `check_comment_flood_db()` has been changed into a wrapper function for a call to `add_filter()`; this, in turn, adds the *actual* comment flood check to a new filter, 'wp_is_comment_flood'. Note that direct calls to `check_comment_flood_db()` will no longer do anything in isolation. Props websupporter, rachelbaker. Fixes #36901. Built from https://develop.svn.wordpress.org/trunk@38778 git-svn-id: http://core.svn.wordpress.org/trunk@38721 1a063a9b-81f0-0310-95a4-ce76da25c4cd --- wp-includes/comment.php | 135 ++++++++++++++++++++++++++------ wp-includes/default-filters.php | 2 +- wp-includes/version.php | 2 +- 3 files changed, 113 insertions(+), 26 deletions(-) diff --git a/wp-includes/comment.php b/wp-includes/comment.php index 54a6fe1256..b260ee8643 100644 --- a/wp-includes/comment.php +++ b/wp-includes/comment.php @@ -588,13 +588,19 @@ function sanitize_comment_cookies() { * Validates whether this comment is allowed to be made. * * @since 2.0.0 + * @since 4.7.0 The `$avoid_die` parameter was added, allowing the function to + * return a WP_Error object instead of dying. * * @global wpdb $wpdb WordPress database abstraction object. * - * @param array $commentdata Contains information on the comment - * @return int|string Signifies the approval status (0|1|'spam') + * @param array $commentdata Contains information on the comment. + * @param bool $avoid_die When true, a disallowed comment will result in the function + * returning a WP_Error object, rather than executing wp_die(). + * Default false. + * @return int|string|WP_Error Allowed comments return the approval status (0|1|'spam'). + * If `$avoid_die` is true, disallowed comments return a WP_Error. */ -function wp_allow_comment( $commentdata ) { +function wp_allow_comment( $commentdata, $avoid_die = false ) { global $wpdb; // Simple duplicate check @@ -639,10 +645,15 @@ function wp_allow_comment( $commentdata ) { * @param array $commentdata Comment data. */ do_action( 'comment_duplicate_trigger', $commentdata ); - if ( wp_doing_ajax() ) { - die( __('Duplicate comment detected; it looks as though you’ve already said that!') ); + if ( true === $avoid_die ) { + return new WP_Error( 'comment_duplicate', __( 'Duplicate comment detected; it looks as though you’ve already said that!' ), $dupe_id ); + } else { + if ( wp_doing_ajax() ) { + die( __('Duplicate comment detected; it looks as though you’ve already said that!') ); + } + + wp_die( __( 'Duplicate comment detected; it looks as though you’ve already said that!' ), 409 ); } - wp_die( __( 'Duplicate comment detected; it looks as though you’ve already said that!' ), 409 ); } /** @@ -651,18 +662,49 @@ function wp_allow_comment( $commentdata ) { * Allows checking for comment flooding. * * @since 2.3.0 + * @since 4.7.0 The `$avoid_die` parameter was added. * * @param string $comment_author_IP Comment author's IP address. * @param string $comment_author_email Comment author's email. * @param string $comment_date_gmt GMT date the comment was posted. + * @param bool $avoid_die Whether to prevent executing wp_die() + * or die() if a comment flood is occurring. */ do_action( 'check_comment_flood', $commentdata['comment_author_IP'], $commentdata['comment_author_email'], - $commentdata['comment_date_gmt'] + $commentdata['comment_date_gmt'], + $avoid_die ); + /** + * Filters whether a comment is part of a comment flood. + * + * The default check is wp_check_comment_flood(). See check_comment_flood_db(). + * + * @since 4.7.0 + * + * @param bool $is_flood Is a comment flooding occurring? Default false. + * @param string $comment_author_IP Comment author's IP address. + * @param string $comment_author_email Comment author's email. + * @param string $comment_date_gmt GMT date the comment was posted. + * @param bool $avoid_die Whether to prevent executing wp_die() + * or die() if a comment flood is occurring. + */ + $is_flood = apply_filters( + 'wp_is_comment_flood', + false, + $commentdata['comment_author_IP'], + $commentdata['comment_author_email'], + $commentdata['comment_date_gmt'], + $avoid_die + ); + + if ( $is_flood ) { + return new WP_Error( 'comment_flood', __( 'You are posting comments too quickly. Slow down.' ) ); + } + if ( ! empty( $commentdata['user_id'] ) ) { $user = get_userdata( $commentdata['user_id'] ); $post_author = $wpdb->get_var( $wpdb->prepare( @@ -715,24 +757,50 @@ function wp_allow_comment( $commentdata ) { } /** - * Check whether comment flooding is occurring. + * Hooks WP's native database-based comment-flood check. + * + * This wrapper maintains backward compatibility with plugins that expect to + * be able to unhook the legacy check_comment_flood_db() function from + * 'check_comment_flood' using remove_action(). + * + * @since 2.3.0 + * @since 4.7.0 Converted to be an add_filter() wrapper. + */ +function check_comment_flood_db() { + add_filter( 'wp_is_comment_flood', 'wp_check_comment_flood', 10, 5 ); +} + +/** + * Checks whether comment flooding is occurring. * * Won't run, if current user can manage options, so to not block * administrators. * - * @since 2.3.0 + * @since 4.7.0 * * @global wpdb $wpdb WordPress database abstraction object. * - * @param string $ip Comment IP. - * @param string $email Comment author email address. - * @param string $date MySQL time string. + * @param bool $is_flood Is a comment flooding occurring? + * @param string $ip Comment IP. + * @param string $email Comment author email address. + * @param string $date MySQL time string. + * @param bool $avoid_die When true, a disallowed comment will result in the function + * returning a WP_Error object, rather than executing wp_die(). + * Default false. + * @return bool Whether comment flooding is occurring. */ -function check_comment_flood_db( $ip, $email, $date ) { +function wp_check_comment_flood( $is_flood, $ip, $email, $date, $avoid_die = false ) { + global $wpdb; + + // Another callback has declared a flood. Trust it. + if ( true === $is_flood ) { + return $is_flood; + } + // don't throttle admins or moderators if ( current_user_can( 'manage_options' ) || current_user_can( 'moderate_comments' ) ) { - return; + return false; } $hour_ago = gmdate( 'Y-m-d H:i:s', time() - HOUR_IN_SECONDS ); @@ -774,13 +842,19 @@ function check_comment_flood_db( $ip, $email, $date ) { * @param int $time_newcomment Timestamp of when the new comment was posted. */ do_action( 'comment_flood_trigger', $time_lastcomment, $time_newcomment ); + if ( true === $avoid_die ) { + return true; + } else { + if ( wp_doing_ajax() ) { + die( __('You are posting comments too quickly. Slow down.') ); + } - if ( wp_doing_ajax() ) - die( __('You are posting comments too quickly. Slow down.') ); - - wp_die( __( 'You are posting comments too quickly. Slow down.' ), 429 ); + wp_die( __( 'You are posting comments too quickly. Slow down.' ), 429 ); + } } } + + return false; } /** @@ -1730,6 +1804,8 @@ function wp_throttle_comment_flood($block, $time_lastcomment, $time_newcomment) * * @since 1.5.0 * @since 4.3.0 'comment_agent' and 'comment_author_IP' can be set via `$commentdata`. + * @since 4.7.0 The `$avoid_die` parameter was added, allowing the function to + * return a WP_Error object instead of dying. * * @see wp_insert_comment() * @global wpdb $wpdb WordPress database abstraction object. @@ -1753,9 +1829,11 @@ function wp_throttle_comment_flood($block, $time_lastcomment, $time_newcomment) * @type string $comment_author_IP Comment author IP address in IPv4 format. Default is the value of * 'REMOTE_ADDR' in the `$_SERVER` superglobal sent in the original request. * } - * @return int|false The ID of the comment on success, false on failure. + * @param bool $avoid_die Should errors be returned as WP_Error objects instead of + * executing wp_die()? Default false. + * @return int|false|WP_Error The ID of the comment on success, false or WP_Error on failure. */ -function wp_new_comment( $commentdata ) { +function wp_new_comment( $commentdata, $avoid_die = false ) { global $wpdb; if ( isset( $commentdata['user_ID'] ) ) { @@ -1804,7 +1882,10 @@ function wp_new_comment( $commentdata ) { $commentdata = wp_filter_comment($commentdata); - $commentdata['comment_approved'] = wp_allow_comment($commentdata); + $commentdata['comment_approved'] = wp_allow_comment( $commentdata, $avoid_die ); + if ( is_wp_error( $commentdata['comment_approved'] ) ) { + return $commentdata['comment_approved']; + } $comment_ID = wp_insert_comment($commentdata); if ( ! $comment_ID ) { @@ -1818,7 +1899,10 @@ function wp_new_comment( $commentdata ) { $commentdata = wp_filter_comment( $commentdata ); - $commentdata['comment_approved'] = wp_allow_comment( $commentdata ); + $commentdata['comment_approved'] = wp_allow_comment( $commentdata, $avoid_die ); + if ( is_wp_error( $commentdata['comment_approved'] ) ) { + return $commentdata['comment_approved']; + } $comment_ID = wp_insert_comment( $commentdata ); if ( ! $comment_ID ) { @@ -2940,11 +3024,14 @@ function wp_handle_comment_submission( $comment_data ) { 'user_ID' ); - $comment_id = wp_new_comment( wp_slash( $commentdata ) ); + $comment_id = wp_new_comment( wp_slash( $commentdata ), true ); + if ( is_wp_error( $comment_id ) ) { + return $comment_id; + } + if ( ! $comment_id ) { return new WP_Error( 'comment_save_error', __( 'ERROR: The comment could not be saved. Please try again later.' ), 500 ); } return get_comment( $comment_id ); - } diff --git a/wp-includes/default-filters.php b/wp-includes/default-filters.php index 3402e48ef2..dba691ab66 100644 --- a/wp-includes/default-filters.php +++ b/wp-includes/default-filters.php @@ -197,7 +197,7 @@ add_filter( 'tiny_mce_before_init', '_mce_set_direction' ); add_filter( 'teeny_mce_before_init', '_mce_set_direction' ); add_filter( 'pre_kses', 'wp_pre_kses_less_than' ); add_filter( 'sanitize_title', 'sanitize_title_with_dashes', 10, 3 ); -add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 3 ); +add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 ); add_filter( 'comment_flood_filter', 'wp_throttle_comment_flood', 10, 3 ); add_filter( 'pre_comment_content', 'wp_rel_nofollow', 15 ); add_filter( 'comment_email', 'antispambot' ); diff --git a/wp-includes/version.php b/wp-includes/version.php index 01ad23b478..206f643524 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -4,7 +4,7 @@ * * @global string $wp_version */ -$wp_version = '4.7-alpha-38777'; +$wp_version = '4.7-alpha-38778'; /** * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.