Chapter 10

Extending Your Server with Extensions


In this chapter, we increase the scope of your Web server with two ISAPI extensions. These are URLJump, which redirects Web client browsers to another page, and ISAPI Guestbook. We build these extensions from the ground up.

By now you are familiar with the Microsoft Foundation Classes (MFC) implementation of ISAPI, with open database connectivity (ODBC), and with the basics of the common gateway interface (CGI) and hypertext transfer protocol (HTTP). In this chapter, we highlight those aspects of ISAPI development as we build our ISAPI extensions.

URLJump

Have you ever been to a Web site and wondered how they created those nifty drop-down list boxes that send you to another URL? If you thought it was magic, you'll soon find out that it's simple.

We create these list boxes by sending the client a redirect message. The HTTP 1.1 draft defines a redirect as follows:

A redirect tells the browser the page has moved. Although most popular Web browsers automatically request the new URL, some legacy browsers do not.

This means that we need to send a redirect message to the client- and we need to give a Hypertext Markup Language (HTML) body a link to the final page. If we don't send an HTML body, a visitor with an older browser will be left staring at a blank page.

Now that we know what the code does, let's write it:

  1. Open Microsoft Developer Studio.
  2. Create a new project workspace.
  3. Specify ISAPI Extension Wizard as the type and URLJump as the name.
  4. Check that a Server Extension object is selected by default.
  5. Change the Extension Class Name to CURLJump, click Finish, and click OK.

AppWizard inserts seven files into your project. The files are listed in Table 10.1.

Table 10.1 Files Generated by AppWizard

File Name

Description

URLJUMP.H

Header file for CURLJump

URLJUMP.CPP

Implementation file for CURLJump

URLJUMP.DEF

Exports definition file for the extension

URLJUMP.RC

Resource file for the extension

STDAFX.H

Standard Application Framework header file for MFC extensions

STDAFX.CPP

Simply #includes STDAFX.H

URLJUMP.PCH

The extension's precompiled header

Constants

URLJump uses only one constant: the virtual path of your script folder, shown in Listing 10.1. If your path is different, you need to change this value.

Listing 10.1 URLJUMP.CPP-Constant Used in URLJump

/*

server specific

THIS VALUE SHOULD BE CHANGED TO REFLECT YOUR SERVER'S SETUP

*/

static const TCHAR SCRIPTPATH[] = _T("/scripts/");

Parse Map

Since URLJump is an MFC ISAPI extension, we use a parse map. In URLJUMP.CPP, edit the default parse map so it matches this one, as shown in Listing 10.2.

Listing 10.2 URLJUMP.CPP-URLJump's Parse Map

BEGIN_PARSE_MAP(CURLJump, CHttpServer)

ON_PARSE_COMMAND(Test, CURLJump, ITS_EMPTY)

ON_PARSE_COMMAND(Default, CURLJump, ITS_PSTR)

ON_PARSE_COMMAND_PARAMS("URL=default")

DEFAULT_PARSE_COMMAND(Default, CURLJump)

END_PARSE_MAP(CURLJump)

As you can see, URLJump accepts two commands, Test and Default. The first function we code is Default(), as shown in Listing 10.3.

Listing 10.3 URLJUMP.CPP-CURLJump's Default Command Handler

void CURLJump::Default(CHttpServerContext* pCtxt, LPTSTR pURL)

{

StartContent(pCtxt);

WriteTitle(pCtxt);

DWORD dwBuf = sizeof(pURL);

/*

Send the redirect message

*/

if(!pCtxt->ServerSupportFunction(HSE_REQ_SEND_URL_REDIRECT_RESP, pURL, &dwBuf, NULL))

{

*pCtxt << "<center>Redirect message could not be sent</center>";

return;

}

/*

this code should execute only if the user's browser doesn't

support automatic redirection

*/

*pCtxt << "<center>Your browser does not support automatic redirection</center>";

*pCtxt << "<br>";

*pCtxt << "<center>To upgrade, click <a href=\"http://www.microsoft.com/ie/ie.htm\">here</a></center>";

*pCtxt << "<br>";

*pCtxt << "<center>To go to the anticipated URL, click <a href=" << pURL << ">here</a></center>";

EndContent(pCtxt);

}

The line statement

pCtxt->ServerSupportFunction(HSE_REQ_SEND_URL_REDIRECT_RESP, pURL, &dwBuf, NULL)

sends a 302 Moved Temporarily redirect message to the client. If the client does not accept automatic redirection, it can upgrade to Microsoft Internet Explorer (IE) and get a link to the intended page.

If for you need to send a different redirect status code, such as 301 Moved Permanently or 303 See Other, you can do this by manipulating the header information before it's sent to the client.


Now let's override WriteTitle() so it fits our extension, just in case we have a visitor with an older browser. To do this, open ClassWizard and scroll down using the Messages list box until you see WriteTitle.

Click WriteTitle, click Add Function, and click Edit Code. Edit WriteTitle() so it looks like the code in Listing 10.4.

Listing 10.4 URLJUMP.CPP-Overridden WriteTitle Function

void CURLJump::WriteTitle(CHttpServerContext* pCtxt) const

{

*pCtxt << "<title>SE Using ISAPI</title>";

}

Testing the Redirection

URLJump includes a second command handler, Test. This makes it easy for others to put the dynamic-link library (DLL) on their server and make sure it's working properly. Open URLJUMP.CPP in your editor and add the Test() function after Default().

Listing 10.5 URLJUMP.CPP-CURLJump's Test Command Handler

void CURLJump::Test(CHttpServerContext* pCtxt)

{

StartContent(pCtxt);

WriteTitle(pCtxt);

/*

HTML for the test page

*/

*pCtxt << "<body bgcolor=\"#ffffff\" test=\"#000000\">\n";

*pCtxt << "<font face=Arial>\n";

*pCtxt << "<font size=+2><b>URLJump Test Page</b></font><br><br>\n";

*pCtxt << "<center>\n";

*pCtxt << "This page tests our URLJump ISAPI Extension<br>\n";

*pCtxt << "View this page's source code to see how URLJump is used\n<br><br><br>\n\n";

*pCtxt << "<!--Your form action statement should look like this. Be sure to use the POST method or you'll get an error-->\n";

*pCtxt << "<form action=\"" << SCRIPTPATH << "urljump.dll\" method=post>\n";

*pCtxt << "<select name=\"URL\">\n\n";

*pCtxt << "<!--Notice how the URL is set as the value of each option-->\n";

*pCtxt << "<!--For the extension to work properly, you must follow this example-->\n";

*pCtxt << "<option value=\"http://rampages.onramp.net/~steveg/isapi.html\">The ISAPI Developer's Site\n";

*pCtxt << "<option value=\"http://rampages.onramp.net/~steveg/isapifaq.html\">ISAPI Frequently Asked Questions\n";

*pCtxt << "<option value=\"http://microsoft.ease.lsoft.com/archives/isapi.html\">ISAPI Issues Mailing List Archive\n";

*pCtxt << "<option value=\"http://rampages.onramp.net/~steveg/iis.html\">IIS Frequently Asked Questions\n\n";

*pCtxt << "<!--Notice how the next value is set to the script's URL. This let's us section off our drop down list box to avoid an error-->\n";

*pCtxt << "<option value=\"" << SCRIPTPATH << "urljump.dll?Test\">===== NEWS LINKS =====\n\n";

*pCtxt << "<option value=\"http://www.cnn.com/\">CNN Online\n";

*pCtxt << "<option value=\"http://www.usatoday.com/\">USA Today\n";

*pCtxt << "<option value=\"http://www.cbs.com/news/\">CBS News\n";

*pCtxt << "</select>\n";

*pCtxt << "<br><br><br><br>\n";

*pCtxt << "<input type=submit value=\"Test It\">\n";

*pCtxt << "<input type=reset value=\"Reset\">\n";

*pCtxt << "</form>\n";

*pCtxt << "</center>\n";

*pCtxt << "</font>\n";

*pCtxt << "</body>\n";

EndContent(pCtxt);

}

Test() just sends a "quicky" Web page. This allows the Webmaster to install the script and verify that it's working. It's one less file to distribute.

For those of you who are new to CGI, some potential problems are lurking. Carefully examining the Test() command handler will reveal them.

Using URLJump

Since this section deals mainly with calling the DLL from a Web page, I include only the dynamically generated HTML.

The easiest way to use URLJump is to create an HTML-based form with one select element, one submit button, and one reset button. Name the select element URL because this is what Default() expects.

For each option on the list, specify the URL to which you want the client directed. Immediately following the OPTION tag, enter the characters you want displayed in the list box. Here's a simple example:

<form action="/scripts/urljump.dll?" method=post>

<select name="URL">

<option value=" http://www.mcp.com/que/">QUE Publishing Home Page

</select>

</form>

The rest is covered in CGI, HTTP, and HTML references. Some of the ones I use are shown in Table 10.2.

