TV version (Display Regular Site)

Skip to: Navigation | Content | Sidebar | Footer


Weblog Entry

PHP, CGI, and MT: Together at Last

July 08, 2004

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" /> 
 </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.


Reader Comments

1
MattH says:
July 08, 03h

Man, it’s great to see your research posts back! I was just wondering about this the other day.

Rich says:
July 08, 03h

That’s a lot of work just to use PHP. Maybe it’s time for you to look at WordPress. ;-)

Dave S. says:
July 08, 03h

I’ve tried WordPress. Right around the time I redesigned, in fact. It was pretty obvious after my poking around that customizing it would take just as much work as customizing MT has taken me.

In fact, I haven’t yet seen a WordPress site that has a comment preview page. I haven’t been looking, mind you, but…

July 08, 03h

Yeah! More posts by Dave! I was wondering if you were goinging to add another post today…

July 08, 04h

Sounds like some neat fiddling. I’m sometimes glad I run my own software so I don’t have to muckaround like that. Of course you could look at is as me having to muck around to get anything to work so maybe I’m not better off ;-)

Anyway, to get around to my point: Good idea to echo the POST data if there’s a problem, but I think–and here I could be wrong–that “echo $_POST;” will just print “Array”. I think you need some more complicated mojo to actually print all the data.

Anil Dash says:
July 08, 04h

Nice work! All these docs indicate two things. 1: You’re doing some really innovative stuff that I love seeing and 2: we need to make this stuff easier. :)

Thanks for taking the time to write it up for everyone to benefit.

July 08, 04h

Rory is right. To print the contents of an array in PHP, try print_r($_POST).

http://www.php.net/print_r

Nice work, Dave.

July 08, 04h

Note: You may want to convert the output of print_r to HTML to preserve the indenting, etc. Something like,

echo(str_replace(” “, “&nbsp;”, nl2br(print_r($_POST, true))));

Or, you could just wrap it in <pre>’s, of course.

BTW, Dave, when I posted that last comment, I was taken back to the page for this post, but I was still “within” the preview.php wrapper, which meant none of the post-specific links were correct. For example, the href for the link to view replies was simply http://www.mezzoblue.com/mt/comments/ . I guess you’d need to add some logic to the PHP wrapper to determine if the comment has been posted, and forward to the bare MT page instead. Unfortunately, I haven’t ever used MT, so I’m not sure how the comment posting is done. I imagine you’d just want to do something along the lines of

if(isset($_POST[‘post’])) header(“Location: /path/to/comment/”);

But just thought I’d give you a heads up. I still very much like the idea and implementation.

July 08, 04h

Better yet, you can try Xdbug. I like it, even has timing and performance reviews.

http://www.xdebug.org/

July 08, 04h

Arr, I hate multiple posting, sorry. Just wanted to mention that I made a mistake in my last comment - you should swap the order of the function calls in the example I gave above. Leave nl2br() for last, or else the space in the <br />’s it generates will also be converted to a non-breaking space entity by the str_replace(). So it should look like,

echo(nl2br(str_replace(” “, “&nbsp;”, print_r($_POST, true))));

Phew. Entirely too much commenting for a line of code that should never be reached anyway :)

Matt says:
July 08, 04h

I would recommend PHP’s built-in URL encoding function, which covers many more cases than the one Dan gave you:

http://us4.php.net/urlencode

Dave S. says:
July 08, 06h

“BTW, Dave, when I posted that last comment, I was taken back to the page for this post, but I was still “within” the preview.php wrapper, which meant none of the post-specific links were correct.”

Yeah, that’s another problem I’m still working out – how to redirect the post-submission script back to my custom comments page. There must be a way, just haven’t had the eureka moment yet.

July 08, 10h

Maybe dumb thinking, but you could add a <input typ=”hidden” name=”preview” value=”posted”> to the form in the comments preview template, and if the $_POST[“preview”] value equals “posted” use a location statement as Dan McCormack suggested in comment number 8.

It’s a trick I use a lot for redirects after some form info has been handled and I want to redirect back to another page showing the end result.

I’m not an expert on PHP, I’m more a ‘get it to do what I want’ person, so I cannot determine if there are any drawbacks by using this approach. I never encountered a problem though.

July 08, 11h

Re: Martijn ten Napel’s comment # 13:

“Maybe dumb thinking, but you could add a <input typ=”hidden” name=”preview” value=”posted”> to the form in the comments preview template, and if the $_POST[“preview”] value equals “posted” use a location statement as Dan McCormack suggested in comment number 8.”

But then that would not let you preview it more than once :) There’s a more definite way to determine if the comment ahs been posted. Notice that the code for the preview form contains:

<input type=”submit” id=”preview” name=”preview” value=”Preview Again” />
<input type=”submit” id=”post” name=”post” value=”Post” />

