Live Comments on WP - how I did it

Welcome to Seven Goslings and my first feature article. When you start doing a blog about PHP and AJAX you will need a site that shows of exactly that - PHP and AJAX. I chose to base this site on WordPress, mainly because I was to lazy to show off my PHP skills by wasting a month or two of doing my own system from the ground up. One of the most popular aspects of a WP site to ajaxify is the posting of comments. So guess what I am going to do. Ajaxify the posting of comments.

This being the first time I chose to base something on WP I was quite excited on getting my hands dirty with the code, so the first draft of this live comment posting was - well, a hack. So I wiped the slate clean, started over and decided to set a few game rules:

The first bullet is easy, and basically comes for free when you have the second bullet. By not replacing the original files you can still use the old fashion way of posting files. So all you have to do in order to handle the first bullet is to also leave the original comment form alone. If the files
and the form are intact - you haven’t changed anything (which I in fact hadn’t at this point). But neither had I added the whole "live comment" concept. So I created a simple JavaScript class that: 1) initializes at page load 2) finds the form and 3) adds a onSubmit function to the form. The function intercepts the submit action, does what it has to do and simply returns false. If a onSubmit function returns false the browser stops there and never submits the form.
The JavaScript class looks like this:

WP_Dark_Comment = Class.create();
WP_Dark_Comment.prototype = {
  url: '',
  initialize: function(url) {
    this.url = url;
    $('posttwo').getElementsByTagName('form')[0].onsubmit = this.handleSubmit.bindAsEventListener(this);
  },
  handleSubmit: function(evt) {
    alert(’form submitted’);
    return false;
  }
}
Event.observe(window, ‘load’, function() { new WP_Dark_Comment(’_url_to_post_to.php’); }, true);

So whenever the user clicks the submit button (assuming he has JS enabled here) instead of submitting the data as it should have done, it does a quick alert and stops dead. Wauw - that wasn’t what I was looking for, huh? Well, no it isn’t - but it’s a step in the right direction. All you have to do now is to preform a POST-request with the forms data to your interception-script and display you new comment in the already present list of comments on you post. Sounds simple, right?

Since we need a receiver-script we will dive into the wonders of WPs PHP code. If you aren’t a programmer, this won’t give you that much help. But if you are a programmer it might give you some insight to how I work. So, non-programmers: feel free to skip this paragraph. The original comment-form submits to /wp-comments-post.php, so that’s the file we start our investigation at. Since we specified in bullet two that we weren’t going to modify this file it was clear that we need some way of changing the behaviour. Without actually changing it!
I was stuck dead, so I tried to get some ideas from other WordPress sites that has Live Comments, namely Squible[1] and Siriux [2]. Yes they worked, but no - they violate my third bullet: don’t replace core files. Both of these solutions provide a replacement for wp-comments-post.php which was exactly what I was trying to avoid. The problem with wp-comments-post.php is that whenever it receives information that it didn’t expect it exits with an error-message. This use of the die() command really makes it hard to achieve the effect that you want, without changing it or creating a replacement for it. Squible did, however, give me an idea. If you send out a HTTP/1.0 500, Internal Server Error header you can catch it with the Ajax-request by using the onFailure event-trigger. I also spotted a comment_post hook [3] which was called after the comment was saved in the database.
This started an idea in my head. What will happen if you first send a "HTTP/1.0 500" header, and then a "HTTP/1.0 200 OK" - will the 200 OK replace the 500? A simple PHP-script and ethereal proved me right. The 500 header will be replaced with the 200 OK header. So if you send a 500 header, includes the original wp-comments-post.php file, attaches a comment_post hook that sends out a 200 OK header, checks if the comment was posted via AJAX and if it was, sends back a
copy of the comment so that the JavaScript can insert it into the page and then exits. If you are feeling lost - really lost - by now, don’t worry - so was I. In reality it doesn’t matter if you get all the technical details - just as long as you know that it will work, and why this is better than simply changing the core WordPress files.

I created a receiver-script in the theme directory that containing nothing but this:

<?php
header("HTTP/1.0 500 Internal Server Error");
require "./../../wp-comments-post.php";
?>