Table 10.2 CGI, HTTP, and HTML References

Title

Author(s)

ISBN/Location

SE Using CGI

Jeffry Dwight and Michael Erwin

0-7897-0740-3

HTML Quick Reference

Robert Mullen

0-7897-0867-1

Webmaster's Professional Ref

Loren Buhle, Mark Pesce, Vinay Kumar, et al.

1-56205-473-2

Hyper Text Transfer Protocol

R. Fielding, J. Gettys, J. C. Mogul, H. Frystyk, T. Berners-Lee

http://www.w3.org/pub/WWW/Protocols/


Now on to our Guestbook.

ISAPI Guestbook

I know-just what the Web needs-yet another guest book. In my defense, a guest book seems like the perfect example. The meaning of a guest book is universal. No matter what the context, a guest book in its simplest sense is a storage facility that allows visitors to leave their mark.

Furthermore, it's easy to create and doesn't need complex queries or coding. Our guest book only has eight fields and fits nicely into a single table.

I admit that using Microsoft Structured Query Language (SQL) Server 6.x for such a task is like using a sledge hammer on a push pin. But at this point, that's probably not such a bad idea.

Our guest book is designed to be a simple extension that fills a real-world need. Ok, maybe not exactly a need, but you get the idea.

When we finish our extension, you'll have a fully functioning guest book that visitors to your site can sign. And you'll know how to build your own ODBC-enabled ISAPI extensions, with or without MFC.

If you decide to use this guest book on your Web site, you may want to think about removing the code that allows others to edit and delete guest-book entries.

Before You Begin

Although you can configure the data source to work with another SQL database server, the following instructions are for Microsoft SQL Server.

In Microsoft SQL Enterprise Manager, create a new database named Guestbook.

  1. Close Enterprise Manager and open the script in Microsoft ISQL/W.
  2. Select the Guestbook database from the DB drop-down box.
  3. Open the GUESTBOOK.SQL file on the companion CD to this book.
  4. Execute the query to create the Guestbook table.

Before visitors to your Web site can access this data source, you'll need to designate it as a system data source (SDN). To make this an SDN, do the following.

  1. From Control Panel, select the ODBC32 applet and click System DSN.
  2. When the System Data Sources dialog box appears, click Add.
  3. Select the appropriate ODBC driver for your database server and click Setup.
  4. Specify Guestbook as the Data Source Name and database.
  5. Close the System Data Source dialog box and the Data Sources dialog box.

Figure 10.1 shows the System Data Sources dialog box in Windows NT Server 3.51.

Fig. 10.1

System Data Sources dialog in Windows NT Server 3.51.

The System DSN specifies data sources that are local to a computer versus dedicated to a user. Any user with the right privileges can use a System DSN. So any ODBC ISAPI extension you create should use a System Data Source Name (DSN).

We use MFC's ISAPI CHttpServer and CHttpServerContext classes to create the ISAPI Guestbook. To use this code for the Guestbook, you need Microsoft Visual (MSVC) C++ 4.2 or higher.

Although MSVC 4.1 allows you to create ISAPI extensions, the database CDatabase and CRecordset classes are not thread-safe. Therefore, you should not use them in a multithreaded environment unless you apply some form of thread synchronization.

This is usually done by putting susceptible code in a critical section. For more information on critical sections, see Chapter 18, "Making Your Extensions Thread-Safe," or see Microsoft Visual C++ Books Online.

As an alternative, you can code the ISAPI extension using MFC. But use the ODBC API directly instead of CDatabase and CRecordset. (Chapter 18 also covers multithreading.) In addition, I've found the references in Table 10.3 handy.

Table 10.3 References on MultiThreading

Title

Author

ISBN

The Revolutionary Guide to MFC4

Mike Blasczak

1-874416-92-3

Advanced Windows

Jeffrey Richter

1-55615-677-4

Let's get started.

You can use Microsoft's data access objects (DAO) in CGI scripts built as console applications. But trying to use them in an ISAPI DLL can be fatal. The reason for this goes beyond normal thread safety. For more information, see Technical Note 67 in the Microsoft Knowledge Base.

Open Developer Studio and create a new project workspace with ISAPI Extension Wizard as the type and Guestbook as the name (see Figure 10.2).

Fig. 10.2

Developer Studio's New Project Workspace dialog box.

Generate a Server Extension object should be selected by default. Change the Extension Class Name to CGuestbook, click Finish, and click OK to have AppWizard create your ISAPI skeleton project (see Figure 10.3).

Fig. 10.3

Developer Studio's ISAPI Extension Wizard

Remember from Chapter 9, "Building Extensions with MFC," that you have seven files added to your project. These files are listed in Table 10.4.

Table 10.4 Files Generated by AppWizard

Filename

Description

GUESTBOOK.H

Header file for CGuestbook

GUESTBOOK.CPP

Implementation file for CGuestbook

GUESTBOOK.DEF

Exports definition file for the extension

GUESTBOOK.RC

Resource file for the extension

STDAFX.H

Standard Application Framework header file for MFC extensions

STDAFX.CPP

Simply #includes STDAFX.H

GUESTBOOK.PCH

The extension's precompiled header

AppWizard does not automatically include the header file you need for database applications when building an MFC ISAPI extension. So we have to manually enter the file name. Open STDAFX.H and add the following line to the end:

#include <afxdb.h>

Now that our extension has access to MFC's database classes, let's add the data source and the CGuestbookQuery class. CGuestbookQuery will be the interface to our database.

To add CGuestbookQuery, click View, ClassWizard. This opens the MFC ClassWizard (see Figure 10.4). Click Add Class and click New.


Fig. 10.4

ClassWizard

The Create New Class Dialog box is open (see Figure 10.5). In the Name edit box, type CGuestbookQuery From the drop-down box, select Crecordset, and click Create.

Fig. 10.5

Create New Class dialog box.

The Database Options dialog box appears, as shown in Figure 10.6.


Fig. 10.6

Database Options dialog box.

Select Guestbook as the data source and Dynaset as the Recordset type. Select Bind all columns in Advanced and click OK.

Depending on how your system is set up, you may be prompted to log into SQL Server. If so, enter your login ID and password, if any. Click OK and select dbo.Guestbook from Select Database Tables (see Figure 10.7). Click OK.

Fig. 10.7

Select Database Tables dialog box.

Click OK to close ClassWizard. Notice the File View in your Project Workspace, shown in Figure 10.8).

Fig. 10.8

Project Workspace File view.

Two files were added to your project, listed in Table 10.5.

Table 10.5 Files Added by ClassWizard

File Name

Description

GUESTBOOKQUERY.H

Header file for CGuestbookQuery

GUESTBOOKQUERY.CPP

Implementation file for CGuestbookQuery

Even though ClassWizard was nice enough to add the files to your project, for your extension to have access to CGuestbookQuery, we need to add an entry to your CGuestbook implementation file. Open GUESTBOOK.CPP and add

#include "GuestbookQuery.h"

after the line following the last #include directive.

Virtually all ODBC ISAPI extensions you create will follow our steps up to this point. Now we need to create the code for the ISAPI Guestbook extension.

Constants

We'll need several items in our extension, such as the server's Base Reference and the ISAPI scripts folder. We add these values after the last #include directive in Listing 10.6.

Listing 10.6 GUESTBOOK.CPP-Constants Used in ISAPI Guestbook

#define NULLSTRING ""

#define MAX_BUFFER 256 /* maximum buffer size */

// protocol

static const LPTSTR HTTP_USER_AGENT = _T("HTTP_USER_AGENT");

static const TCHAR HTTP_PROTOCOL[] = _T("http://");

// database

static const TCHAR CONNECTSTRING[] = _T("ODBC;UID=sa;PWD=;");

static const TCHAR GUESTBOOKDB[] = _T("Guestbook"); // DSN

// helper

static const TCHAR HTML[] = _T("HTML");

// server specific

/* THESE VALUES SHOULD BE CHANGED TO REFLECT YOUR SERVER SETUP */

static const TCHAR BASEHREF[] = _T("http://test.midnite.net/");

static const TCHAR SCRIPTPATH[] = _T("/scripts/");

The first two lines are directives to the compiler telling it to put "" and 256 wherever NULLSTRING and MAX_BUFFER occur, respectively. NULLSTRING helps us test whether or not a user entered a value in a necessary field. MAX_BUFFER is used to set the size of the buffer into which the user's browser is inserted.

The line

static const LPTSTR HTTP_USER_AGENT = _T("HTTP_USER_AGENT");

is used with CHttpServerContext::GetServerVariable() to detect the user's browser. Not all browsers accept this variable.

Next, the line

static const TCHAR HTTP_PROTOCOL[] = _T("http://");

identifies the HTTP protocol. This value is used to check whether or not a user has entered a home page in their Guestbook entry.

This line

static const TCHAR CONNECTSTRING[] = _T("ODBC;UID=sa;PWD=;");

