Building A Quick-And-Dirty Guestbook With patGuestbook (part 2)

Learn to customize and secure patGuestbook.

Bells And Whistles

In the first part of this article, I gave you a quick rundown on the patGuestbook application, right from downloading the application to the nitty-gritty of installation, configuration and basic usage. This was followed by an express introduction to guestbook creation and deployment.

Now, in this concluding article, I shall focus on some of the bells and whistles offered by the application to the more enthusiastic developers out there (count me in as a permanent member of this group). Among the items under discussion: controlling the entries listed in the guestbook, customizing the user interface with the included patTemplate class, and protecting access to the application's administration module. Keep reading!

Adopting A Moderate Approach

If you recollect, one of the options available at guestbook creation time was related to guestbook entry moderation. In the first part of this article, I had decided to leave this at its default setting and not moderate the entries for my guestbook; as a result, entries were immediately displayed on the site as soon as they were entered.

However, in the real world, it is often essential to moderate the entries in the guestbook, and thereby control the content displayed to other visitors. And this is where patGuestbook's moderation feature comes in handy.

In order to enable moderation, you need to navigate back to the administration module, and select the "Voice of the People" entry from the drop-down menu at the top of the page. On the resulting information screen, navigate to the "General Settings" section, and check the box for guestbook moderation. Use the "Save Changes" button to record your changes, and the job is done!

Now, whenever a user tries to enter a comment in your guestbook, a message will appear indicating that the entry will be moderated,

The administrator - in other words, you - can then selectively approve or reject each comment via the site administration module. In order to do this, navigate back to the administration module, and select the "Voice of the People" entry from the drop-down menu at the top of the page. On the resulting information screen, navigate to the "General Settings" section and you will be presented with a list of all active and inactive entries.

You can update the status of each entry, and use the "Update Status" command to save your changes; all entries marked as active will not appear in the guestbook.

If Looks Could Kill...

