PHP, CGI, and MT: Together at Last

July 08, 2004 4PM PST

A long-standing problem with Movable Type’s templating system is that the Perl-based .cgi files it relies on don’t allow for the use of PHP. That’s changing though.

The lovely and talented Shaun Inman wrote up his method of using PHP includes to pull in CGI data back in January, which allowed him to pass a query string to the script using HTTP GET. Technical details are at Shaun’s, go read and come back when you’re done.

Using his thinking as a starting point, I spent some time a few weeks back tweaking to solve another Movable Type problem — the comment preview pages that are so often the neglected children of sites like, say, this one.

The problem is that I use PHP to include a standard set of header and footer files across the site; changing one of those files changes the entire site equally. Includes are a beautiful thing, but .cgi files won’t allow you to run PHP. So the only solution I’ve had till now is to flatten a copy of my HTML, reduce it to the bare minimum, and drop the whole thing into my preview template. Which results in parts of the site falling further and further out of synch with the rest as things change.

Out of the box, Movable Type jumps all comment previews and new comment posts to a file called mt-comment.cgi after you hit the submit button, which does the parsing and filtering and spits out a result based on whichever template you have told Movable Type to use. (There are a few possible templates here including those for Comment Preview, Comment Listing, Comment Error, and new in MT3, Comment Pending. Defining a non-default template for each of these is important and I haven’t yet spent the time figuring out how to apply this to Error or Pending.)

Comment Preview is the one we’re trying to hook into. Instead of relying on the .cgi file to render the whole page, instead what I’ve done is strip the template to a bare minimum, and hijack the Individual Entry Archive to point to a different PHP file that includes it.

Chart showing the before and after comment submission processes.

So that’s the summary, but it’s difficult to make sense of that without the examples. First step, having the Individual Entry Archive and Comment Preview pages point to the new PHP file instead of mt-comments.cgi. Look for the line that goes like this:

<form method="post" 
 action="<$MTCGIPath$><$MTCommentScript$>"
 name="comments_form">

And change it to this (replacing the /path/to/preview.php with the path to the .php file on your own server, of course):

<form method="post" 
 action="/path/to/preview.php"
 name="comments_form">

Do that with both templates. Next step is to build the actual PHP file that does the heavy lifting. Since your site will require custom includes and a completely different wrapper than my own, there’s no point in listing the whole file. Instead, here’s the code you’ll need. Drop this first bit in the page header, before anything else happens. (Thanks to Dan Benjamin for help with the Post to Get conversion. All insanely genius type code is his; all mucking-around-without-a-clue type code is mine.)

<?php

function makeGetRequest( $str ) {
 $result = str_replace(':','%3A',$str);
 $result = str_replace('/','%2F',$result);
 $result = str_replace('@','%40',$result);
 return $result;
}

if ($_POST['post']) {
 $variables = 'entry_id=' . $_POST['entry_id'] . '&static=' . $_POST['static'];
 $variables .= '&author=' . urlencode($_POST['author']) . '&email=' . $_POST['email'];
 $variables .= '&url=' . $_POST['url'] . '&text=' . urlencode(stripslashes($_POST['text']));
 $variables .= '&post=post' . '&bakecookie=' . $_POST['bakecookie'];
	
 makeGetRequest($variables);
 readfile("http://www.example.com/path/to/mt-comments.cgi?$variables");

 exit;
}

Drop this second bit later in the page, where you’d normally place the preview text and the comment form.

<?php

if ($_POST) {
 $variables = 'entry_id=' . $_POST['entry_id'] . '&static=' . $_POST['static'];
 $variables .= '&author=' . urlencode($_POST['author']) . '&email=' . $_POST['email'];
 $variables .= '&url=' . $_POST['url'] . '&text=' . urlencode(stripslashes($_POST['text']));
 $variables .= '&preview=Preview+Comment' . '&bakecookie=' . $_POST['bakecookie'];
	
 makeGetRequest($variables);
 readfile("http://www.example.com/path/to/mt-comments.cgi?$variables");
} else {
 echo "
  <p>This is the comment preview form. You’re seeing this message because either something went wrong, or you’ve typed in the URL, or you clicked a link that shouldn’t have been pointing here in the first place.</p>
  <p>Whatever the case, there should be a form here, but there isn’t because of the above reasons. So if you were expecting a comment preview form, <a href=\"/contact/\">let me know that you got this instead</a>. Otherwise, hit the back button and try again. Thanks!</p>
  <hr />
  <p>For the sake of debugging and possibly retrieving your comment, this is the data that was posted to this page:</p>
  "; echo $_POST;
}
?>

