Enable commenting on front-end preview page for pending posts

Preface

Once post preview is frontend, the comment form depends on how theme handle it. I will assume in this answer that the comment form is shown using the standard comment_form() function.

The previous assumption isn’t enough, in fact comment_form is probably the function in WordPress core with more hooks. There is almost an hook for every line. So what is shown on page, depends a lot on plugins or theme that can change it.

After that, comment_form use to save comment a POST to wp-comment-post.php that contain a lot of hooks too, so even if my solution works on a vanilla install of WP using twenty* theme, I can’t assure it works also with some plugins or custom theme.

Workflow

So, once we are assuming to be in a standard case, what you ask is simpler than the linked Q/A, because comment form don’t use ajax and because there are a lot of hooks that can help us to obtain the desired result (talked about negative side of this point in preface).

Essentially we have to:

  1. Adding a field in the comment form when we are on pending post preview, in this way we can recognize a comment coming from that form.
  2. Once we have to break a security check of WordPress to allow comment on pending post, is a good idea replace this check with another: using a nonce for the hidden field at point 1. can be a good idea.
  3. As additional security check, I think is a good idea enable comments only for registered users (but seems you are already allowing preview access only to registered users). Probably also restrict the roles is a good idea.
  4. The core part of this workflow, is break the check that WordPress does to prevent comment on pending and draft posts. My idea is just to tease WordPress leaving it believe our post is published. The way I will do it, can be taken as an example of why global variables should be avoided to prevent our code is teased by someone.
  5. Last thing we have to do, is intercept the redirect after comment is created, because standard redirect point to standard post permalink, we need to point to post preview permalink

The Code

First of all, let’s create a function that add a nonce field in comment form, only for allowed logged users and only in post preview. I will also write a function to check if the current user is one of the allowed, in this way I can use it for other scopes.
In this function I put a custom filter hook, in this way roles alloewd can be changed.

/**
 * Return true if user can comment on post preview
 */
function is_a_preview_commenter() {
  // change this roles according to your needs
  $roles = array('administrator', 'editor', 'author', 'contributor');
  $allowed_roles = apply_filters('preview_comment_allowed_roles', $roles);
  $user = wp_get_current_user();
  $inrole = array_intersect($user->roles, $allowed_roles);
  return ! empty( $inrole );
} 

/**
 * Add a nonce field for comment form in post preview for allowed users
 */
function additional_comment_fields() {
  if ( is_preview() && is_a_preview_commenter() ) {
    $nonce = wp_create_nonce('comment_preview');
    echo '<input type="hidden" name="check_the_preview" value="' . $nonce . '" />';
  }
}
add_action( 'comment_form_logged_in_after', 'additional_comment_fields');

Now let’s tease WordPress. The way WP uses to check if the post status is good to be commented is to call $status_obj = get_post_status_object($status) (where $status is the post_status field of the post being commented) then check if $status_obj is private or public otherwise wp_die. But the only thing that get_post_status_object does is to return a value from the global array $wp_post_statuses (the one keyed with the requested post status).
Faking the global variable means faking get_post_status_object and so also faking WordPress check.

Change the global variable is super easy, however I will do some check to assure the post come from us (thanks to nonce hidden field previously added) and also check if the current user exists and is allowed:

/**
 * On 'wp_loaded', when the current page is wp-comments-post.php, check if the request
 * comes from post preview, if so and also the current user is allowed replace 
 * $GLOBALS['wp_post_statuses']['pending'] with $GLOBALS['wp_post_statuses']['publish']
 */
function fake_public_pending() {
  global $pagenow;
  if ( $pagenow === 'wp-comments-post.php' && is_a_preview_commenter() ) {
    $nonce = filter_input(INPUT_POST, 'check_the_preview', FILTER_SANITIZE_STRING);
    if ( empty($nonce) || ! wp_verify_nonce($nonce, 'comment_preview') ) return;
    global $wp_post_statuses;
    // let WordPress believe all pending posts are published post
    $wp_post_statuses['pending'] = $wp_post_statuses['publish'];
  }
}
add_action('wp_loaded', 'fake_public_pending');

Now, after a comment is inserted from post preview, we have to redirect browser again to post preview. As usual we assure the request comes from post preview and current user has
one of the allowed roles

/**
 * After a comment is inserted, redirect page to post preview when request come from
 * post preview. Also check if current user role is one of the allowed
 */
function redirect_to_preview( $comment, $user ) {
  if ( ! is_a_preview_commenter() ) return;
  $nonce = filter_input(INPUT_POST, 'check_the_preview', FILTER_SANITIZE_STRING);
  if ( empty($nonce) || ! wp_verify_nonce($nonce, 'comment_preview') ) return;
  $link = get_permalink($comment->comment_post_ID);
  $url = add_query_arg( array('preview' => 'true'), $link );
  wp_safe_redirect("{$url}#comment-{$comment->comment_ID}", 302);
  exit();
}
add_action('set_comment_cookies', 'redirect_to_preview', 9999, 2 );

Leave a Comment