Next up, interface customization. As you may already know, patGuestbook is tightly integrated with a sister project, patTemplate, a powerful PHP-based template engine (if you don't know how patTemplate works, you should read the introductory material at http://www.melonfire.com/community/columns/trog/article.php?id=130 and only then proceed forward with this tutorial). This template engine makes it fairly easy to create your own skins for the patGuestbook interface (and even share them with others, if you so desire).

First things first - where are the templates located? If you recollect, this location was specified as part of the configuration parameters located in the "patGuestbook.php" file in your installation's "config" directory:

<?php

// snip

// Directory where the templates for subscription pages are stored
$skins_dir = "skins";

// snip

?>

Look inside this directory, and you'll see a structure like this:

skins
|
 --------- pat
|      |
|      -------  img
|      |
|          |
|               -------  styles
 --------- textonly
       |
       -------  img
       |
       |
                -------  styles

Do those directory names ring a bell? They should - they're the template names that appear every time you create a new guestbook. So, if you want to create your own set of templates, this is obviously a good way to start.

Now, the patGuestbook application uses three different templates for rendering the user interface:

  1. patGuestbookList.tmpl - this is the template that displays the entries in the guestbook

  2. patGuestbookAdd.tmpl - this is the template which handles adding new entries to the guestbook

  3. patGuestbookDisabled.tmpl - this template simply displays an error message when a particular guestbook is disabled

Let's start with the "patGuestbookList.tmpl" file. To make things easier, I'll give you a quick peek at the desired output before I explain the template's innards to you.

Now, if you take a close look at it, you'll see that this is very similar to the "textonly" template - all I've really done is add a navigation menu to the left side of the page.

I'm going to call my new template "melonfire" (feel free to name your appropriately), and so my first task is to create a directory parallel to the "pat' and "textonly" folders in the "skins" directory. Under this directory, I'll add an "img" directory to store images, and a "styles" directory to store stylesheets.

Next up, the page layout. After much thought and coffee-napkin scrawls, I decided on a simple two-column layout for my guestbook, with the navigation bar in the left column and the main guestbook content in the right one. Here's the basic skeleton:

<pattemplate:tmpl name="page">
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
    <title>{GB_NAME}</title>
    <link rel="stylesheet" href="skins/melonfire/styles/main.css">
</head>
<body bgcolor="#FFFFFF" text="#000000" link="#990000" vlink="#990000" alink="#990000" marginheight="10" marginwidth="10" topmargin="10" rightmargin="10" bottommargin="10" leftmargin="10">
<img src="skins/melonfire/img/px.gif" width="1" height="10" alt="" border="0"><br>
<div align="center">
<table border="0" cellpadding="0" cellspacing="0" width="100%" height="100%">
<tr>
    <td width="25%" align="center">
        <!-- menu comes here -->
        // snip
    </td>
    <td width="75%" align="center">
        <!-- guestbook pages come here -->
        // snip
    </td>
</tr>
</table>
</div>
</body>
</html>
</pattemplate:tmpl>

Since the menu on the left is going to be constant across all pages, it can be hardcoded into the template - here it is:

<!-- menu -->
<table cellSpacing="0" cellPadding="0" border="0">
      <tr>
        <td><a href="http://www.melonfire.com/services/">
        <img alt="Services" src="skins/melonfire/img/mm_svc_n.jpg" align="absMiddle" border="0" width="113" height="26"></a>
        </td>
      </tr>
      <tr>
        <td><a href="http://www.melonfire.com/company/">
        <img alt="The Company" src="skins/melonfire/img/mm_cmp_n.jpg" align="absMiddle" border="0" width="113" height="26"></a></td>
      </tr>
      <tr>
        <td><a href="http://www.melonfire.com/community/">
        <img alt="Community" src="skins/melonfire/img/mm_cmt_n.jpg" align="absMiddle" border="0" width="115" height="28"></a></td>
      </tr>
      <tr>
        <td><a href="http://www.melonfire.com/account/">
        <img alt="Your Account" src="skins/melonfire/img/mm_act_n.jpg" align="absMiddle" border="0" width="113" height="26"></a></td>
      </tr>
      <tr>
        <td>
        <img alt="Contact Us" src="skins/melonfire/img/mm_cnt_n.jpg" align="absMiddle" border="0" width="115" height="28"></td>
      </tr>
    </table>

Of course, since the menu is going to be constant across the pages, you can even abstract it into another template - I leave that to you as an exercise.

Bringing In The Database

At this point, I have identified the layout for the pages, and also shown you the menu that will be displayed on each page. Now for the most important item - connecting all this up to the patGuestbook database.

Here's the code:

<table border="0" cellpadding="0" cellspacing="0">
<tr>
    <td class="textinvert" colspan="3">
        Welcome to {GB_NAME}!<br><br>
    </td>
    </tr>
</table>

<pattemplate:tmpl name="entry">

<pattemplate:tmpl name="displayName" visibility="hidden" varscope="entry" type="simpleCondition" requiredVars="ENTRY_NAME">

<table width="550" cellpadding="0" cellspacing="1" border="0">
    <tr>
        <td>
            <table width="100%" cellpadding="6" cellspacing="0" border="0">
                <tr>
                    <td class="head" >By <b>{ENTRY_NAME}</b> on {ENTRY_DATE}</td>
                </tr>
            </table>
        </td>
    </tr>

</pattemplate:tmpl>

    <tr>
        <td class="text">
            <table border="0" cellpadding="0" cellspacing="0" width="100%">
                <tr>
                    <td>

                    <br>
                        <table border="0" cellpadding="0" cellspacing="2">
                        <pattemplate:tmpl name="displayEmail" visibility="hidden" varscope="entry" type="simpleCondition" requiredVars="ENTRY_EMAIL">
                            <tr valign="top">
                                <td class="text" nowrap><b>{LABEL_EMAIL}</b></td>
                                <td class="text">&nbsp;:&nbsp;</td>
                                <td class="text"><a href="mailto:{ENTRY_EMAIL}">{ENTRY_EMAIL}</a></td>
                            </tr>
                        </pattemplate:tmpl>

                        <pattemplate:tmpl name="displayHomepage" visibility="hidden" varscope="entry" type="simpleCondition" requiredVars="ENTRY_HOMEPAGE">
                            <tr valign="top">
                                <td class="text" nowrap><b>{LABEL_HOMEPAGE}</b></td>
                                <td class="text">&nbsp;:&nbsp;</td>
                                <td class="text"><a href="{ENTRY_HOMEPAGE}" target="_blank">{ENTRY_HOMEPAGE}</a></td>
                            </tr>
                        </pattemplate:tmpl>

                        <pattemplate:tmpl name="displayEntry" visibility="hidden" varscope="entry" type="simpleCondition" requiredVars="ENTRY_ENTRY">
                            <tr>
                                <td colspan="3"><img src="skins/melonfire/img/px.gif" width="1" height="5" alt="" border="0"></td>
                            </tr>
                            <tr valign="top">
                                <td class="text" nowrap><b>{LABEL_ENTRY}</b></td>
                                <td class="text">&nbsp;:&nbsp;</td>
                                <td class="text">{ENTRY_ENTRY}</td>
                            </tr>
                        </pattemplate:tmpl>

                        <pattemplate:tmpl name="ratings" visibility="hidden">
                            <tr>
                                <td colspan="3"><img src="skins/melonfire/img/px.gif" width="1" height="5" alt="" border="0"></td>
                            </tr>
                            <tr valign="top">
                                <td class="text" colspan="3"><b>Ratings</b></td>
                            </tr>
                            <pattemplate:tmpl name="ratingEntry">
                            <tr valign="top">
                                <td class="text" nowrap><b>&nbsp;&middot;&nbsp;{RATING_LABEL}</b></td>
                                <td class="text">&nbsp;:&nbsp;</td>
                                <td class="text">{RATING_VALUE}</td>
                            </tr>
                            </pattemplate:tmpl>
                        </pattemplate:tmpl>
                        </table>
                 <img src="skins/melonfire/img/px.gif" width="1" height="5" alt="" border="0"><br>
                    </td>
                </tr>
            </table>
        </td>
    </tr>
</table>
<img src="skins/melonfire/img/px.gif" width="1" height="20" alt="" border="0"><br>
</pattemplate:tmpl>

<table width="550" cellpadding="1" cellspacing="0" border="0">
    <tr>
        <td>
            <table width="100%" cellpadding="3" cellspacing="0" border="0">
                <tr>
                <pattemplate:tmpl name="previouspage" type="condition" conditionvar="URL_PREVIOUSPAGE">
                    <patTemplate:sub condition="default">
                    <td class="text" nowrap width="33%">
                        <a href="{URL_PREVIOUSPAGE}" style="text-decoration:none"><< previous&nbsp;page</a>
                        <br>
                    </td>
                    </patTemplate:sub>
                    <patTemplate:sub condition="empty">
                    <td width="33%"><img src="skins/melonfire/img/px.gif" width="1" height="1" alt="" border="0"></td>
                    </patTemplate:sub>
                </pattemplate:tmpl>

                    <td align="center" class="text" width="33%"><a href="{URL_ADDENTRY}" style="text-decoration:none">add entry</a></td>

                <pattemplate:tmpl name="nextpage" type="condition" conditionvar="URL_NEXTPAGE">
                    <patTemplate:sub condition="default">
                    <td class="text" align="right" nowrap width="33%">
                        &nbsp;<a href="{URL_NEXTPAGE}" style="text-decoration:none" >next&nbsp;page >></a>
                        <br>
                    </td>
                    </patTemplate:sub>
                    <patTemplate:sub condition="empty">
                    <td width="33%"><img src="skins/melonfire/img/px.gif" width="1" height="1" alt="" border="0"></td>
                    </patTemplate:sub>
                </pattemplate:tmpl>
                </tr>
            </table>
        </td>
    </tr>
</table>

Chaos, you're thinking...and rightly so. But let me help make some sense of it.

  1. First, the page header, displaying the name of the guestbook.
<tr>
<td class="textinvert" colspan="3">
        Welcome to {GB_NAME}!<br><br>
    </td>
</tr>

{GB_NAME} is a special patGuestbook template variable that will be replaced by the name of the guestbook specified at run time - in this example, "Voice of the People".

  1. Next, I have to define the template used for display of each field in the guestbook. In this example, I would like to display the name of the user along with the time at which the entry was saved.
<pattemplate:tmpl name="displayName" visibility="hidden" varscope="entry" type="simpleCondition" requiredVars="ENTRY_NAME">
<table width="550" cellpadding="0" cellspacing="1" border="0">
    <tr>
        <td>
            <table width="100%" cellpadding="6" cellspacing="0" border="0">
                <tr>
                    <td class="head" >By <b>{ENTRY_NAME}</b> on {ENTRY_DATE}</td>
                </tr>
            </table>
        </td>
    </tr>
</pattemplate:tmpl>

Once I am done with the user's name via the {ENTRY_NAME} and {ENTRY_DATE} variables, I can proceed to the user's email address and URL.

<pattemplate:tmpl name="displayEmail" visibility="hidden" varscope="entry" type="simpleCondition" requiredVars="ENTRY_EMAIL">
                            <tr valign="top">
                                <td class="text" nowrap><b>{LABEL_EMAIL}</b></td>
                                <td class="text">&nbsp;:&nbsp;</td>
                                <td class="text"><a href="mailto:{ENTRY_EMAIL}">{ENTRY_EMAIL}</a></td>
                            </tr>
</pattemplate:tmpl>
<pattemplate:tmpl name="displayEmail" visibility="hidden" varscope="entry" type="simpleCondition" requiredVars="ENTRY_EMAIL">
                            <tr valign="top">
                                <td class="text" nowrap><b>{LABEL_EMAIL}</b></td>
                                <td class="text">&nbsp;:&nbsp;</td>
                                <td class="text"><a href="mailto:{ENTRY_EMAIL}">{ENTRY_EMAIL}</a></td>
                            </tr>
                        </pattemplate:tmpl>

                        <pattemplate:tmpl name="displayHomepage" visibility="hidden" varscope="entry" type="simpleCondition" requiredVars="ENTRY_HOMEPAGE">
                            <tr valign="top">
                                <td class="text" nowrap><b>{LABEL_HOMEPAGE}</b></td>
                                <td class="text">&nbsp;:&nbsp;</td>
                                <td class="text"><a href="{ENTRY_HOMEPAGE}" target="_blank">{ENTRY_HOMEPAGE}</a></td>
                            </tr>
</pattemplate:tmpl>

Once again, two special patGuestbook variables -{ENTRY_EMAIL} and {ENTRY_HOMEPAGE} - are used to retrieve the information entered by the user. I can also display the appropriate labels for each field via the {LABEL_EMAIL} and {LABEL_HOMEPAGE} variables.

How about displaying the heart of the guestbook - the user's comments?

<pattemplate:tmpl name="displayEntry" visibility="hidden" varscope="entry" type="simpleCondition" requiredVars="ENTRY_ENTRY">
<tr>
<td colspan="3"><img src="skins/melonfire/img/px.gif" width="1" height="5" alt="" border="0"></td>
</tr>
<tr valign="top">
<td class="text" nowrap><b>{LABEL_ENTRY}</b></td>
                <td class="text">&nbsp;:&nbsp;</td>
                                <td class="text">{ENTRY_ENTRY}</td>
                            </tr>
</pattemplate:tmpl>

Finally, the rating field, which is also fairly straightforward.

<pattemplate:tmpl name="ratings" visibility="hidden">
    <tr>
        <td colspan="3"><img src="skins/melonfire/img/px.gif" width="1" height="5" alt="" border="0"></td>
                </tr>
                <tr valign="top">
                    <td class="text" colspan="3"><b>Ratings</b></td>
                </tr>
                <pattemplate:tmpl name="ratingEntry">
                <tr valign="top">
                    <td class="text" nowrap><b>&nbsp;&middot;&nbsp;{RATING_LABEL}</b></td>
                    <td class="text">&nbsp;:&nbsp;</td>
                    <td class="text">{RATING_VALUE}</td>
                </tr>
    </pattemplate:tmpl>
</pattemplate:tmpl>

One of the configuration variables in the guestbook is the number of entries to be displayed on a single page. So, I also need to add paging logic, and a link to add new entries to the system.

<table width="550" cellpadding="1" cellspacing="0" border="0">
    <tr>
        <td>
            <table width="100%" cellpadding="3" cellspacing="0" border="0">
                <tr>
                <pattemplate:tmpl name="previouspage" type="condition" conditionvar="URL_PREVIOUSPAGE">
                    <patTemplate:sub condition="default">
                    <td class="text" nowrap width="33%">
                        <a href="{URL_PREVIOUSPAGE}" style="text-decoration:none"><< previous&nbsp;page</a>
                        <br>
                    </td>
                    </patTemplate:sub>
                    <patTemplate:sub condition="empty">
                    <td width="33%"><img src="skins/melonfire/img/px.gif" width="1" height="1" alt="" border="0"></td>
                    </patTemplate:sub>
                </pattemplate:tmpl>

                    <td align="center" class="text" width="33%"><a href="{URL_ADDENTRY}" style="text-decoration:none">add entry</a></td>

                <pattemplate:tmpl name="nextpage" type="condition" conditionvar="URL_NEXTPAGE">
                    <patTemplate:sub condition="default">
                    <td class="text" align="right" nowrap width="33%">
                        &nbsp;<a href="{URL_NEXTPAGE}" style="text-decoration:none" >next&nbsp;page >></a>
                        <br>
                    </td>
                    </patTemplate:sub>
                    <patTemplate:sub condition="empty">
                    <td width="33%"><img src="skins/melonfire/img/px.gif" width="1" height="1" alt="" border="0"></td>
                    </patTemplate:sub>
                </pattemplate:tmpl>
                </tr>
            </table>
        </td>
    </tr>
</table>

The {URL_PREVIOUSPAGE} and {URL_NEXTPAGE} variables are used to display the links to the previous and next page, if required. the {URL_ADDENTRY} variable contains the URL that allows users to add a new entry to the guestbook.

A Well-Formed Plan

So that takes care of the main guestbook page - now how about customizing the input form for new entries?

Here's what it should look like,

and here's the code that makes it happen:

<form action="{DISPATCHER}" method="post" name="adder">
<table border="0" cellpadding="0" cellspacing="0" width="100%" height="100%">
    <tr>
        <td align="center">
            <input type="hidden" name="action" value="addEntry">
            <input type="hidden" name="save" value="yes">
            <table width="550" border="0" cellpadding="0" cellspacing="0">
                <tr>
                    <td>

                        <table width="100%" cellpadding="0" cellspacing="0" border="0">
                        <tr valign="top">
                            <td>&nbsp;</td>
                            <td width="100%">
                        <table width="100%" border="0" cellpadding="0" cellspacing="0">             <tr>
                            <td class="textinvert" align="center">Welcome to {GB_NAME}!<br><br>                                                 </td>
                        </tr>
                    </table>
                            </td>
                            <td width="100%" align="right">&nbsp;</td>
                        </tr>
                        </table>
                        <table width="100%" cellpadding="0" cellspacing="0" border="0">
                        <tr>
                            <td >&nbsp;</td>
                            <td width="100%" bgcolor="#FFFFFF" class="text">

                                <!-- message for moderated guestbook -->
                                <pattemplate:tmpl name="moderated" visibility="hidden">
                                Note that this guestbook is moderated, and your entry will only appear in the list of entries once it has been approved by a moderator.<br><br>
                                </pattemplate:tmpl>

                                <!-- errors -->
                                <pattemplate:tmpl name="errors" visibility="hidden">
                                <table width="400" cellpadding="0" cellspacing="0" border="0">
                                <tr>
                                    <td class="text">
                                        Ooops, I've found some errors in your entries... please check them again:<br>

                                        <img src="skins/melonfire/img/px.gif" width="1" height="5" alt="" border="0"><br>

                                        <table width="100%" cellpadding="0" cellspacing="0" border="0">
                                        <pattemplate:tmpl name="errorEntry" type="condition" conditionvar="err_code">

                                        <pattemplate:sub condition="invalid_email">
                                        <tr valign="top">
                                            <td class="text">&#149;&nbsp;</td>
                                            <td class="text">{ERR_FIELD}: The email you entered is of a type unknown to the net as it exists today. Please enter something a little more comprehensible.</td>
                                        </tr>
                                        </pattemplate:sub>

                                        <pattemplate:sub condition="invalid_url">
                                        <tr valign="top">
                                            <td class="text">&#149;&nbsp;</td>
                                            <td class="text">{ERR_FIELD}: The url you entered is of a type unknown to the net as it exists today. Please enter something a little more comprehensible.</td>
                                        </tr>
                                        </pattemplate:sub>

                                        <pattemplate:sub condition="field_required">
                                        <tr valign="top">
                                            <td class="text">&#149;&nbsp;</td>
                                            <td class="text">{ERR_FIELD}: This field is required, but you left it utterly empty. Please feed it something to make it happy :)</td>
                                        </tr>
                                        </pattemplate:sub>

                                        <pattemplate:sub condition="rating_required">
                                        <tr valign="top">
                                            <td class="text">&#149;&nbsp;</td>
                                            <td class="text">{ERR_FIELD}: You've forgotten to give a rating for this item...</td>
                                        </tr>
                                        </pattemplate:sub>

                                        </pattemplate:tmpl>
                                        </table>
                                    </td>
                                </tr>
                                </table>
                                <br>
                                </pattemplate:tmpl>

                                <table border="0" cellpadding="0" cellspacing="2" width="100%">
                                <pattemplate:tmpl name="displayName" visibility="hidden" varscope="page">
                                    <tr>
                                        <td class="text" nowrap><b>{LABEL_NAME}</b></td>
                                        <td class="text">&nbsp;:&nbsp;</td>
                                        <td width="100%"><input type="text" name="data[name]" value="{ENTRY_NAME}" size="30" class="text" style="width:95%"></td>
                                    </tr>
                                </pattemplate:tmpl>

                                <pattemplate:tmpl name="displayEmail" visibility="hidden" varscope="page">
                                    <tr>
                                        <td class="text" nowrap><b>{LABEL_EMAIL}</b></td>
                                        <td class="text">&nbsp;:&nbsp;</td>
                                        <td width="100%"><input type="text" name="data[email]" value="{ENTRY_EMAIL}" size="30" class="text" style="width:95%"></td>
                                    </tr>
                                </pattemplate:tmpl>

                                <pattemplate:tmpl name="displayHomepage" visibility="hidden" varscope="page">
                                    <tr>
                                        <td class="text" nowrap><b>{LABEL_HOMEPAGE}</b></td>
                                        <td class="text">&nbsp;:&nbsp;</td>
                                        <td width="100%"><input type="text" name="data[homepage]" value="{ENTRY_HOMEPAGE}" size="30" class="text" style="width:95%"></td>
                                    </tr>
                                </pattemplate:tmpl>

                                <pattemplate:tmpl name="displayEntry" visibility="hidden" varscope="page">
                                    <tr>
                                        <td colspan="3"><img src="skins/melonfire/img/px.gif" width="1" height="6" alt="" border="0"></td>
                                    </tr>
                                    <tr>
                                        <td class="text" colspan="3"><b>{LABEL_ENTRY}</b></td>
                                    </tr>
                                    <tr>
                                        <td colspan="3"><textarea name="data[entry]" rows="5" cols="50" class="text" style="width:95%">{ENTRY_ENTRY}</textarea></td>
                                    </tr>
                                </pattemplate:tmpl>

                                <pattemplate:tmpl name="ratings" visibility="hidden">
                                    <tr>
                                        <td colspan="3"><img src="skins/melonfire/img/px.gif" width="1" height="6" alt="" border="0"></td>
                                    </tr>
                                    <tr>
                                        <td class="text" colspan="3"><b>{RATINGS}</b></td>
                                    </tr>

                                    <pattemplate:tmpl name="ratingEntry">
                                    <tr>
                                        <td class="text" nowrap><b>{RATING_LABEL}</b></td>
                                        <td class="text">&nbsp;:&nbsp;</td>
                                        <td width="100%">
                                            <select name="data[ratings][]" class="text" size="1" style="width:95%">
                                                <option value="">Please select a rating...</option>
                                                <pattemplate:tmpl name="ratingEntryValue" type="condition" conditionvar="rating_selected">
                                                <pattemplate:sub condition="yes">
                                                <option value="{RATING_VALUE}" selected>{RATING_VALUE}</option>
                                                </pattemplate:sub>
                                                <pattemplate:sub condition="default">
                                                <option value="{RATING_VALUE}">{RATING_VALUE}</option>
                                                </pattemplate:sub>
                                                </pattemplate:tmpl>
                                            </select>
                                        </td>
                                    </tr>
                                    </pattemplate:tmpl>

                                </pattemplate:tmpl>
                                </table>

                            </td>
                            <td >&nbsp;</td>
                        </tr>
                        </table>
                    </td>
                </tr>
            </table>

            <br>

            <table bgcolor="#FFFFFF" width="550" cellpadding="0" cellspacing="0" border="0">
            <tr>
                <td align="center"><input type="submit" value="Submit your entry to our guestbook..." ></td>
            </tr>
            </table>
        </td>
    </tr>