That means that when the user submits the form via the “Preview” or “Preview Again” button, the php script will see a variable named $_POST[‘preview’] with value “Preview” or “Preview Again”, respectively, but $_POST[‘post’] will not be defined (set) unless they submit the form via the Post button. So you can just use isset() to determine which of the variables was set, and react accordingly. I’m pretty sure this statement would do what Dave wants, then:

if(isset($_POST[‘post’])) header(“Location: /path/to/comment/”);

The only part I’m not sure about is how to determine the path to the comment being posted. But if nothing else, you could modify the MT cgi script to include a

or something along those lines. Or there may be some simple way to figure it out directly in the PHP script - I’m not sure how MT works exactly.

jim... says:
July 09, 01h

just a point….

…but i dont want to preview my comment - just post it, so now do I have to click twice?

jim... says:
July 09, 01h

oh yes… i do… :(

preview is very useful when commenting in depth and with formatting that you may want to check, but for a quick ‘text-only’ comment, a post button at the first stage, would be useful as well methinks…

bloody nice coding though ;)

Noah says:
July 09, 04h

The following line seems rather silly:

echo(nl2br(str_replace(â€� “, “ â€�, print_r($_POST, true))));

when (X)HTML which handles this automaticaly! Instead, use the following code:

echo “\n”;
print_r($_POST);
echo “\n\n”;

(Notice the elements to wrap around the )

If you want to get clever about it you could try this:

$arrPost = (count($_POST) > 0) ? $_POST : array();
$arrGet = (count($_GET) > 0) ? $_GET : array();
$arrData = array_merge($arrGet, $arrPost);

if (count($arrData) > 0) {
echo “\n”;
print_r($arrData);
echo “\n\n”;
} else {
echo “No data!”;
}


That code checks and adds the contents of GET and POST to an arbitary array. If anything was found it will be displayed, else and error message is displayed.

You might then want to follow up on this to make the display of the data a little nicer, us of a foreach loop would work well with this. Example:

// Set up a definition list
echo “\n”;

// Loop through data adding definitions
foreach($arrData as $key => $value) {
// $key is the name of the data
// $value is, you guessed it, the value of the data

echo “$key\n”;
echo “$value\n”;
}

// Close definition list
echo “\n”;


P.S. I’m on my lunch break and dont have time to test this code - I’m sure there are a few errors, but you get the idea! :)

Noah says:
July 09, 04h

Ok… the html tags I put in were removed - thus taking out much meaning from my code samples. So here they are again

The following line seems rather silly:

echo(nl2br(str_replace(â€� “, “ â€�, print_r($_POST, true))));

when (X)HTML which handles this automaticaly! Instead, use the following code:

echo “<p><pre>\n�;
print_r($_POST);
echo “</pre></p>\n\n�;

(Notice the <p> elements to wrap around the <pre>)

If you want to get clever about it you could try this:

$arrPost = (count($_POST) > 0) ? $_POST : array();
$arrGet = (count($_GET) > 0) ? $_GET : array();
$arrData = array_merge($arrGet, $arrPost);

if (count($arrData) > 0) {
echo “<p><pre>\n�;
print_r($arrData);
echo “</pre></p>\n\n�;
} else {
echo “<p>No data!</p>�;
}


That code checks and adds the contents of GET and POST to an arbitary array. If anything was found it will be displayed, else and error message is displayed.

You might then want to follow up on this to make the display of the data a little nicer, us of a foreach loop would work well with this. Example:

// Set up a definition list
echo “<dl>\n�;

// Loop through data adding definitions
foreach($arrData as $key => $value) {
// $key is the name of the data
// $value is, you guessed it, the value of the data

echo “<dt><em>$key</em></dl>\n�;
echo “<dd>$value</dd>\n�;
}

// Close definition list
echo “<dl>\n�;


P.S. I’m on my lunch break and dont have time to test this code - I’m sure there are a few errors, but you get the idea! :D

P.P.S. This comment script realy should just replace < with &lt; and > with &gt; - it was a real pain in the bum posting examples with HTML code :S

sosa says:
July 09, 06h

I’ve decided myself for a live comment preview that also supports textile formatting. It’s way less elegant but is simple and kinda cool.

July 09, 06h

Not sure it matters, but your statement that PHP can’t be run as a CGI is false.

#!/path/to/php

will work like any other script.

July 09, 06h

Just a few notes on some previously posted code:

$arrPost = (count($_POST) > 0) ? $_POST : array();
$arrGet = (count($_GET) > 0) ? $_GET : array();

Theres absolutely NO need for that. $_POST and $_GET are always defined as arrays, even if they are empty. You are creating / copying new variables for nothing.
Besides, !== would be much faster than > here

Heres how i would dump post data.

function pretty_print_r($array)
{
  $text = ‘<ul>’;

  foreach($array as $key => $value)
  {
    $text .= ‘<li><strong>’ . htmlspecialchars($key) . ‘</strong>   ’;

    if(is_array($value))
    {
      $text .= pretty_print_r($value);
      }
    else
    {
      $text .= htmlspecialchars($value);
    }

    $text .= ‘</li>’;
  }

  $text .= ‘</ul>’;

  return $text;
}