So now every request gets a HTTP/1.0 500 header. You then need to register the hook. In order for themes to do this they need a functions.php, which then will be included automaticly. My functions.php basicly contains this:

<?php
class WP_Dark {
  function handleAddComment($id, $approved) {
    header("HTTP/1.0 200 OK");
    if( isset($_POST['ajax']) ) {
      $comment = get_comment($id);
      // display added comment here
      // do a few extra things that happens after the do_action
      die();
    }
  }
}
add_action(’comment_post’, array(’WP_Dark’, ‘handleAddComment’), 1, 2);
?>

Of course this isn’t exactly what my functions.php contains, I have removed quite a bit of code to show the structure I used to achieve the right effect. Now we basicly have all the parts ready. We have the interceptor (which also acts like the quarterback) and the receiver (which, ironicly, itself is sort of a interceptor). Now we only need to teach them how to throw (I know - bad pun, don’t spam me, please). We have rigged the receiver so it will look for a ‘ajax’ post-variable. So when we send the form-data we need to add a extra variable called ajax.
My JavaScript now looks like this:

WP_Dark_Comment = Class.create();
WP_Dark_Comment.prototype = {
  url: '',
  initialize: function(url) {
    this.url = url;
    $('posttwo').getElementsByTagName('form')[0].onsubmit = this.handleSubmit.bindAsEventListener(this);
  },
  handleSubmit: function(evt) {
    postbody = Form.serialize($(’posttwo’).getElementsByTagName(’form’)) + ‘&ajax=1′;
    Ajax.Request(this.url, { postBody: postbody, method: ‘post’, onSuccess: this.handleResponse.bind(this), onFailure: this.handleFailure.bind(this) } );
    return false;
  },
  handleResponse: function(response) {
    $(’placeholder’).innerHTML = response.responseText;
  },
  handleFailure: function(response) {
    alert(response.responseText);
  }
}
Event.observe(window, ‘load’, function() { new WP_Dark_Comment(’_url_to_post_to.php’); }, true);

This will insert whatever the receiver sends, back into the element with the id of "placeholder". If the server only sends out the HTTP/1.0 500 (hence having exited prematurely), it will then do a simple alert() of the text received. No, this isn’t final, but it will do for now. Now we need to modify the PHP backend so that it, given the comment is sent via ajax, returns the rendered comment that was just sent to it. My functions.php now looks like this:

<?php

class WP_Dark {
  function handle_add_comment($id, $approved) {
    // Override the HTTP/1.0 500 header we sent out previously
    header("HTTP/1.0 200 OK");

    if( isset($_POST['ajax'])) {
      // We need to fetch the comment again…
      $comment = get_comment($id);
      // Since we are going to "kill" this process to avoid the Location-header, we have to emulate the rest of the wp_new_comment() function
      if ( ’spam’ !== $approved ) {
        if ( ‘0′ == $approved ) {
          wp_notify_moderator($id);
        }

        $post = &get_post($comment->comment_post_ID);

        if ( get_settings(’comments_notify’) && $approved && $post->post_author != $comment->user_ID ) {
          wp_notify_postauthor($id, $comment->comment_type);
        }
      }

      if( empty($comment->comment_author_url) || trim($comment->comment_author_url) == ‘http://’ ) {
        $link = $comment->comment_author;
      } else {
        $link = ‘<a xhref="’ . $comment->comment_author_url . ‘" rel="external nofollow">’ . $comment->comment_author . ‘</a>’;
      }

      $post  = ‘<div class="commentcontent">’ . "\n";
      $post .= ‘    <p>’ . apply_filters(’comment_text’, apply_filters(’get_comment_text’, $comment->comment_content)) . ‘</p>’ . "\n";
      $post .= ‘<p class="commentinfo"><cite>’ . __(’Comment’) . ‘ ‘. __(’by’) . ‘ ‘ . $link . ‘ &#8212; ‘ . date(get_settings(’date_format’), time()) . ‘ @ <a xhref="#comment-’ . $comment_id . ‘">’ . date(get_settings(’time_format’), time()) . ‘</a></cite></p>’ . "\n";
      $post .= ‘</div></div>’;

      echo $post;
      die;
    }
  }
}

add_action(’comment_post’, array(’WP_Dark’, ‘handle_add_comment’), 1, 2);

?>