holds the text sent to SQL Server when the data source was opened and the line

static const TCHAR GUESTBOOKDB[] = _T("Guestbook"); // DSN

names Guestbook as the data source. The line

static const TCHAR HTML[] = _T("HTML");

is used with the LoadLongResource() function to identify Web pages bound to the ISAPI DLL as custom resources.

The last two constants

static const TCHAR BASEHREF[] = _T("http://test.midnite.net/");

static const TCHAR SCRIPTPATH[] = _T("/scripts/");

are server-specific and should be changed to reflect your server's setup before you build and use ISAPI Guestbook. BASEHREF specifies the server's root folder and SCRIPTPATH is the folder holding the Guestbook DLL. Setting these constants allows others to easily reuse this extension.

Parse Map

Recall from Chapter 9, "Building Extensions with MFC," that the MFC ISAPI extensions use parse maps. The parse map for ISAPI Guestbook is shown in Listing 10.7.

Listing 10.7 GUESTBOOK.CPP-CGuestbook's Parse Map

BEGIN_PARSE_MAP(CGuestbook, CHttpServer)

ON_PARSE_COMMAND(Add, CGuestbook, ITS_EMPTY)

ON_PARSE_COMMAND(Find, CGuestbook, ITS_PSTR)

ON_PARSE_COMMAND_PARAMS("Command=Details")

ON_PARSE_COMMAND(Details, CGuestbook, ITS_PSTR)

ON_PARSE_COMMAND_PARAMS("Name")

ON_PARSE_COMMAND(Delete, CGuestbook, ITS_PSTR)

ON_PARSE_COMMAND_PARAMS("Name=default")

ON_PARSE_COMMAND(Edit, CGuestbook, ITS_PSTR)

ON_PARSE_COMMAND_PARAMS("Name")

ON_PARSE_COMMAND(WriteGuestbookEntry, CGuestbook, ITS_PSTR ITS_PSTR

ITS_PSTR ITS_PSTR

ITS_PSTR ITS_PSTR

ITS_PSTR ITS_PSTR)

ON_PARSE_COMMAND_PARAMS("Name=default Browser=default Email=default "

"HomePage=default City=default State=default "

"Country=default Comments=default")

ON_PARSE_COMMAND(FindGuestbookEntry, CGuestbook, ITS_PSTR ITS_PSTR

ITS_PSTR ITS_PSTR

ITS_PSTR ITS_PSTR

ITS_PSTR ITS_PSTR)

ON_PARSE_COMMAND_PARAMS("Name=default Browser=default Email=default "

"HomePage=default City=default State=default "

"Country=default Command=default")

ON_PARSE_COMMAND(EditGuestbookEntry, CGuestbook, ITS_PSTR ITS_PSTR

ITS_PSTR ITS_PSTR

ITS_PSTR ITS_PSTR

ITS_PSTR ITS_PSTR)

ON_PARSE_COMMAND_PARAMS("Name=default Browser=default Email=default "

"HomePage=default City=default State=default "

"Country=default Comments=default")

ON_PARSE_COMMAND(Default, CGuestbook, ITS_EMPTY)

DEFAULT_PARSE_COMMAND(Default, CGuestbook)

END_PARSE_MAP(CGuestbook)

When you create parse maps and SQL queries that span multiple lines, make sure you include spaces in the right places. If you don't, your extension will wander off into la-la land and when you find the bug, you might just inflict bodily harm on yourself or others.

As you can see, Guestbook accepts nine commands: Add, Delete, Find, Details, Edit, WriteGuestbookEntry, FindGuestbookEntry, EditGuestbookEntry, and Default. Open GUESTBOOK.CPP and edit your parse map so it is the same as the one Listing 10.7, shown above.

The Default Function

The Default command corresponds with the Default function.

Listing 10.8 GUESTBOOK.CPP-CGuestbook's Default Command Handler

void CGuestbook::Default(CHttpServerContext* pCtxt)

{

StartContent(pCtxt);

WriteTitle(pCtxt);

CString strOutput;

CString strTemp;

CString strTest;

// try to load the html document

if(LoadLongResource(strTemp, IDS_HTML_MAIN))

strOutput.Format(strTemp, BASEHREF, /* server base url*/

SCRIPTPATH, /* script folder */

SCRIPTPATH, /* script folder */

SCRIPTPATH, /* script folder */

SCRIPTPATH);/* script folder */

else

strOutput.Format(IDS_ERROR, "Default",/* function */

"LoadLongResource"); /* operation */

/* send the result */

*pCtxt << strOutput;

EndContent(pCtxt);

}

As you saw in Chapter 9, "Building Extensions with MFC," all MFC ISAPI commands take a pointer to a CHttpServerContext as their first parameter. In Default(), this is the only parameter because this function just sends the main menu of the script.

Default() makes a call to the LoadLongResource() function to copy into a CString object an HTML document bound to the DLL as a resource. This function was included as part of Microsoft's WWWQuote example and is very handy (see Listing 10.9).

Listing 10.9 GUESTBOOK.CPP-LoadLongResource from Microsoft's WWWQuote Sample ISAPI Extension

/*

LoadLongResource()

taken from Microsoft's WWWQuote sample

*/

BOOL CGuestbook::LoadLongResource(CString& str, UINT nID)

{

HRSRC hRes;

HINSTANCE hInst = AfxGetResourceHandle();

BOOL bResult = FALSE;

hRes = FindResource(hInst, MAKEINTRESOURCE(nID), HTML);

if (hRes == NULL)

ISAPITRACE1("Error: Resource %d could not be found\r\n", nID);

else

{

DWORD dwSize = SizeofResource(hInst, hRes);

if (dwSize == 0)

{

str.Empty();

bResult = TRUE;

}

else

{

LPTSTR pszStorage = str.GetBufferSetLength(dwSize);

HGLOBAL hGlob = LoadResource(hInst, hRes);

if (hGlob != NULL)

{

LPVOID lpData = LockResource(hGlob);

if (lpData != NULL)

{

memcpy(pszStorage, lpData, dwSize);

bResult = TRUE;

}

FreeResource(hGlob);

}

}

}

#ifdef _DEBUG

if (!bResult)

str.Format(_T("<b>Could not find string %d</b>"), nID);

#endif

return bResult;

}

Now we need to override the WriteTitle() function. Open ClassWizard and activate the Message Maps tab. Under the Messages list box, scroll down until you see WriteTitle.

Click WriteTitle, click Add Function, and click Edit Code. Edit WriteTitle so it looks like the code in Listing 10.10.

Listing 10.10 GUESTBOOK.CPP-CGuestbook's WriteTitle Function

/*

WriteTitle()

send the page title

*/

void CGuestbook::WriteTitle(CHttpServerContext* pCtxt) const

{

CString strOutput;

CString strTitle;

strTitle.LoadString(IDS_TITLE);

strOutput.Format("<title>%s</title>", strTitle);

*pCtxt << strOutput;

}

Be sure to remove the line

CHttpServer::WriteTitle(CHttpServerContext* pCtxt);

If you don't, your title won't appear. To add the right string resource, click Insert, Resource and double-click String Resource. The identifier for your title, as you can see in Listing 10.10 above, is IDS_TITLE and it should be ISAPI Guestbook.

Adding a Guestbook Entry

The next function we enter is Add. Like Default(), the only parameter it takes is a pointer to a CHttpServerContext object. The Add function is shown in Listing 10.11.

Listing 10.11 GUESTBOOK.CPP-CGuestbook's Add Command Handler

/*

Add()

this command loads the form that allows the user to

enter their information and attempts to detect their browser

*/

void CGuestbook::Add(CHttpServerContext* pCtxt)

{

StartContent(pCtxt);

WriteTitle(pCtxt);

CString strTemp;

CString strOutput;

CString strBrowser = GetHttpVariable(pCtxt, HTTP_USER_AGENT);

/* try to load the html document */

if(LoadLongResource(strTemp, IDS_HTML_ADD))

strOutput.Format(strTemp, BASEHREF,/* server root */

SCRIPTPATH, /* script folder */

strBrowser); /* user's browser */

else

strOutput.Format(IDS_ERROR, "Add",/* function */

"LoadLongResource");/* operation */

/* send the result */

*pCtxt << strOutput;

EndContent(pCtxt);

}

Add() does a little more than Default(). It also tries to detect the user's browser by calling the GetHttpVariable()function, as shown in Listing 10.12.

Listing 10.12 GUESTBOOK.CPP-GetHttpVariable Tries to Retrieve a Server Variable

/*

GetHttpVariable()

attempts to retrieve a server variable

*/

CString CGuestbook::GetHttpVariable(CHttpServerContext* pCtxt, LPTSTR pstrVar)