echo ‘<h2>Post Data</h2>’;
echo count($_POST) === 0 ? ‘No post data.’ : pretty_print_r($_POST);

and same for GET data or COOKIE data, or any other arrays for that matter. I just wrote that on the fly, so i didnt check for bugs heh, but you get the idea of how i do it.

July 09, 06h

eh, ignore the masses of nbsp’s in the previous post. The preview page and the actual comment page displays comments differently.

Anyways, i had something else to say about the bits of code you have in your article dave. You should double check GPC data everytime. As in checking if the variable is set first, then checking that its value is what its supposed to be. And also dealing with magic quotes.

first of all, heres the function i use to deal with magic quotes

function strip_mq($s)
{
return get_magic_quotes_gpc() ? stripslashes($s) : $s;
}

then heres how i grab data coming from the user (same for get and cookie data)

$_POST[‘string_var’] = isset($_POST[‘string_var’]) ? strip_mq(trim($_POST[‘string_var’])) : ”;
$_POST[‘int_var’] = isset($_POST[‘int_var’]) ? intval($_POST[‘int_var’] : 0;

then lets say int_var should not be lesser than 0
if($_POST[‘int_var’] < 0)
{
$_POST[‘int_var’] = 0;
}

With these simple checks, it is much more difficult to hack your script through GPC data.

Heres one last function i use to make strings ready for SQL

function slash_sql($s)
{
return str_replace(‘'’, ‘\'’, str_replace(‘\’, ‘\\’, $s));
}

This one makes it impossible for SQL injection when properly used.

23
steve comrie says:
July 09, 08h

You can actually parse php code from inside a Perl CGI script, including php includes and such.

It usually take a little bit of playing around with, but if you ever need to do it, here’s the basic code:

================================
my $html = “a string containing the html output including php code”;
my $tempdir = ‘/usr/home/sometempdirectory’;
my $uniquefn = time .’-‘. int(rand(10000)) . ‘.php’;

open( FILE, “>$tempdir/$uniquefn” ) || die “write error: $!\n”;
print FILE $html;
close FILE;

$html = `/usr/local/bin/php $tempdir/$uniquefn` || die “parse error: $!\n”;
unlink “$tempdir/$uniquefn”;
================================

And the explanation.

In Moveable type all html code is stored in a variable named $html before it get’s returned to the browser. So starting with that $html variable, we identify a temp directory & a unique temp file name. Then we save the html to the new temp file in that directory. Now we run php from the command line with that temp file as the argument and collect the output back into the $html variable. Now all the php inside $html should have been executed. Then just a bit of clean-up by deleting the temp file when we’re done with it.

If you were doing a lot of this in your perl code, it would probably be best to write a function to handle it that you could just call using parse_my_php($html) or something.

Like I said, takes a little bit of playing around with to get it to work in all cases, but if you’re in a pinch, this might be your best bet.

Simon Cox says:
July 09, 09h

Am I missing something here? I use php includes all the time in MT - no need to flatten a site. I create new MT Template Modules, header footer nav etc, with the PHP I want in them. For example: a php tracking beacon is put in a footer module and then in my main index and other templates I include the MT module:

$MTInclude module=”footer” $

That includes my php snippet or navigation etc into the page.

Or you could just use a php include statement inside the MT include tag.

Its too simple I must have read the article wrong somewhere - back to staples…

Roman says:
July 09, 11h

There’s a bug in your PHP. makeGetRequest() will not url-encode the $variables string.

Your function calls look like:
makeGetRequest($variables);

whereas they should look like:
$variables = makeGetRequest($variables);

Alternatively, you could change makeGetRequest() to take a reference to a string and change it, or just call PHP’s built-in function urlencode() rather than three string substitutions.

Jannis says:
July 10, 09h

You might want to check out www.s9y.org as blogging system :)

Michael says:
July 11, 04h

Comment Preview is certainly a popular topic these days.

WordPress users should visit http://chrisjdavis.org/index/2004/03/15/live-preview-for-comments/463/

TextPattern users have the option of using Textile from http://www.creatimation.net/journal/live-request

28
jeremiah johnson says:
July 26, 09h

why not just enable server side includes and avoid the whole php grafting? perl can do anything PHP can do, but better. You just need to know how to do it.

In this case, I think the best solution isn’t even a Perl or PHP solution, its a solution with the web server. Server-Side Includes is what you want.

29
David says:
July 28, 01h

Hey look at this MT3.1 feature note. “Dynamic PHP publishing, controllable on a per-template basis”

WillB says:
November 18, 03h

Any chance of an update to this post? I’ve been trying to implement this technique on a blog I’m developing, but without much joy.

Using readfile() threw up “failed to open stream: HTTP request failed! HTTP/1.1 400 Bad Request”

Substituting in virtual() and a relative path to mt-comments.cgi got the preview to work, but on hitting the post button I get the comment preview only without the php wrapper.

I know next to nothing about php, so this is probably beyond me anyway.