</table>
</form>

Ugly isn't it?

  1. First, the page header, displaying the name of the guestbook.
<table width="100%" border="0" cellpadding="0" cellspacing="0">
<tr>
    <td class="textinvert" align="center">
            Welcome to {GB_NAME}!<br><br>
</td>
    </tr>
</table>
  1. The template that displays a message to the user when moderation follows the header.
<!-- message for moderated guestbook -->
<pattemplate:tmpl name="moderated" visibility="hidden">
Note that this guestbook is moderated, and your entry will only appear in the list of entries once it has been approved by a moderator.<br><br>
</pattemplate:tmpl>
  1. This is followed by a list of error messages, which are displayed when required fields are left empty.
<!-- errors -->
<pattemplate:tmpl name="errors" visibility="hidden">
<table width="400" cellpadding="0" cellspacing="0" border="0">

<tr>
<td class="text">
Ooops, I've found some errors in your entries... please check them again:<br>
<img src="skins/melonfire/img/px.gif" width="1" height="5" alt="" border="0"><br>
<table width="100%" cellpadding="0" cellspacing="0" border="0">

<pattemplate:tmpl name="errorEntry" type="condition" conditionvar="err_code">
<pattemplate:sub condition="invalid_email">
<tr valign="top">
<td class="text">&#149;&nbsp;</td>
<td class="text">{ERR_FIELD}: The email you entered is of a type unknown to the net as it exists today. Please enter something a little more comprehensible.</td>
</tr>
</pattemplate:sub>
<pattemplate:sub condition="invalid_url">