{

LPTSTR pstrBuf[MAX_BUFFER];

CString strVar;

DWORD dwSize;

UINT nRetCode;

nRetCode = pCtxt->GetServerVariable(pstrVar, pstrBuf, &dwSize);

switch(nRetCode)

{

case ERROR_INVALID_PARAMETER:

return "Bad connection handle";

break;

case ERROR_INVALID_INDEX:

return "Bad or unsupported variable identifier";

break;

case ERROR_INSUFFICIENT_BUFFER:

return "Buffer too small";

break;

case ERROR_MORE_DATA:

return "Buffer too small, only part of data returned. "

"The total size of the data is not returned.";

break;

case ERROR_NO_DATA:

return "The data requested is not available.";

break;

}

strVar = (LPCTSTR)pstrBuf;

return strVar;

}

GetHttpVariable() takes two parameters, a pointer to a CHttpServerContext object and a pointer to a null-terminated Windows or Unicode character string (LPTSTR). The return value is a CString object holding either the server variable or an error message.

Since Add() and GetHttpVariable() are custom functions, we need to add them to the declaration of CGuestbook. While we're here, we should also add LoadLongResource(). Open GUESTBOOK.H and add the following lines to the public: section of the class declaration:

// ISAPI Commands

void Add(CHttpServerContext* pCtxt);

// Helpers

BOOL LoadLongResource(CString &str, UINT nID);

CString GetHttpVariable(CHttpServerContext* pCtxt, LPTSTR pstrVar);

The corresponding function to Add() is WriteGuestbookEntry(), shown in Listing 10.13.

Listing 10.13 GUESTBOOK.CPP-CGuestbook's WriteGuestbookEntry Command Handler

/*

WriteGuestbookEntry()

writes an new entry into the guestbook

*/

void CGuestbook::WriteGuestbookEntry(CHttpServerContext* pCtxt,

LPTSTR pstrName, /*user's name*/

LPTSTR pstrBrowser, /*user's browser*/

LPTSTR pstrEmail, /*user's e-mail address*/

LPTSTR pstrHomePage, /*user's home page url */

LPTSTR pstrCity, /*user's city*/

LPTSTR pstrState, /*user's state or province */

LPTSTR pstrCountry, /*user's country*/

LPTSTR pstrComments) /*user's comments*/

{

StartContent(pCtxt);

WriteTitle(pCtxt);

CString strQuery;

CString strOutput;

CString strHeader;

CDatabase db;

strHeader.Format(IDS_STDHEADER, BASEHREF);

*pCtxt << strHeader;

/* make sure the required fields have values */

if(!strcmp(pstrName,NULLSTRING) || !strcmp(pstrCity,NULLSTRING) ||

!strcmp(pstrState,NULLSTRING) || !strcmp(pstrCountry,NULLSTRING))

{

*pCtxt << "<br><center>"

<< "Please be certain to enter your Name, City,"

<< " State and Country. Thank you."

<< "</center>";

return;

}

if(!strcmp(pstrHomePage,HTTP_PROTOCOL))

pstrHomePage = NULLSTRING;

/* format the query */

strQuery.Format("Name = '%s'", pstrName);

/* try to open the database, exit if it can't be opened */

if(!db.Open(GUESTBOOKDB, /*lpszDSN*/

FALSE, /*bExclusive*/

FALSE, /*bReadOnly*/

CONNECTSTRING, /*lpszConnect*/

FALSE)) /*bUseCursorLib*/

{

*pCtxt << "Could not open database";

return;

}

CGuestbookQuery rsGuestbook(&db);

/* assign the query to the recordset */

rsGuestbook.m_strFilter = strQuery;

/* try to open the recordset, if it can't be opened catch

the exception and show the user */

try

{

if(rsGuestbook.Open())

{

/* if the record is already on file,

this should return FALSE, then

notify the user */

if(!rsGuestbook.IsBOF())

strOutput.Format(IDS_ONFILE, pstrName, SCRIPTPATH);

else

{

rsGuestbook.AddNew();

/* assign the values */

rsGuestbook.m_Name = pstrName; /*user's name*/

rsGuestbook.m_Browser = pstrBrowser; /*user's browser*/

rsGuestbook.m_Email = pstrEmail;/*user's e-mail address*/

rsGuestbook.m_HomePage = pstrHomePage;/*user's home page*/

rsGuestbook.m_City = pstrCity; /* user's city*/

rsGuestbook.m_State = pstrState; /*user's state */

rsGuestbook.m_Country = pstrCountry; /*user's country*/

rsGuestbook.m_Comments = pstrComments; /*user's comments*/

/* now try to add the record to the database */

if(rsGuestbook.Update())

strOutput.Format(IDS_THANKYOU, pstrName, SCRIPTPATH);

else

strOutput.Format(IDS_NOUPDATE, SCRIPTPATH);

}

}

else

/* wonder what happened here??? */

strOutput.Format(IDS_UNKNOWN, SCRIPTPATH);

}

catch(CDBException* pEX)

{

TCHAR szError[1024];

/* get the error message */

if(pEX->GetErrorMessage(szError, sizeof(szError)))

strOutput.Format(IDS_EXCEPTION, szError, SCRIPTPATH);

else

strOutput.Format(IDS_UNKNOWN, SCRIPTPATH);

}

/* close the recordset and database */

rsGuestbook.Close();

db.Close();

/* send the result information to the user */

*pCtxt << strOutput;

EndContent(pCtxt);

}

Since WriteGuestbookEntry() is the first function we've written that manipulates our database, let's walk through it step by step:

strHeader.Format(IDS_STDHEADER, BASEHREF);

*pCtxt << strHeader;

loads the top of our Web page from a resource string, then inserts the BASEHREF constant, which holds the server's root, into the proper location. The HTML is shown in Listing 10.14.

Listing 10.14 GUESTBOOK.RC-IDS_STDHEADER

<base href="%s">

<body bgcolor="#ffffff" text="#000000">

<font face=Arial>

<center>

<img src="gb.gif" width=550 height=75 units=pixels>

</center>

Next, we compare the necessary parameters with NULLSTRING to make sure the values have been entered. If one or more of these variables are missing, we send a message to the user and exit the function.

Notice that in this instance, the NOT (!) operator is needed to test strcmp() for a TRUE value. This is because it returns "0" if the strings are identical.

/* make sure the required fields have values */

if(!strcmp(pstrName,NULLSTRING) || !strcmp(pstrCity,NULLSTRING) ||

!strcmp(pstrState,NULLSTRING) || !strcmp(pstrCountry,NULLSTRING))

{

*pCtxt << "<br><center>"

<< "Please be certain to enter your Name, City,"

<< " State and Country. Thank you."

<< "</center>";

return;

}

Now we check to see if the user entered a home page. We do this by comparing the pstrHomePage variable with the HTTP_PROTOCOL constant since the HTML form we sent in the Add() function places the http:// value in the Home Page field:

if(!strcmp(pstrHomePage,HTTP_PROTOCOL))

pstrHomePage = NULLSTRING;

Before we add a new record, we should be sure that the new record won't try to duplicate an existing Primary Key. We do this with a query that filters the records. This line formats the query; the query isn't executed until our CGuestbookQuery object is opened:

/* format the query */

strQuery.Format("Name = '%s'", pstrName);

Now we try to open the database. If it can't be opened, we send a message to the user and exit the function:

/* try to open the database, exit if it can't be opened */

if(!db.Open(GUESTBOOKDB, /* lpszDSN */

FALSE, /* bExclusive */

FALSE, /* bReadOnly */

CONNECTSTRING, /* lpszConnect */

FALSE)) /* bUseCursorLib */

{

*pCtxt << "Could not open database";

return;

}

Now for the heart of WriteGuestbookEntry(): the database code.

First, our CGuestbookQuery object is created and is passed as a pointer to our recently opened CDatabase object:

CGuestbookQuery rsGuestbook(&db);

The CGuestbookQuery data member used for filtering records, m_strFilter, is assigned the query string we formatted earlier:

/* assign the query to the recordset */

rsGuestbook.m_strFilter = strQuery;

Now we try to open the CGuestbookQuery object. Notice the try and catch blocks surrounding the object's function calls. Since we're using MFC for the database code, if we don't supply exception handling, any exceptions that occur will be caught by MFC.

This will cause the extension to be terminated by the AfxTerminate function. As a result, the user will have no idea what happened because the browser will either be totally blank or it will display an error message that the user will have trouble interpreting.

So we incorporate exception handling into our database code. Then if an exception occurs, the catch block retrieves the appropriate error message (the error message associated with the exception) and displays it to the user.

As you can see, exceptions need very little extra code and can be extremely helpful for debugging not only an ISAPI extension but just about any C++ program:

/* try to open the recordset, if it can't be opened catch

the exception and show the user */

try