As you might have noticed, there is quite a bit of code that doesn’t actually help our process, but as I have explain in the comment, we are going to halt the script after we have returned the new comment. So I have recreated the the 10 lines that are in wp_new_comment() but below the call the do_action(). Okay, so this kinda violates my 3. rule, but there is no other way to do this. At least not one I could think of. This is an open invitation – can this be done in a better way?.

So we should now be able to let the magic happen. The JavaScript is able to to get the ball, ehh, comment and throw it to the receiver, ehh PHP. But it
throws like a girl, doesn’t it? No Magic? No Sparks? No Nothing? Well, that’s no fun. We need some shiny effect to show of our new-found AJAX fame.

I change my handleRequest() function in JavaScript to look like this:

        handleRequest: function(response) {
          c = $('posttwo'); cmts = Array();
          $A(c.getElementsByTagName('div')).each( function(div) { if( c == div.parentNode ) { cmts[cmts.length] = div; }});

          comment = window.document.createElement(’div’);
          if((cmts.length - 1) == 0) {
            comment.className = ‘commentodd’;
            comment.innerHTML = response.responseText;
            c.insertBefore(comment, c.getElementsByTagName(’h2′)[0].nextSibling);
            $(’posttwo’).getElementsByTagName(’h2′)[0].innerHTML = ‘1 COMMENT’;
          } else {
            firstComment = $(’posttwo’).getElementsByTagName(’div’)[1];
            newClass = (firstComment.getAttribute(’class’) == ‘commentodd’) ? ‘commenteven’ : ‘commentodd’;
            comment.className = newClass;
            comment.innerHTML = response.responseText;
            c.insertBefore(comment, firstComment);
            c.getElementsByTagName(’h2′)[0].innerHTML = cmts.length + ‘ COMMENTS’;
          }

          np = new fx.Height(comment, {duration: 2000});
          np.hide();
          new fx.Height(this.form.parentNode, {duration: 2000, onComplete: function() {np.toggle();}}).toggle();
        }

As you can quickly see, the function has evolved pretty far from the $(’placeholder’).innerHTML = response.responseText; it started out with. First it creates an array cmts in which it places all the current comments on the post. We need this so we can tell whether this is the first comment or not and to update the heading with the new number of comments. We also create a div-element called comment in which we now place the responseText and inserts this at the top of the comment-stack.
At the end we have a bit of Moo.fx effects going on to get the fold-in/fold-out action we so desperately need to show off our new-found AJAX glory. Basically we create an effect on our new comment and hides it. We then create an effect on the form, and tells it to call the other effect when
finished. There – goal, score, touchdown – we did it. We have a nice effect that shows off our AJAX with some nice folding action.

If you left confused and dazzled by the fact that we did those effect in 3 compressed lines, checkout the Moo.fx documentation, as it explains what the hell it does and how…

I have left out quite an important part of the PHP-script because, important as it is, it is still expendable. It is the UTF-8 encode and decode functions I have placed in the WP_Dark class. Why do I have these, why are they important yet expendable you might ask. Don’t ask me – Ask the Safari guys. The reason why I have those UTF8 encode/decode wrapper functions is because Safari expects input from xmlhttprequest calls to be in the ISO-8859-1 encoding whilst the rest of the world (IE and Firefox) expects it to be UTF-8 encoded. And in my world UTF-8 wins hands down, so we go with UTF-8, but to accommodate Safaris need for ISO-8859-1 I have added the wrapper functions to deal with that. If you need them download the theme and check them out, and lure when I use them to make sure Safari and the rest of the world is happy.

If I get the time I will rework the whole “I know exacly what elements I’m looking for so lets just go for the childNodes of node XX” issue the script suffers under. It really shouldn’t just assume a whole lot of stuff like “comments are placed as childNodes of the element with id posttwo” as its bound to make it break at some time when you update the html. I just haven’t had to time yet, and who knows.. maybe I never will. But if I do, I will post a follow-up article.

I hope you enjoyed my first article here at SevenGoslings, and I hope that you will come back in the future to read more articles about PHP and AJAX development.

Regards,
- Morten Fangel

Footnotes:

Read more | July 29th, 2006 | AJAX, PHP | 8 comments

« Next Entries