The message in the second half is just a bit of defensive design to let the user know there’s a problem if somehow they wind up on the preview page without going through the proper channel. Just in case something goes wrong, I’m dumping the post data as well to allow retrieval, though I can’t foresee much chance of it ever being displayed. Never hurts to be helpful though.

At this point, it should work — but the comment preview page will look a little off. You need to strip everything but the essentials if you’re going to be including the rest of your site structure. This will usually mean taking out everything from the HTML <html> start tag to where the comment preview form begins, and everything after to the </html>. For what it’s worth, my revised, light preview template:

<div class="reply">
 <p class="postedBy"><$MTCommentPreviewAuthorLink show_email="0"$> says:</p>
 <div class="reply-body">
  <$MTCommentPreviewBody$></p>

 </div>
 <p class="posttimestamp">Posted on <$MTCommentPreviewDate$> §</p>
</div>

<hr class="divider" />

<div class="comments-body">
<h2 class="dom">Edit / Post Your Comment:</h2>
 
<form method="post" action="/path/to/preview.php" name="comments_form" onsubmit="if (this.bakecookie.checked) rememberMe(this)">
<div id="replyForm">
 <input type="hidden" id="entry_id" name="entry_id" value="<$MTEntryID$>" />
 <input type="hidden" name="static" value="1" />

 <span><label for="author">Name:</label><input type="text" id="author" name="author" value="<$MTCommentPreviewAuthor encode_html="1"$>" /></span>
 <span><label for="email">Email Address:</label><input type="text" id="email" name="email" value="<$MTCommentPreviewEmail encode_html="1"$>" /></span>
 <span><label for="url">URL:</label><input type="text" id="url" name="url" value="<$MTCommentPreviewURL encode_html="1"$>" /></span>
 <span><label for="text">Comments:</label><textarea id="text" name="text" rows="13" cols="55"><$MTCommentPreviewBody convert_breaks="0" encode_html="1" $></textarea></span>
 <span class="submit">
  <input type="submit" id="preview" name="preview" value="Preview Again" />
  <input type="submit" id="post" name="post" value="Post" />&nbsp;
 </span>
</form>
</div>

<hr class="divider" />

<h2 class="dom">Previous Comments</h2>

<MTComments>
<div class="reply">
 <span class="replynumber"><a href="#c<$MTCommentID pad="3"$>"><$MTCommentOrderNumber$></a></span>
 <p class="postedBy"><$MTCommentAuthorLink show_email="0"$> says:</p>
 <div class="reply-body">
  <MTMacroApply><$MTCommentBody regex="1" smarty_pants="2"$></MTMacroApply>
 </div>
 <p class="posttimestamp">Posted on <$MTCommentDate$> <a href="#c<$MTCommentID pad="3"$>">§</a></p>
</div>
</MTComments>

</div>

It’s not perfect. The conversion dropped out a few of the custom filters I relied on to get rid of things like SmartyPants-generated “smart quotes”, and things are messy when Unicode characters are pasted. But it’s been running for almost a month now and I haven’t received any nasty emails about comment loss, so the basic functionality is there.

As always, share and enjoy.

Addendum: Under some circumstances, every new comment starts being automatically blocked. It appears MT has some form of automatic IP banning. The comment form posts new comments from the IP of the site its running on, instead of the user’s IP, so all it takes is one instance to trigger the banning, and all new comments are denied.

Quick fix — in your weblog’s configuration, hit ‘IP Banning’ and delete the entry for your site. (If you don’t know what it is, you may have to delete them all). Long term fix — still looking for one.