{

When CRecordset::Open() is called, the query we built at the beginning of this function is executed:

if(rsGuestbook.Open())

{

Now we test for the beginning of the file using the CRecordset::IsBOF() function. If this function returns TRUE, the Primary Key resulting from adding this record is not a duplicate. So a matching record is not already on file.

Because of this, our first test is for a return value of FALSE, which would tell us the record is already on file. The Unsigned Integer (UINT) IDS_ONFILE value identifies the resource in our string table that the extension will display if the record is already on file. Note that if we don't do this test, an exception will result.

/* if the record is already on file,

this should return FALSE, then

notify the user */

if(!rsGuestbook.IsBOF())

strOutput.Format(IDS_ONFILE, pstrName, SCRIPTPATH);

Since we know this is a new record, we can call the CRecordset::AddNew() function to prepare the table for a new record. When CRecordset::AddNew() is called, an empty record is created.

But our changes are not saved until we call CRecordset::Update(). If we were to scroll to another record between the CRecordset::AddNew() and CRecordset::Update()calls, all our changes would be lost without notification!

Because of this, make sure that any functions used between CRecordset::AddNew() and CRecordset::Update() do not change your position in the table you are working with. Failure to do so will result in a bug that's extremely difficult to track down.

else

{

rsGuestbook.AddNew();

Now we assign values to the data members of CGuestbookQuery. The MFC framework uses a method called record field exchange (RFX). When CRecordset::AddNew() or CRecordset::Edit() are called, RFX stores the edit buffer so that it can be restored if necessary.

For CRecordset::AddNew(), the field data members are marked as empty. When Update() is called, RFX checks for changed fields, builds an SQL INSERT statement for CRecordset::AddNew() or a CRecordset::UPDATE statement for Edit().

The SQL statement built by RFX is sent to the data source. For CRecordset::AddNew(), the edit buffer is restored. If the operation was CRecordset::Edit(), the backed-up values are deleted.

/* assign the values */

rsGuestbook.m_Name = pstrName; /* user's name */

rsGuestbook.m_Browser = pstrBrowser; /* user's browser */

rsGuestbook.m_Email = pstrEmail; /*user's e-mail address */

rsGuestbook.m_HomePage = pstrHomePage; /* user's home page */

rsGuestbook.m_City = pstrCity; /*user's city */

rsGuestbook.m_State = pstrState; /* user's state */

rsGuestbook.m_Country = pstrCountry; /* user's country */

rsGuestbook.m_Comments = pstrComments; /* user's comments */

/* now try to add the record to the database */

if(rsGuestbook.Update())

strOutput.Format(IDS_THANKYOU, pstrName, SCRIPTPATH);

else

strOutput.Format(IDS_NOUPDATE, SCRIPTPATH);

}

}

else

/* wonder what happened here??? */

strOutput.Format(IDS_UNKNOWN, SCRIPTPATH);

}

catch(CDBException* pEX)

{

TCHAR szError[1024];

/* get the error message */

if(pEX->GetErrorMessage(szError, sizeof(szError)))

strOutput.Format(IDS_EXCEPTION, szError, SCRIPTPATH);

else

strOutput.Format(IDS_UNKNOWN, SCRIPTPATH);

}

Now let's put the declaration of WriteGuestbookEntry() under the ISAPI Commands section of GUESTBOOK.H:

// ISAPI Commands

void Add(CHttpServerContext* pCtxt);

void WriteGuestbookEntry(CHttpServerContext* pCtxt,

LPTSTR pstrName=NULL,

LPTSTR pstrBrowser=NULL,

LPTSTR pstrEmail=NULL,

LPTSTR pstrHomePage=NULL,

LPTSTR pstrCity=NULL,

LPTSTR pstrState=NULL,

LPTSTR pstrCountry=NULL,

LPTSTR pstrComments=NULL);

We complete this section with the HTML form that allows our visitors to enter their information. To import this into our project, click Insert and Resource on the Developer Studio menu bar. When the Insert Resource dialog box appears, click Import (see Figure 10.9).


Fig. 10.9

Insert Resource dialog box.

Change File of type to All Files(*.*)and change to the directory holding the HTML files. Click the ADD.HTM file and click Import. When the Custom Resource Type dialog box appears (see Figure 10.10), specify HTML as the Resource Type.

Fig. 10.10

Custom Resource Type dialog box.

After the file is imported, change the Resource ID to IDS_HTML_ADD. The four other HTML resources you should add to your project are listed in Table 10.6.

Table 10.6 HTML Resources Used in ISAPI Guestbook

Resource ID

File Name

IDS_HTML_EDIT

EDIT.HTM

IDS_HTML_DETAILS

DETAILS.HTM

IDS_HTML_FIND

FIND.HTM

IDS_HTML_MAIN

MAIN.HTM

ISAPI Guestbook also uses several String Table resources, listed in Table 10.7.

Table 10.7 String Table Resources Used by ISAPI Guestbook

Resource ID

Caption

IDS_TITLE

ISAPI Guestbook

IDS_TABLEHEAD

<table width=100%><tr><td align=center><b><u>Name</u></b></td><td align=center><b><u>E-Mail Address</u></b></td><td align=center><b><u>Home Page</u></b></td><td></td></tr>

IDS_TABLEFOOT

</table>

IDS_TABLEROW

<tr><td align=left>%s</td><td align=center>%s</td><td align=left>%s</td><td align=center><form action="%sguestbook.dll?" method=post><input type=hidden name=MfcISAPICommand value="%s"><input type=hidden name="Name" value="%s"><input type=submit value="%s"></form></td></tr>

IDS_MAILTO

<a href="mailto:%s">%s</a>

IDS_HOMEPAGE

<a href="%s">%s</a>

IDS_ERROR

<body bgcolor="#ffffff" text="#000000">\n<font face=Arial>\n<center>\n<img src="gb.gif" width=550 height=75 units=pixels></center><br><center><font size=+1 color="#ff0000">Error</font><br>The operation was aborted<br>Function: <b> %s</b><br>Operation: <b>%s</b></center>

IDS_STDHEADER

<base href="%s">\n<body bgcolor="#ffffff" text="#000000">\n<font face=Arial>\n<center>\n<img src="gb.gif" width=550 height=75 units=pixels>\n</center>

IDS_NOMATCH

<center><b>No matching records were found.</b></center><br><center><b><a href=%sguestbook.dll?>Return to the Main Menu</a></b></center>

IDS_THANKYOU

<center><b>Thank you for signing our guestbook, %s</b></center><br><center><b><a href=%sguestbook.dll?>Return to the Main Menu</a></b></center>

IDS_NOUPDATE

<center><b>Could not update record</b>\n<br>Data may not have changed</center><br><center><b><a href=%sguestbook.dll?>Return to the Main Menu</a></b></center>

IDS_ONFILE

<center><b>%s is already on file</b>\n<br>Try Edit instead of Add</center><br><center><b><a href=%sguestbook.dll?>Return to the Main Menu</a></b></center>

IDS_REMOVED

<center><b>%s was removed from our guestbook</b></center><br><center><b><a href=%sguestbook.dll?>Return to the Main Menu</a></b></center>

IDS_UNKNOWN

<center><b>Query failed for an unknown reason</b></center><br><center><b><a href=%sguestbook.dll?>Return to the Main Menu</a></b></center>

IDS_DBERROR

<center><b>Could not open database</b></center><br><center><b><a href=%sguestbook.dll?>Return to the Main Menu</a></b></center>

IDS_EXCEPTION

<center><b>%s</b></center><br><center><b><a href=%sguestbook.dll?>Return to the Main Menu</a></b></center>

Figure 10.11 shows the custom resource properties.

Fig. 10.11

Custom resource properties.

Now that our visitors can add entries to the Guestbook, we'll create the code that allows other visitors to find the entries.

Finding Guestbook Entries

The Find() function takes two parameters: a pointer to a CHttpServerContext object and an LPTSTR named pstrCommand. Much like the Default() and Add() command handlers, Find()sends an HTML document (see Listing 10.15). In this case, the document allows the user to enter search parameters to find entries in our Guestbook.

Listing 10.15 GUESTBOOK.CPP-CGuestbook's Find Command Handler

/*

Find()

sends the form that allows the user to search the guestbook

*/

void CGuestbook::Find(CHttpServerContext* pCtxt, LPTSTR pstrCommand)

{

StartContent(pCtxt);

WriteTitle(pCtxt);

CString strTemp;

CString strOutput;

// try to load the html document

if(LoadLongResource(strTemp, IDS_HTML_FIND))

strOutput.Format(strTemp, BASEHREF, /* server root */

SCRIPTPATH, /* script folder */

pstrCommand);/* command for find */

else

strOutput.Format(IDS_ERROR, "Find", /*function*/

"LoadLongResource");/*operation*/

// send the result

*pCtxt << strOutput;

EndContent(pCtxt);

}

Other functions, such as Edit(), Delete(), and Details(), need to find individual entries. So whenever Find() is called, the second parameter, lpstrCommand, is used to hold the resulting command. If Find() is called without the lpstrCommand specified, the Details() command is used.

Details() is covered in the next section. Supplementing Find() is FindGuestBookEntry(), shown in Listing 10.16.

