WordPress MU and AJAX comments

Discontinued

This plugin has been discontinued. There are better, more compatible ones available by now so there’s really no reason to further support this one!

How complicated could it be to adopt a WordPress plugin to work with WordPress MU? Pretty complicated, if you’re the purist and perfectionist I am. From adoption to rewrite, up to a weird WordPress issue i am currently fighting.

Starting

I already mentioned a while ago that I am looking for an elegant solution of combining multiple blogs. Well, turns out that WordPress can do just this for me, with the superior free thing that WordPress MU is. During installation I stumbled up a PlugIn that is called AJAX Comments, and this is something I always wanted to have in WordPress. So i thought I’ll just grab it, bring it to WordPress MU and enjoy the sexyness of AJAXified commenting.

I wished.

Reality turned out to be much more cruel. First of all the plugin didn’t work. Ok, probably I’m just missing out on a few adoptions. Probably that would have done the trick, but as I started digging into the code I got enthusastic to bring this thing to a more sophisticated stage of operating. What was bothering me about that PlugIn?

  • It’s all mixed in one file
  • The whole comment processing happens independent from WordPress core functionality
  • Therefore it’s interfering with other plugins
  • It needs in-depth adoption for different themes, which especially for use with WordPress MU seemed to be a problem

Please note that, by now, there’s still a problem with fetching the WordPress error message, so the PlugIn will just use a generic, non-localized, uninformative standard error message!

Organize, baby!

So I started rewriting (as I thought it might be interesting to others I added precious documentation to everything, so if if you’re not interested in my rambling and just want to get to the code have a look at the sources). First of all WordPress MU seems to not like PlugIns inside folders (at least not in the folder /mu-plugins), so I put the main file to /mu-plugins and added the folder for the additional stuff. Then I started separating functionality. The JavaScript was output through that very file, due it needed a few little parameters. I took the whole JS-functionality and put it into one clean, static .js – file and then hooked in a few variable definitions that would solve the parameter problem. So let’s have a look how the JavaScript-sections turn out after the little rework.

@/wpmu/mu-plugins/ajax-comments.php
  1. global $ajax_comments_count;
  2. $ajax_comments_count = 0;
  3. add_action('wp_footer',    'ajax_comments_getcount');
  4. add_action('comment_text', 'ajax_comments_countdisplay');
  5. add_action('wp_head',      'ajax_comments_js');

What for?

43, 44 and 45 handle with a simple but annoying issue. The problem is that when a page is viewed by anyone who is logged in chances are that there are more comments actually being displayed than are approved. For example if you’re an admin you’ll see all unapproved comments, but the variables available in WordPress core functions don’t tell us anything about how many comments there actually are on the page. So what I did was to hook a function to comment_text, abusing a filter, to increment the global variable $ajax_comments_count by 1 for every actually displayed commment. Neat:

  1. @/wpmu/mu-plugins/ajax-comments/functions.php
  2. function ajax_comments_countdisplay($comment = '') {
  3.      global $ajax_comments_count;
  4.     $ajax_comments_count++;
  5.     return $comment;
  6. }

Line 44 hooks in just before the closing body-tag and hardcodes the JavaScript variable telling the scripting-magic-pixie-stuff if there’s an even or odd number of comments:

  1. @/wpmu/mu-plugins/ajax-comments/functions.php
  2. function ajax_comments_getcount() {
  3.    global $ajax_comments_count;
  4.    echo '<script type="text/javascript">ajax_comments_odd  = '. ($ajax_comments_count % 2 == 0 ? 'true' : 'false') .';</script>' . "\n";
  5. }

Multiple Theme Support

Line 46 deals with everything else regarding JavaScript, including theme-specific settings. Yep, if you install the original PlugIn it soon turns out that you’ll have to modify it for various themes. That’s disturbing, but ok in general. However, when using this for WordPress MU you’d run into problems if the available themes use different markup. So I added a few settings to make this more flexible:

@/wpmu/mu-plugins/ajax-comments/functions.php
  1. function ajax_comments_js() {
  2.    require_once('themes.php');
  3.    if (!$ajax_comments_themes['hardcode']) {
  4.        $curTempDir = (substr(TEMPLATEPATH, strlen(dirname(TEMPLATEPATH))));
  5.        $allThemes  = array_keys($ajax_comments_themes);
  6.        for ($i=0; !is_array($theme), $i&lt;count($allThemes); $i++) {
  7.            if (strpos($curTempDir, $allThemes[$i])) {
  8.                $theme = $ajax_comments_themes[$allThemes[$i]];
  9.            }
  10.        }
  11.        if (!is_array($theme)) $theme = $ajax_comments_themes['default'];
  12.    } else {
  13.        $theme = $ajax_comments_themes[$ajax_comments_themes['hardcode']];
  14.    }

Okay, so let’s have a look at this. Line 2 includes the theme-settings script themes.php. In this script you can specify theme-specific settings, like so (this also is the default setting the PlugIn falls back to if it can’t figure out anything else):

  1. @/wpmu/mu-plugins/ajax-comments/functions.php
  2. $ajax_comments_themes['default'] = array(
  3.     'commentform',   // [0] = id or classname form
  4.     'commentlist',   // [1] = id or classname 04: comment-&lt;ol&gt;
  5.     'commentform',   // [2] = id or classname element comments are inserted before
  6.     ''               // [3] = comma-separated list of element-id's to hide
  7. );

Line 3 is easy: If there’s a hardcoded theme-setting the PlugIn will obey, use it and just go on. The more interesting part happens if there’s no hardcoded setting. Then the current template path is used to find a matching setting (hint: if you add settings, name them according to the directory they are located at and all the magic will do just fine). If there’s no matching setting available, the default settings will be used, matching the default kubrick-theme.

  1. @/wpmu/mu-plugins/ajax-comments/functions.php
  2.     $hideMore = $theme[3];
  3.     if (strpos($hideMore, ',') !== false) {
  4.         $all = explode(',', $hideMore);
  5.         for ($i=0; $i&lt;count($all); $i++) {
  6.             $all[$i] = trim($all[$i]);
  7.         }
  8.         $hideMore = join("','", $all);
  9.     }
  10.  
  11.     if (!empty($hideMore)) $hideMore = "'". trim($hideMore) . "'";

$hideMore can be one or many Id’s, so the PlugIn has to act accordingly, creating a nice string that can be used as output to feed the new Array() that soon will follow:

  1. @/wpmu/mu-plugins/ajax-comments/functions.php
  2.     echo '<script type="text/javascript" src="'. PLUGIN_AJAXCOMMENTS_ROOT.PLUGIN_AJAXCOMMENTS_PATH .'ajax-comments/scriptaculous/prototype.js"></script>' . "\n";
  3.     echo '<script type="text/javascript" src="'. PLUGIN_AJAXCOMMENTS_ROOT.PLUGIN_AJAXCOMMENTS_PATH .'ajax-comments/scriptaculous/scriptaculous.js"></script>' . "\n";
  4.     echo '<script type="text/javascript" src="'. PLUGIN_AJAXCOMMENTS_ROOT.PLUGIN_AJAXCOMMENTS_PATH .'ajax-comments/ajax-comments.js"></script>' . "\n";
  5.     echo '<script type="text/javascript">' . "\n"
  6.         .'  ajax_comments_path = "'. PLUGIN_AJAXCOMMENTS_ROOT.PLUGIN_AJAXCOMMENTS_PATH .'";' . "\n"
  7.         ."  ajax_comments_form = '${theme[0]}';\n"
  8.         ."  ajax_comments_list = '${theme[1]}';\n"
  9.         ."  ajax_comments_here = '${theme[2]}';\n"
  10.         ."  ajax_comments_hide = new Array($hideMore);\n"
  11.         ."</script>\n";
  12. }

On to sophisticated AJAX-Magic

There you go. All JavaScript-stuff done, theme-related settings included. So we can head on to the next part our PlugIn will have to do server-sided: Handling the AJAX-request. Here is where to biggest changes happened, because I totally changed the way this worked. The original PlugIn was called instead of WordPress, then imported WordPress-functionality and finally handled the comment-process itself, hardcoded and separated from core WordPress functionality. This is a method I just can’t get along with. Not only does it intefere with all other PlugIns by just stripping them out, but it also makes the PlugIn very unstable due it totally relies on the current state of WordPress.

Integrating

PlugIns should be integrated into, and therefore benefiting of, the core application’s functionality. WordPress offers a very nice way of doing so: Hooks, that is. I already handled the JavaScript-stuff with hooks, so doing the same with the AJAX-functionality seemed like a good and clean idea. After a little research, however, it turned out that action hooks are a kind of unconsistent implemented thing. There are a lot of hooks, granted, but there are also a lot of hooks missing. Especially if it comes to comments, things get kind of weird. When WordPress encounters an error there’s a good chance it will simply call wp_die();, thus killing the app immediately, without any notice to PlugIns. That’s no good. But let’s get back to this later. First the core AJAX-section:

  1. @/wpmu/mu-plugins/ajax-comments.php
  2. if(isset($_POST['ajax-comments-submit'])) {
  3.     add_action('comment_post', 'ajax_comments_send');
  4. }

Update: The PlugIn now takes the whole error-page as a response and then strips out the error-message from there. This way no files need to be modified whatsoever.