<tr valign="top">
<td class="text">&#149;&nbsp;</td>
<td class="text">{ERR_FIELD}: The url you entered is of a type unknown to the net as it exists today. Please enter something a little more comprehensible.</td>
</tr>
</pattemplate:sub>
<pattemplate:sub condition="field_required">

<tr valign="top">
<td class="text">&#149;&nbsp;</td>
<td class="text">{ERR_FIELD}: This field is required, but you left it utterly empty. Please feed it something to make it happy :)</td>
</tr>
</pattemplate:sub>

<pattemplate:sub condition="rating_required">
<tr valign="top">
<td class="text">&#149;&nbsp;</td>
<td class="text">{ERR_FIELD}: You've forgotten to give a rating for this item...</td>
</tr>
</pattemplate:sub>
</pattemplate:tmpl>

</table>
</td>
</tr>
</table>
<br>
</pattemplate:tmpl>

Feel free to edit the error messages above to reflect the personality and style of your site.

  1. Finally, the meat of the template - the form that is displayed to the user. As usual, there are pre-defined patGuestbook templates that I can work with for this section. Remember to be careful when tweaking these templates (unless, of course, you're comfortable with patTemplate, in which case, tweak away!).
<table border="0" cellpadding="0" cellspacing="2" width="100%">

<pattemplate:tmpl name="displayName" visibility="hidden" varscope="page">
<tr>
<td class="text" nowrap><b>{LABEL_NAME}</b></td>
<td class="text">&nbsp;:&nbsp;</td>
<td width="100%"><input type="text" name="data[name]" value="{ENTRY_NAME}" size="30" class="text" style="width:95%"></td>
</tr>
</pattemplate:tmpl>

<pattemplate:tmpl name="displayEmail" visibility="hidden" varscope="page">
<tr>
<td class="text" nowrap><b>{LABEL_EMAIL}</b></td>
<td class="text">&nbsp;:&nbsp;</td>
<td width="100%"><input type="text" name="data[email]" value="{ENTRY_EMAIL}" size="30" class="text" style="width:95%"></td>
</tr>
</pattemplate:tmpl>

<pattemplate:tmpl name="displayHomepage" visibility="hidden" varscope="page">
<tr>
<td class="text" nowrap><b>{LABEL_HOMEPAGE}</b></td>
<td class="text">&nbsp;:&nbsp;</td>
<td width="100%"><input type="text" name="data[homepage]" value="{ENTRY_HOMEPAGE}" size="30" class="text" style="width:95%"></td>
</tr>
</pattemplate:tmpl>

<pattemplate:tmpl name="displayEntry" visibility="hidden" varscope="page">
<tr>
<td colspan="3"><img src="skins/melonfire/img/px.gif" width="1" height="6" alt="" border="0"></td>
</tr>
<tr>
<td class="text" colspan="3"><b>{LABEL_ENTRY}</b></td>
</tr>
<tr>
<td colspan="3"><textarea name="data[entry]" rows="5" cols="50" class="text" style="width:95%">{ENTRY_ENTRY}</textarea></td>
</tr>
</pattemplate:tmpl>

<pattemplate:tmpl name="ratings" visibility="hidden">
<tr>
<td colspan="3"><img src="skins/melonfire/img/px.gif" width="1" height="6" alt="" border="0"></td>
</tr>
<tr>
<td class="text" colspan="3"><b>{RATINGS}</b></td>
</tr>

<pattemplate:tmpl name="ratingEntry">
<tr>
<td class="text" nowrap><b>{RATING_LABEL}</b></td>
<td class="text">&nbsp;:&nbsp;</td>
<td width="100%">
<select name="data[ratings][]" class="text" size="1" style="width:95%">
<option value="">Please select a rating...</option>

<pattemplate:tmpl name="ratingEntryValue" type="condition" conditionvar="rating_selected">
<pattemplate:sub condition="yes">
<option value="{RATING_VALUE}" selected>{RATING_VALUE}</option>
</pattemplate:sub>
<pattemplate:sub condition="default">
<option value="{RATING_VALUE}">{RATING_VALUE}</option>
</pattemplate:sub>
</pattemplate:tmpl>
</select>
</td>
</tr>
</pattemplate:tmpl>

</pattemplate:tmpl>
</table>

For each field in the guestbook, I have two tags - one displaying the label and the other displaying the form field to the user. For example, for the user's name, I've used the {LABEL_NAME} variable for the label and the {ENTRY_NAME} variable for the text box that is displayed to the user.

When Things Go Wrong

Finally, patGuestbook includes a template to display an error message to the user when a particular guestbook has been specifically disabled.

    <p>&nbsp;</p>
    <p>&nbsp;</p>
<form action="{DISPATCHER}" method="post" name="adder">
<input type="hidden" name="action" value="addEntry">
<input type="hidden" name="save" value="yes">
<img src="skins/melonfire/img/px.gif" width="1" height="30" alt="" border="0"><br>
<div align="center">
<table width="550" cellpadding="0" cellspacing="0" border="0">
    <tr>
        <td width="1" ><img src="skins/melonfire/img/px.gif" width="1" height="1" alt="" border="0"></td>
        <td width="8"><img src="skins/melonfire/img/px.gif" width="8" height="1" alt="" border="0"></td>
        <td width="532" class="text">
            <img src="skins/melonfire/img/px.gif" width="532" height="20" alt="" border="0"><br>

            Houston, we have a problem...<br>But, the <span class="head">{GB_NAME}</span> will be back soon...

            <img src="skins/melonfire/img/px.gif" width="1" height="20" alt="" border="0"><br>
        </td>
        <td width="8"><img src="skins/melonfire/img/px.gif" width="8" height="1" alt="" border="0"></td>
        <td width="1" ><img src="skins/melonfire/img/px.gif" width="1" height="1" alt="" border="0"></td>
    </tr>
</table>
</div>
</form>

Pretty simple, this - plain ol' HTML, no fancy-shmancy gimmicks or convoluted variables. In order to see what it looks like, turn off a guestbook from the administration module and try accessing it - you should see something like this:

That's about it for the user interface templates that can be customized. If you thought that was easy and you're hankering for another challenge, you can always try customizing the administration module as well (alternatively, you could get up from your computer and go get yourself a life).

Locking It Down

If there is one drawback to the patGuestbook application, it is the lack of security for the administration module. By default, patGuestbook leaves the entire administration section totally unprotected and open to malicious attacks. If you're using the Apache Web server (you probably are), you can access the server's authentication features to add basic security to this section.

In order to illustrate how this works, let's consider a simple example. Let's assume the existence of the following directory structure:

/usr/local/apache/htdocs/patGuestbook/
                       example.php
                             /admin/
                           guestbook.php

Now, let's suppose that I want to protect the directory "admin". It's fairly simple to do with HTTP authentication.

The first step is to ensure that your Apache build includes support for the "mod_auth" module. You can check this by executing the Apache binary with the "-l" command-line option.

$ /usr/local/apache/bin/httpd -l
Compiled-in modules:
  http_core.c
  mod_env.c
  mod_log_config.c
  mod_mime.c
  mod_negotiation.c
  mod_status.c
  mod_include.c
  mod_autoindex.c
  mod_dir.c
  mod_cgi.c
  mod_asis.c
  mod_imap.c
  mod_actions.c
  mod_userdir.c
  mod_alias.c
  mod_access.c
  mod_auth.c
  mod_setenvif.c
  mod_php4.c

If you don't see "mod_auth" in the list, you'll need to recompile Apache with support for that module.

Next, check Apache's configuration file, "httpd.conf", and ensure that the option

AllowOverride All

is present in the section for the server document root. This allows you to override global server settings via per-directory ".htaccess" control files.

Next, create a file named ".htaccess" in the "admin" directory, and put the following lines into it:

AuthType Basic
AuthUserFile /usr/local/apache/users
AuthName "patGuestbook Administration Module"
Require valid-user

This tells the server that access to the "admin" directory (the directory in which the ".htaccess" file is located) is to be controlled, and access is to be granted to users based on the username/password information in the file "/usr/local/apache/users"

The final step is to create the "users" file. Change to the "/usr/local/apache" directory (or whichever directory you've decided to store the user data in) and use the "htpasswd" command:

$ htpasswd -c users john
New password: ****
Re-type new password: ****
Adding password for user john

You can add more users to this file if you like (remember to omit the "-c" parameter for all subsequent additions, as that parameter creates a brand-new, empty file).

Remember not to store the "users" file in a directory under the server document root, or else malicious users will be able to view and download the password database through a browser.

Now, attempt to access the "admin" directory via your Web browser. The browser should pop up a dialog box and prompt you for a username and password. Access to the "admin" directory will be granted only if you enter a correct username and password, as defined in the "users" file.

Note that this is very primitive authentication, and can substantially add to the load on your Web server if it involves a large number of users. For a more comprehensive solution, take a look at http://www.melonfire.com/community/columns/trog/article.php?id=115

Over And Out

And that's about all we have time for. In this two-part article, I introduced you to patGuestbook, a PHP application that makes setting up a guestbook on your site as easy as clicking your way through a series of menus. I showed you how to create a new guestbook, configure required and optional fields, and explore rating possibilities in your guestbook. I also showed you how to moderate entries as they are added, customize the user interface via the patTemplate engine, and protect unauthorized access to your guestbook with simple HTTP authentication.

In case you'd like to learn more about the topics discussed in this tutorial, take a look at the following links:

The official patGuestbook Web site, at http://www.php-tools.de/

Template-Based Web Development With patTemplate, at http://www.melonfire.com/community/columns/trog/article.php?id=130

User Authentication With Apache And PHP, at http://www.melonfire.com/community/columns/trog/article.php?id=115

Until next time...stay healthy!

Note: All examples have been tested on Linux/i586 with Apache 1.3.28, PHP 4.2 and patGuestbook 1.0. Examples are illustrative only, and are not meant for a production environment. Melonfire provides no warranties or support for the source code described in this article. YMMV!

This article was first published on 28 Feb 2003.