Listing 10.16 GUESTBOOK.CPP-CGuestbook's FindGuestbookEntry Command Handler

/*

FindGuestbookEntry()

locates an entry in the guestbook

*/

void CGuestbook::FindGuestbookEntry(CHttpServerContext* pCtxt,

LPTSTR pstrName, /* name param */

LPTSTR pstrBrowser, /* browser param */

LPTSTR pstrEmail, /* e-mail param */

LPTSTR pstrHomePage, /* home page param */

LPTSTR pstrCity, /* city param */

LPTSTR pstrState, /* state param */

LPTSTR pstrCountry, /* country param */

LPTSTR pstrCommand)

{

AddHeader(pCtxt, "Pragma: no-cache\r\n");

StartContent(pCtxt);

WriteTitle(pCtxt);

CString strQuery;

CString strForm;

CString strOutput;

CDatabase db;

strQuery = BuildQuery(pstrName, /* name param */

pstrBrowser, /*browser param */

pstrEmail, /*e-mail param */

pstrHomePage, /*home page param */

pstrCity, /* city param */

pstrState, /* state param */

pstrCountry); /* country param */

/* try to open the database, exit if it can't be opened */

if(!db.Open(GUESTBOOKDB, /* lpszDSN */

FALSE, /* bExclusive */

FALSE, /* bReadOnly */

CONNECTSTRING, /* lpszConnect */

FALSE)) /* bUseCursorLib */

{

*pCtxt << "Could not open database";

return;

}

CGuestbookQuery rsGuestbook(&db);

rsGuestbook.m_strFilter = strQuery;

strForm.Format(IDS_STDHEADER, BASEHREF);

*pCtxt << strForm;

try

{

if(rsGuestbook.Open())

{

if(rsGuestbook.IsBOF())

strOutput.Format(IDS_NOMATCH, SCRIPTPATH);

else

{

CString strRows;

CString strTableHead;

CString strTableFoot;

CString strTableRow;

CString strEntry;

strTableHead.LoadString(IDS_TABLEHEAD);

strTableFoot.LoadString(IDS_TABLEFOOT);

strTableRow.LoadString(IDS_TABLEROW);

// create the table

*pCtxt << strTableHead;

/* Load records here */

while(!rsGuestbook.IsEOF())

{

strEntry.Format(strTableRow,

rsGuestbook.m_Name,

rsGuestbook.m_Email,

rsGuestbook.m_HomePage,

SCRIPTPATH,

pstrCommand,

rsGuestbook.m_Name,

pstrCommand);

*pCtxt << strEntry;

strEntry.Empty();

rsGuestbook.MoveNext();

}

*pCtxt << strTableFoot;

}

}

else

strOutput.Format(IDS_UNKNOWN, SCRIPTPATH);

}

catch(CDBException* pEX)

{

TCHAR szError[1024];

if(pEX->GetErrorMessage(szError, sizeof(szError)))

strOutput.Format(IDS_EXCEPTION, szError, SCRIPTPATH);

else

strOutput.Format(IDS_UNKNOWN, SCRIPTPATH);

}

*pCtxt << strOutput;

rsGuestbook.Close();

db.Close();

EndContent(pCtxt);

}

Although FindGuestbookEntry() resembles WriteGuestbookEntry(), it's basically different. The most obvious difference is that it doesn't write a new record to our table. Instead, it finds existing entries. A more subtle difference is in the very first line:

AddHeader(pCtxt, "Pragma: no-cache\r\n");

This line instructs the browser not to cache this page. By doing this, we make sure that each time visitors look for an entry, they get the most current listing.

Another difference between FindGuestbookEntry() and WriteGuestbookEntry() is the way we build the query executed by our CGuestbookQuery object when Open() is called. In this case, our query is built by another function, BuildQuery(), shown in Listing 10.17.

Listing 10.17 GUESTBOOK.CPP-BuildQuery Formats the SQL Query for FindGuestbookEntry

/*

BuildQuery()

create an sql query based on the user's input

*/

CString CGuestbook::BuildQuery(LPTSTR pstrName, LPTSTR pstrBrowser,

LPTSTR pstrEmail, LPTSTR pstrHomePage,

LPTSTR pstrCity, LPTSTR pstrState,

LPTSTR pstrCountry)

{

CString strQuery;

if(strcmp(pstrName, NULLSTRING))

strQuery.Format("Name LIKE '%%%s%%'", pstrName);

if(strcmp(pstrBrowser, NULLSTRING))

{

if(!strQuery.IsEmpty())

strQuery += " AND ";

strQuery.Format(strQuery + "Browser LIKE '%%%s%%'", pstrBrowser);

}

if(strcmp(pstrEmail, NULLSTRING))

{

if(!strQuery.IsEmpty())

strQuery += " AND ";

strQuery.Format(strQuery + "Email LIKE '%%%s%%'", pstrEmail);

}

if(strcmp(pstrHomePage, NULLSTRING))

{

if(!strQuery.IsEmpty())

strQuery += " AND ";

strQuery.Format(strQuery + "HomePage LIKE '%%%s%%'", pstrHomePage);

}

if(strcmp(pstrCity, NULLSTRING))

{

if(!strQuery.IsEmpty())

strQuery += " AND ";

strQuery.Format(strQuery + "City LIKE '%%%s%%'", pstrCity);

}

if(strcmp(pstrState, NULLSTRING))

{

if(!strQuery.IsEmpty())

strQuery += " AND ";

strQuery.Format(strQuery + "State LIKE '%%%s%%'", pstrState);

}

if(strcmp(pstrCountry, NULLSTRING))

{

if(!strQuery.IsEmpty())

strQuery += " AND ";

strQuery.Format(strQuery + "Country LIKE '%%%s%%'", pstrCountry);

}

return strQuery;

}

BuildQuery() is called immediately after WriteTitle(). It takes seven LPTSTRs: pstrName, pstrBrowser, pstrEmail, pstrHomePage, pstrCity, pstrState, and pstrCountry.

BuildQuery() compares each parameter to the NULLSTRING constant. If each value is empty, so is the query. If any or all variables hold values, BuildQuery() puts them in the right locations and returns a CString object.

FindGuestbookEntry() takes our query and it becomes the m_strFilter data member of our CGuestbookQuery object.

When our record set is opened, we need to check for records matching the visitor's parameters. Since our m_strFilter member is in place, this is done when we call CRecordset::Open():

if(rsGuestbook.Open())

