WordPress: wp-comments-post.php

0
1571
Wordpress WP-Comments-Post

In this article, I look at WordPress’ wp-comments-post.php file.

We are going to start off this post by going through the file in a snippet like fashion. Previous articles that I wrote on WordPress innards involved very small source files but that situation is about to change as we delve even deeper. So open up your text editor and follow along. You can also view the source code online at WordPress’ GitHub.

The comment form (wp-includes\comment-template.php) has as its action attribute this file. So wp-comments-post.php gets called when the Submit button is pressed.

Let’s start off with this snippet. What does it do and most importantly, why?

  1. if ('POST' != $_SERVER['REQUEST_METHOD']) {
  2.   header('Allow: POST');
  3.   header('HTTP/1.1 405 Method Not Allowed');
  4.   header('Content-Type: text/plain');
  5.   exit;
  6. }

$_SERVER[‘REQUEST_METHOD’] returns HTTP 1.1 methods of the following (based on W3C HTTP specification):

  • CONNECT
  • DELETE
  • GET
  • HEAD
  • OPTIONS
  • POST
  • PUT
  • TRACE

If the user agent is not requesting an HTTP POST, return a HTTP 405 error. WordPress does this because this PHP file is only for posting comments. This is a nice little snippet that should be used in all your code to validate the actual method being requested.

On a side note, while I was trying to understand this code, I noticed a minor flaw in that WordPress is always saying it is running HTTP 1.1. It could very well be that WordPress is running on HTTP 2. To solve this, $_SERVER[‘SERVER_PROTOCOL’] needs to be used to interrogate the HTTP server it is running on:

  1. if ('POST' != $_SERVER['REQUEST_METHOD']) {
  2.   header('Allow: POST');
  3.   header($_SERVER['SERVER_PROTOCOL'] . ' 405 Method Not Allowed');
  4.   header('Content-Type: text/plain');
  5.   exit;
  6. }

This piece of code can be refactored to handle a more generic situation:

  1. function CheckRequestMethod($method, $message) {
  2.   $method = strtoupper($method);
  3.   if ($method != $_SERVER['REQUEST_METHOD']) {
  4.     header('Allow: ' . $method);
  5.     header($_SERVER['SERVER_PROTOCOL'] . " " . $message);
  6.     header('Content-Type: text/plain');
  7.     exit;
  8.   }
  9. }

The next few lines loads wp-load.php. This is the bootstrap file that brings in wp-config.php.

  1. require( dirname(__FILE__) . '/wp-load.php' );
  2. nocache_headers();

The headers should not be cached so an expiration day in the past is sent along with some metadata cache settings:

  1.   'Expires' => 'Wed, 11 Jan 1984 05:00:00 GMT'
  2.   'Cache-Control' => 'no-cache, must-revalidate, max-age=0'

But, a better solution is this (StackOverflow no cache):

  1. function HTTPNoCache() {
  2.   header("Cache-Control: no-cache, no-store, must-revalidate");
  3.   header("Pragma: no-cache");
  4.   header("Expires: 0");
  5. }

Why not cache this request? Because this resulted in an error. If the user agent went back again and supplied a good request, it should be able to and not be turned away until the cache expires the next time. So it makes the request immediate, rather than delayed due to error. This is a good habit to get into and use in your PHP development.

So to summarize up to this point:

  1. It must be a POST request
  2. Bootstrap WordPress
  3. Tell user agent to not cache this request

Now we get the meat of posting a comment!

  1. $comment = wp_handle_comment_submission(wp_unslash($_POST));
  2. if (is_wp_error($comment)) {
  3.   $data = intval($comment->get_error_data());
  4.   if (!empty($data)) {
  5.     wp_die('<p>' . $comment->get_error_message() .
  6.            '</p>', __( 'Comment Submission Failure' ),
  7.            array('response' => $data, 'back_link' => true));
  8.   } else {
  9.     exit;
  10.   }
  11. }

wp_handle_comment_submission() takes the $_POST data and does a lot of validation and checking. On success, it returns a WP_Comment instance. If there are any problems in the comment submission, a WP_Error is returned. is_wp_error() is a neat little function:

  1. function is_wp_error($thing) {
  2.   return($thing instanceof WP_Error);
  3. }

Its neat because it checks to see if the very thing being passed is an instance of a class. In this case, WP_Error which is WordPress’ way of representing an error in the system. Looking at the code block before this, you would think that $comment would contain some sort of string signature like “Error:” in it to distinguish between an actual comment and an error string. But it turns out wp_handle_comment_submission returns a WP_Comment object. Pause and think about that for a minute. You have a function that can return multiple types. This lends itself to PHP being a loosely typed language.

A much more generic version would look like this:

  1. function isOf($obj, $class) {
  2.   return($obj instanceof $class);
  3. }

An important thing to consider is that wp-comments-post.php is located in the root of your WordPress distribution. By it being there, it makes it a little more difficult for someone to know where your WordPress distribution is stored – that is, if it was stored within a WordPress folder. Because it is a public file, it is subject to abuse as well for anyone who knows your website is running WordPress can make a call to this code file externally. This is why it is better that you always force comments to be associated with a registered user and not let guest posts be made. If you don’t do this, you will have to rely upon a spam service like Akismet to do the dirty work or Disqus as your comment system.

Remember, this file is for posting a comment. We want to track the user and set some cookies. If it is a registered user, it will be set to a WP_User instance. Otherwise, its an anonymous user and set to 0. do_action is a plugin function that gets called when something is about to happen. In this case, any plugin that taps into the set_comment_cookies action will be notified.

$user = wp_get_current_user();
do_action('set_comment_cookies', $comment, $user);

After the commenter has finished posting, we redirect him to a new location. This is a POST variable and is usually a hidden variable in a form. By default, the comment form does not have a redirect_to variable.

$location = empty($_POST['redirect_to']) ? get_comment_link($comment) : $_POST['redirect_to'] . '#comment-' . $comment->comment_ID;
$location = apply_filters('comment_post_redirect', $location, $comment);
wp_safe_redirect($location);

This code is a bit hard to read so lets rearrange it:

if (empty($_POST['redirect_to']) {
  $location = get_comment_link($comment);
} else {
  $location = $_POST['redirect_to'] . '#comment-' . $comment->comment_ID:
}
$location = apply_filters('comment_post_redirect', $location, $comment);
wp_safe_redirect($location);

If a $_POST[‘redirect_to’] is not supplied, get_comment_link() returns the permalink to the comment that was posted. If it is supplied, we append a comment fragment identifier along with the comment ID. A filter can be applied for the location. This isn’t used in WordPress core as I found no instances of it. Rather, it is useful to plugins.

wp_safe_redirect() is an interesting little function. It is called to provide a local redirect. The source code suggests that rogue plugins can send the user to another location outside your host. Even imagine if someone stole your comment form code and replaced it with their own location. If the location isn’t local, the redirect is to wp-admin folder.