The first hook is a native WordPress-hook and calls ajax_comments_send() after a comment is saved to the database. That’s exactly when we want to jump in to cut off the regular application, fetch the last comment and send it back as AJAX-response. Nice. However when I started looking for the hook when a comment-error is encountered I soon realized how loose WordPress’ error-handling is. To be honest I was disappointed, that’s really bad implementation there. Let’s have a look at wp-comments-post.php and see what’s happening if there’s an error:

  1. @/wpmu/wp-comments-post.php
  2. // If the user is logged in
  3. $user = wp_get_current_user();
  4. if ( $user-&gt;ID ) {
  1. @/wpmu/wp-comments-post.php
  2. } else {
  3.     if ( get_option('comment_registration') )
  4.         wp_die( __('Sorry, you must be logged in to post a comment.') );
  5. }

Uhm … great. No error is being generated, no action is being called, no handling of this issue whatsoever occurs. The developer is being left with a silent application kill, that’s just a no-go. If wp_die() would call do_action() at least we could react to this in some way, however we can never be sure what happened, because there’s no error code or anything like this. All we would know is: The application has been terminated. Have fun! However, that seems to be the most we can do without going in for some really crucial changes on WordPress’ core. So I added one line of code that should keep me busy for about 10 hours or so:

Update: The PlugIn now takes the whole error-page as a response and then strips out the error-message from there. This way no files need to be modified whatsoever.

AJAX-Response

Looks easy and makes sense, due the function ajax_comments_send() is hooked in to wp_abort and the $message is the most we can get out of WordPress at the time. I’ll explain the problem with this line at the end, due it’s still unresolved. Let’s have a look at the last function:

  1. @/wpmu/mu-plugins/ajax-comments/functions.php
  2. function ajax_comments_send () {
  3.     $passed  = func_get_arg();
  4.     $comment = @get_comment($passed);

So far, so easy. We fetch the argument, check if it was a valid comment-id, and die() with whatever error-message has been sent by do_action(‘wp_abort’) being identified with a status code 406 header if something went wrong. get us the saved comment’s data and go on:

  1. @/wpmu/mu-plugins/ajax-comments/functions.php
  2.         header('Content-type: text/html; charset=utf-8');
  3.         ob_start();
  4.         $comments = array($comment);
  5.         global $comment;
  6.         include(TEMPLATEPATH.'/comments.php');
  7.         $commentout = ob_get_clean();
  8.  
  9.         preg_match('|(<li.*</li>)|ims', $commentout, $matches);
  10.  
  11.         die($matches[1]);
  12.     }
  13. }

This part is hardly modified from the original. This idea is great and I love the simplicity how the comment can be integrated in the current theme without any problems. What we do is we call the template with a fake list of comments only including the one generated by the AJAX-request. Then we fetch out this one comment from the generated HTML and return it. Lovely! And works perfect. Basically I only changed the regexp to fetch the whole list-item, not to loose any attributes. The alternation will be dealt with via JavaScript.

Update: Due the function is only being called if a comment has successfully been saved we don’t need to implement any kind of error handling here. How useful would the message ‘unknown error’ be anyways?!

Finally: DHTML

This part already was very nice, so I only made a few minor changes. I didn’t like the alerts so I added a function that would display the message inside a div with the id ajax-comments-message, nested in another div and getting class error attached if status code 500 is being returned (wp’s header status for wp_die()). Also, if the result is an error, the actual message has to be fetched from the whole page, which is pretty easy due to very simple markup. A lot of words, let’s just have a look of how this turns out:

  1. <div id="ajax-comments-message" class="error">
  2.     <div>
  3. the message
  4.     </div>
  5. </div>

The outer div is for easy CSS access and may has the class error, the inner div is for DHTML compatibility (guaranteeing a firstChild-node) and contains whatever WordPress returns in $message (at the time either p or ul). Furthermore in the original plugin there was no check if the form is there. So I added an onload-block:

  1. @/wpmu/mu-plugins/ajax-comments/ajax-comments-js
  2. ac_oldLoad = window.onload;
  3. window.onload = function () {
  4.     ac_oldLoad;
  5.     f = ajax_comments_find_element(ajax_comments_form, 'form');
  6.     if (f) {
  7.         f.onsubmit = ajax_comments_submit;
  8.         new Insertion.Bottom(f, '<input id="ajax-comments-submit" name="ajax-comments-submit" type="hidden" value="1" />'); // toggle ajax-catch
  9.     }
  10. };

This ensures that the AJAX-functionality on the server is only activated when the script can find the comment-form, otherwise the behaviour will fall back to default. The rest of the JavaScript is pretty straight forward, so just browse through the file if you want to know more.

A pain in the Source

If you’re still reading you’re probably interested in my problem with the do_action() in wp_die(). This is an interesting story and I’d be glad if you have a look at it in the WordPress Support Forum Thread I opened, hoping for help.

Update: The PlugIn now takes the whole error-page as a response and then strips out the error-message from there. This way I don’t need the do_action() in wp_die() anymore, however it’s interesting why it didn’t work …