{

if(rsGuestbook.IsBOF())

strOutput.Format(IDS_NOMATCH, SCRIPTPATH);


An empty m_strFilter member causes all the records in the record set to be returned. With very large record sets, you may want to check m_strFilter for an empty value. Since this is a CString object, it's easily done:

/*

make sure the user entered search parameters

*/

if(m_strFilter.IsEmpty())

{

// send a message and don't execute the query

}

else

{

// execute the query

}

If there are matching records, we need to load the HTML that displays the list of records. We do this by breaking down an HTML Table into three parts: a header, a row, and a footer:

else

{

CString strTableHead;

CString strTableFoot;

CString strTableRow;

CString strEntry;

strTableHead.LoadString(IDS_TABLEHEAD);

strTableFoot.LoadString(IDS_TABLEFOOT);

strTableRow.LoadString(IDS_TABLEROW);

Our table's header is sent first, which allows each record to fall into the same HTML table:

// create the table

*pCtxt << strTableHead;

This while loop scrolls through the resulting record set, record by record, sending each record to the user's browser as a row in the HTML table we created. After each record is sent, the CString strEntry object is emptied. Then we move to the next record until we reach the end of the record set:

/* Load records here */

while(!rsGuestbook.IsEOF())

{

strEntry.Format(strTableRow,

rsGuestbook.m_Name,

rsGuestbook.m_Email,

rsGuestbook.m_HomePage,

SCRIPTPATH,

pstrCommand,

rsGuestbook.m_Name,

pstrCommand);

*pCtxt << strEntry;

strEntry.Empty();

rsGuestbook.MoveNext();

}

Once each record is sent to the user, we send the table footer so our table is formatted properly:

*pCtxt << strTableFoot;

}

Now we add our newly created functions to the CGuestbook header file. Under the ISAPI Commands section, add the declarations for Find() and FindGuestbookEntry():

void Find(CHttpServerContext* pCtxt, LPTSTR pstrCommand="Details");

void FindGuestbookEntry(CHttpServerContext* pCtxt,

LPTSTR pstrName=NULL,

LPTSTR pstrBrowser=NULL,

LPTSTR pstrEmail=NULL,

LPTSTR pstrHomePage=NULL,

LPTSTR pstrCity=NULL,

LPTSTR pstrState=NULL,

LPTSTR pstrCountry=NULL,

LPTSTR pstrCommand="Details");

Under the Helpers section, add BuildQuery().

CString BuildQuery(LPTSTR pstrName, LPTSTR pstrBrowser,

LPTSTR pstrEmail, LPTSTR pstrHomePage,

LPTSTR pstrCity, LPTSTR pstrState,

LPTSTR pstrCountry);

Now that we can find entries in our Guestbook, let's use the command that Find() uses by default.

Viewing Entry Details

The Details() function shows the values entered by a user (see Listing 10.18). Details() takes two parameters: a pointer to a CHttpServerContext object and an LPTSTR holding the name of the user to look up.

Listing 10.18 GUESTBOOK.CPP-CGuestbook's Details Command Handler

/*

Details()

shows a user's record

*/

void CGuestbook::Details(CHttpServerContext* pCtxt, LPTSTR pstrName)

{

StartContent(pCtxt);

WriteTitle(pCtxt);

CString strQuery;

CString strOutput;

CString strTemp;

CDatabase db;

strQuery.Format("Name = '%s'", pstrName);

/* try to open the database, exit if it can't be opened */

if(!db.Open(GUESTBOOKDB, /* lpszDSN */

FALSE, /* bExclusive */

FALSE, /* bReadOnly */

CONNECTSTRING, /* lpszConnect */

FALSE)) /* bUseCursorLib */

{

strOutput.Format(IDS_DBERROR, SCRIPTPATH);

*pCtxt << strOutput;

return;

}

CGuestbookQuery rsGuestbook(&db);

rsGuestbook.m_strFilter = strQuery;

try

{

if(rsGuestbook.Open())

{

if(rsGuestbook.IsBOF())

strOutput.Format(IDS_NOMATCH, pstrName, SCRIPTPATH);

else

{

CString strEmail;

CString strHomePage;

if(!rsGuestbook.m_Email.IsEmpty())

strEmail.Format(IDS_MAILTO,

rsGuestbook.m_Email,

rsGuestbook.m_Email);

else

strEmail="Unknown";

if(!rsGuestbook.m_HomePage.IsEmpty())

strHomePage.Format(IDS_HOMEPAGE,

rsGuestbook.m_HomePage,

rsGuestbook.m_HomePage);

else

strHomePage="Unknown";

LoadLongResource(strTemp, IDS_HTML_DETAILS);

strOutput.Format(strTemp, BASEHREF,

rsGuestbook.m_Name,

rsGuestbook.m_Browser,

strEmail,

strHomePage,

rsGuestbook.m_City,

rsGuestbook.m_State,

rsGuestbook.m_Country,

rsGuestbook.m_Comments,

SCRIPTPATH);

}

}

else

strOutput.Format(IDS_UNKNOWN, SCRIPTPATH);

}

catch(CDBException* pEX)

{

TCHAR szError[1024];

if(pEX->GetErrorMessage(szError, sizeof(szError)))

strOutput.Format(IDS_EXCEPTION, szError, SCRIPTPATH);

else

strOutput.Format(IDS_UNKNOWN, SCRIPTPATH);

}

*pCtxt << strOutput;

rsGuestbook.Close();

db.Close();

EndContent(pCtxt);

}

By this time, the first several lines of source code should look familiar. Our SQL statement is formatted so it holds the name the CGuestbookQuery object should look for.

We open our database, create the CGuestbookQuery object, and pass a pointer to the database. Then we open the CGuestbookQuery object and check if there was a match.

Remember, we're on the Web, so let's spiff it up enough to make the user more productive. As you can see from the snippet below, our code checks both the e-mail address and the home page of the retrieved record for an empty value by using the CString::IsEmpty() function:

if(!rsGuestbook.m_Email.IsEmpty())

strEmail.Format(IDS_MAILTO,

rsGuestbook.m_Email,

rsGuestbook.m_Email);

else

strEmail="Unknown";

if(!rsGuestbook.m_HomePage.IsEmpty())

strHomePage.Format(IDS_HOMEPAGE,

rsGuestbook.m_HomePage,

rsGuestbook.m_HomePage);

else

strHomePage="Unknown";

The first value checked is the e-mail address. If this value is empty, we enter Unknown. If it holds information, we format it with a mailto: link using the IDS_MAILTO Resource ID.

The record's home page value is checked the same way. If it's not empty, it's formatted using IDS_HOMEPAGE (see Table 10.7 earlier in this chapter).

Once we've set the values of the members m_Email and m_HomePage, we load the HTML that allows us to display the record requested:

LoadLongResource(strTemp, IDS_HTML_DETAILS);

strOutput.Format(strTemp, BASEHREF,

rsGuestbook.m_Name,

rsGuestbook.m_Browser,

strEmail,

strHomePage,

rsGuestbook.m_City,

rsGuestbook.m_State,

rsGuestbook.m_Country,

rsGuestbook.m_Comments,

SCRIPTPATH);

Now our visitor can send e-mail or visit a home page if the values were there. If they want to find, add, edit, or delete an entry, they can return to ISAPI Guestbook's main menu.

As with previous functions, we need to add the declaration for this function to our class declaration. Put

void Details(CHttpServerContext* pCtxt, LPTSTR pstrName);

in the ISAPI Commands section of GUESTBOOK.H.

Editing a Guestbook Entry

It's unusual for guest books to allow users to edit their entries once they've been entered. But since this guest book serves a higher purpose (or so we'd like to think), we add a couple of ISAPI command handlers that allow records to be edited. If you browse through the code in the Edit() command handler (in Listing 10.19), you'll see that it's nearly identical to the code for Add().

Listing 10.19 GUESTBOOK.CPP-CGuestbook's Edit Command Handler

/*

Edit()

this command loads the form that allows the user to

enter their information

*/

void CGuestbook::Edit(CHttpServerContext* pCtxt, LPTSTR pstrName)

{

StartContent(pCtxt);

WriteTitle(pCtxt);

CString strTemp;

CString strOutput;

CString strQuery;

CDatabase db;

/* format the query */

strQuery.Format("Name = '%s'", pstrName);

/* try to open the database, exit if it can't be opened */

if(!db.Open(GUESTBOOKDB, /* lpszDSN */

FALSE, /* bExclusive */

FALSE, /* bReadOnly */

CONNECTSTRING, /* lpszConnect */

FALSE)) /* bUseCursorLib */

{

strOutput.Format(IDS_DBERROR, SCRIPTPATH);

*pCtxt << strOutput;

return;

}

CGuestbookQuery rsGuestbook(&db);

/* assign the query to the recordset */

rsGuestbook.m_strFilter = strQuery;

/* try to open the recordset, if it can't be opened catch

the exception and show the user */

try

{

if(rsGuestbook.Open())

{

/* if the record is not already on file,

this should return FALSE, then

notify the user */

if(rsGuestbook.IsBOF())

strOutput.Format("%s is not on file", pstrName);

else

{

LoadLongResource(strTemp, IDS_HTML_EDIT);

strOutput.Format(strTemp,

BASEHREF,

SCRIPTPATH,

rsGuestbook.m_Name,

rsGuestbook.m_Name,

rsGuestbook.m_Browser,

rsGuestbook.m_Email,

rsGuestbook.m_HomePage.IsEmpty() ? HTTP_PROTOCOL : rsGuestbook.m_HomePage,

rsGuestbook.m_City,

rsGuestbook.m_State,

rsGuestbook.m_Country,

rsGuestbook.m_Comments);

}

}

else

strOutput.Format(IDS_UNKNOWN, SCRIPTPATH);

}

catch(CDBException* pEX)

{

TCHAR szError[1024];

/* get the error message */

if(pEX->GetErrorMessage(szError, sizeof(szError)))

strOutput = szError;

else

strOutput.Format(IDS_UNKNOWN, SCRIPTPATH);

}

/* send the result */

*pCtxt << strOutput;

EndContent(pCtxt);

}

There are two significant differences between the Add() and Edit()command handlers. First, Edit() is not called from the main menu displayed by Default(). Instead, Edit() is called from FindGuestbookEntry(), as shown in Listing 10.20.

Remember the variable Command that defaults to Details in Find() and FindGuestbookEntry()? We used that variable so we didn't have to write more than one command handler to find an entry.

The second and more noticeable difference is that Edit() retrieves the user's record from the database and allows the user to change any entry other than the name. We do this because that Name is our Primary Key.

It would not have an adverse effect in this situation. But if our table related to records in another table not in this record set, we would risk compromising the referential integrity of the records.

Listing 10.20 GUESTBOOK.CPP-CGuestbook's EditGuestbookEntry Command Handler

/*

EditGuestbookEntry()

writes an new entry into the guestbook

*/

void CGuestbook::EditGuestbookEntry(CHttpServerContext* pCtxt,

LPTSTR pstrName,

LPTSTR pstrBrowser,

LPTSTR pstrEmail,

LPTSTR pstrHomePage,

LPTSTR pstrCity,

LPTSTR pstrState,

LPTSTR pstrCountry,

LPTSTR pstrComments)

{

StartContent(pCtxt);

WriteTitle(pCtxt);

CString strQuery;

CString strHeader;

CString strOutput;

CDatabase db;

strHeader.Format(IDS_STDHEADER, BASEHREF);

*pCtxt << strHeader;

/* make sure the required fields have values */

if(!strcmp(pstrName,NULLSTRING) || !strcmp(pstrCity,NULLSTRING) ||

!strcmp(pstrState,NULLSTRING) || !strcmp(pstrCountry,NULLSTRING))

{

*pCtxt << "<br><center>"

<< "Please be certain to enter your Name, City,"

<< " State and Country. Thank you."

<< "</center>";

return;

}

if(!strcmp(pstrHomePage,HTTP_PROTOCOL))

pstrHomePage = NULLSTRING;

strQuery.Format("Name = '%s'", pstrName);

/* try to open the database, exit if it can't be opened */

if(!db.Open(GUESTBOOKDB, /* lpszDSN */

FALSE, /* bExclusive */

FALSE, /* bReadOnly */

CONNECTSTRING, /* lpszConnect */

FALSE)) /* bUseCursorLib */

{

strOutput.Format(IDS_DBERROR, SCRIPTPATH);

*pCtxt << strOutput;

return;

}

CGuestbookQuery rsGuestbook(&db);

rsGuestbook.m_strFilter = strQuery;

try

{

if(rsGuestbook.Open())

{

if(rsGuestbook.IsBOF())

strOutput.Format(IDS_NOMATCH, pstrName, SCRIPTPATH);

else

{

rsGuestbook.Edit();

rsGuestbook.m_Browser = pstrBrowser;

rsGuestbook.m_Email = pstrEmail;

rsGuestbook.m_HomePage = pstrHomePage;

rsGuestbook.m_City = pstrCity;

rsGuestbook.m_State = pstrState;

rsGuestbook.m_Country = pstrCountry;

rsGuestbook.m_Comments = pstrComments;

if(rsGuestbook.Update())

strOutput.Format(IDS_THANKYOU, pstrName, SCRIPTPATH);

else

strOutput.Format(IDS_NOUPDATE, SCRIPTPATH);

}

}

else

strOutput.Format(IDS_UNKNOWN, SCRIPTPATH);

}

catch(CDBException* pEX)

{

TCHAR szError[1024];

if(pEX->GetErrorMessage(szError, sizeof(szError)))

strOutput.Format(IDS_EXCEPTION, szError, SCRIPTPATH);

else

strOutput.Format(IDS_UNKNOWN, SCRIPTPATH);

}

rsGuestbook.Close();

db.Close();

*pCtxt << strOutput;

EndContent(pCtxt);

}

Add() and WriteGuestbookEntry() work together to place a record in our database. And Edit() and EditGuestbookEntry() work together to change an existing record.

The Name field is stored in a hidden input object on Edit()'s dynamically generated page. So why do we test pstrName for an empty value?

/* make sure the required fields have values */

if(!strcmp(pstrName,NULLSTRING) || !strcmp(pstrCity,NULLSTRING) ||

!strcmp(pstrState,NULLSTRING) || !strcmp(pstrCountry,NULLSTRING))

{

*pCtxt << "<br><center>"

<< "Please be certain to enter your Name, City,"

<< " State and Country. Thank you."

<< "</center>";

return;

}

Because we have no guarantee that EditGuestbookEntry() will be called from Edit() only. I suppose it is possible to devise a complex method that may or may not work. But it's much simpler (and less error-prone) to make sure that a value for Name was entered.

There may be times when you want to authenticate the user who is editing the record. You'll learn how to do this in Chapter 16, "Extending Your Web Server with Filters."

Once we're sure that pstrName has a value, our code tries to find the record in the record set:

strQuery.Format("Name = '%s'", pstrName);

/* try to open the database, exit if it can't be opened */

if(!db.Open(GUESTBOOKDB, /* lpszDSN */

FALSE, /* bExclusive */

FALSE, /* bReadOnly */

CONNECTSTRING, /* lpszConnect */

FALSE)) /* bUseCursorLib */

{

strOutput.Format(IDS_DBERROR, SCRIPTPATH);

*pCtxt << strOutput;

return;

}

CGuestbookQuery rsGuestbook(&db);

rsGuestbook.m_strFilter = strQuery;

try

{

if(rsGuestbook.Open())

We try to find the record for two reasons. First, without a current record, any call to CRecordset::Edit() results in an exception. Why? Because if there's no record, you can't edit it.

Second, between the Edit() and EditGuestbookEntry() calls, the record could have been deleted. Finding the record before a CRecordset::Edit() call is a simple way to avoid exceptions and hair loss. As with CRecordset::AddNew(), a call to CRecordset::Edit() is completed with a call to CRecordset::Update():

if(rsGuestbook.Update())

strOutput.Format(IDS_THANKYOU, pstrName, SCRIPTPATH);

else

strOutput.Format(IDS_NOUPDATE, SCRIPTPATH);

Because we're using CRecordset and we're doing a CRecordset::Edit() function, if no fields are changed when CRecordset::Update() is called, it returns FALSE.

Deleting a Guestbook Entry

Unlike the other command handlers, Delete() does not have a supplement. FindGuestbookEntry() passes an LPTSTR specifying the name the visitor wants to delete. So we have all the information we need to proceed with the request (see Listing 10.21).

Listing 10.21 GUESTBOOK.CPP-CGuestbook's Delete Command Handler

/*

Delete()

removes an entry from the guestbook

*/

void CGuestbook::Delete(CHttpServerContext* pCtxt, LPTSTR pstrName)

{

StartContent(pCtxt);

WriteTitle(pCtxt);

CString strQuery;

CString strOutput;

CString strHeader;

CString strTemp;

CDatabase db;

strQuery.Format("Name = '%s'", pstrName);

strHeader.Format(IDS_STDHEADER, BASEHREF);

*pCtxt << strHeader;

/* try to open the database, exit if it can't be opened */

if(!db.Open(GUESTBOOKDB, /* lpszDSN */

FALSE, /* bExclusive */

FALSE, /* bReadOnly */

CONNECTSTRING, /* lpszConnect */

FALSE)) /* bUseCursorLib */

{

strOutput.Format(IDS_DBERROR, SCRIPTPATH);

*pCtxt << strOutput;

return;

}

CGuestbookQuery rsGuestbook(&db);

rsGuestbook.m_strFilter = strQuery;

try

{

if(rsGuestbook.Open())

{

if(rsGuestbook.IsBOF())

strOutput.Format(IDS_NOMATCH, pstrName, SCRIPTPATH);

else

{

rsGuestbook.Delete();

if(rsGuestbook.IsEOF())

rsGuestbook.MovePrev();

else

rsGuestbook.MoveNext();

strOutput.Format(IDS_REMOVED, pstrName, SCRIPTPATH);

}

}

else

strOutput.Format(IDS_UNKNOWN, SCRIPTPATH);

}

catch(CDBException* pEX)

{

TCHAR szError[1024];

if(pEX->GetErrorMessage(szError, sizeof(szError)))

strOutput.Format(IDS_EXCEPTION, szError, SCRIPTPATH);

else

strOutput.Format(IDS_UNKNOWN, SCRIPTPATH);

}

*pCtxt << strOutput;

rsGuestbook.Close();

db.Close();

EndContent(pCtxt);

}

Notice that we should test to make sure the record has not been deleted between the FindGuestbookEntry() and Delete()calls, as we did in EditGuestbookEntry():

if(rsGuestbook.IsBOF())

strOutput.Format(IDS_NOMATCH, pstrName, SCRIPTPATH);

Also, once we call CRecordset::Delete(), we should test to see if we are on either the first record or the last record, and call CRecordset::MoveNext() or CRecordset::MovePrev() accordingly.

In ISAPI Guestbook, we test for the end-of-file (EOF). If this function returns TRUE, we move to the previous record. On FALSE, we move to the next record.

Regardless of which method you use, you must move off the deleted record before your record set is considered updatable again. If we don't change to another record and subsequently try to call CRecordset::Delete(), CRecordset::AddNew(), or CRecordset::Edit(), a CDBException results.

else

{

rsGuestbook.Delete();

if(rsGuestbook.IsEOF())

rsGuestbook.MovePrev();

else

rsGuestbook.MoveNext();

strOutput.Format(IDS_REMOVED, pstrName, SCRIPTPATH);

}

After CRecordset::Delete() is called, the record's fields are set to NULL. If we tried another call to CRecordset::Delete() or CRecordset::Edit() before moving off the deleted record, an exception would result because there is no current record. In any event, getting into the habit of moving right after deleting a record will save you grief.

Summary

The extensions we cover in this chapter by no means exhaust the potential of ISAPI, MFC, or CDatabase and CRecordset. By building on the basics in this chapter, you'll soon be on the edge of Internet and intranet technology.

From Here...


© 1996, QUE Corporation, an imprint of Macmillan Publishing USA, a Simon and Schuster Company.