Chapter 9

Building Extensions with MFC


Version 4.1+ of the Microsoft Foundation Classes (MFC) comes with extensive support for Internet Server Application Programming Interface (ISAPI) extensions. The ISAPI Extension Wizard creates the MFC framework on which your Web server application is built.

Processing a form is the mainstay of Internet-based interactive applications and a key to unlocking the power of the Web. In this chapter, you'll learn how to use the framework supplied by the wizard to create an ISAPI extension that processes a Hypertext Markup language (HTML) form.

The Foundation

You have some work to do before you build. In Microsoft Developer Studio, create a new Project Workspace. For the name, enter My and choose ISAPI Extension Wizard as the type. Press Create.

Leave the Extension Class Name as CMyExtension, press Finish, and press OK. Now open the FileView of your project. AppWizard, as you can see, has inserted seven files (see Table 9.1).

Table 9.1 My Project Files

File Name

Description

MY.H

CMyExtension's declaration

MY.CPP

CMyExtension's implementation

MY.RC

My's resource file

MY.DEF

My's exports definition file

STDAFX.H

Application framework header

STDAFX.CPP

Includes STDAFX.H

MY.PCH

My's precompiled header

Open the MY.H file and you should see the code shown in Listing 9.1.

Listing 9.1 MY.H-CMyExtension's Class Declaration

// MY.H - Header file for your Internet Server

// My Extension

#include "resource.h"

class CMyExtension : public CHttpServer

{

public:

CMyExtension();

~CMyExtension();

// Overrides

// ClassWizard generated virtual function overrides

// NOTE - the ClassWizard will add and remove member

functions here.

// DO NOT EDIT what you see in these blocks of

generated code !

//{{AFX_VIRTUAL(CMyExtension)

public:

virtual BOOL GetExtensionVersion(HSE_VERSION_INFO* pVer);

//}}AFX_VIRTUAL

// TODO: Add handlers for your commands here.

// For example:

void Default(CHttpServerContext* pCtxt);

DECLARE_PARSE_MAP()

//{{AFX_MSG(CMyExtension)

//}}AFX_MSG

};

As mentioned earlier, this file holds your CHttpServer- derived class declaration. If you've never used MFC before, this code probably looks like most other class declarations you've created.

A closer look reveals some lines that may seem strange. These are the ClassWizard's override declaration section shown in Listing 9.2.

Listing 9.2 MY.H-ClassWizard Override Declarations

1 //{{AFX_VIRTUAL(CMyExtension)

2 public:

3 virtual BOOL GetExtensionVersion(HSE_VERSION_INFO* pVer);

4 //}}AFX_VIRTUAL

Lines 1 and 4 are the comment delimited lines that help ClassWizard find the beginning and end of virtual function overrides. When AppWizard creates the extension's skeleton code, there is only one overridden function. As you override and remove CHttpServer virtual functions with ClassWizard, their declarations are automatically added and deleted from the AFX_VIRTUAL section in the listing above.

Near the end of MY.H, you see the code in Listing 9.3.

Listing 9.3 MY.H-CMy's Parse Map Declaration

1 DECLARE_PARSE_MAP()

Depending on when your extension links to MFC, the DECLARE_PARSE_MAP() macro adds one private member and up to four public members to your CHttpServer-derived class. The definition of DECLARE_PARSE_MAP() is in AFXISAPI.H.

When I first started Windows 3.x programming, I read somewhere that WINDOWS.H was the ultimate reference for Windows developers. By the same token, the ultimate encyclopedia for MFC developers is the MFC source code provided by Microsoft on your Visual C++ CD-ROM. Although it's substantially larger than WINDOWS.H, it's also where many questions can be answered.

Listing 9.4 shows the implementation file for your extension.

Listing 9.4 MY.CPP-CMy's Implementation File

// MY.CPP - Implementation file for your Internet Server

// My Extension

#include "stdafx.h"

#include "My.h"

////////////////////////////////////////////////////////////////////

///

// The one and only CWinApp object

// NOTE: You may remove this object if you alter your project to no

// longer use MFC in a DLL.

CWinApp theApp;

////////////////////////////////////////////////////////////////////

///

// command-parsing map

BEGIN_PARSE_MAP(CMyExtension, CHttpServer)

// TODO: insert your ON_PARSE_COMMAND() and

// ON_PARSE_COMMAND_PARAMS() here to hook up your commands.

// For example:

ON_PARSE_COMMAND(Default, CMyExtension, ITS_EMPTY)

DEFAULT_PARSE_COMMAND(Default, CMyExtension)

END_PARSE_MAP(CMyExtension)

////////////////////////////////////////////////////////////////////

///

// The one and only CMyExtension object

CMyExtension theExtension;

////////////////////////////////////////////////////////////////////

///

// CMyExtension implementation

CMyExtension::CMyExtension()

{

}

CMyExtension::~CMyExtension()

{

}

BOOL CMyExtension::GetExtensionVersion(HSE_VERSION_INFO* pVer)

{

// Call default implementation for initialization

CHttpServer::GetExtensionVersion(pVer);

// Load description string

TCHAR sz[HSE_MAX_EXT_DLL_NAME_LEN+1];

ISAPIVERIFY(::LoadString(AfxGetResourceHandle(),

IDS_SERVER, sz, HSE_MAX_EXT_DLL_NAME_LEN));

_tcscpy(pVer->lpszExtensionDesc, sz);

return TRUE;

}

////////////////////////////////////////////////////////////////////

///

// CMyExtension command handlers

void CMyExtension::Default(CHttpServerContext* pCtxt)

{

StartContent(pCtxt);

WriteTitle(pCtxt);

*pCtxt << _T("This default message was produced by the

Internet");

*pCtxt << _T(" Server DLL Wizard. Edit your

CMyExtension::Default()");

*pCtxt << _T(" implementation to change it.\r\n");

EndContent(pCtxt);

}

// Do not edit the following lines, which are needed by ClassWizard.

#if 0

BEGIN_MESSAGE_MAP(CMyExtension, CHttpServer)

//{{AFX_MSG_MAP(CMyExtension)

//}}AFX_MSG_MAP

END_MESSAGE_MAP()

#endif // 0

////////////////////////////////////////////////////////////////////

///

// If your extension will not use MFC, you'll need this code to make

// sure the extension objects can find the resource handle for the

// module. If you convert your extension to not be dependent on

MFC,

// remove the comments arounn the following AfxGetResourceHandle()

// and DllMain() functions, as well as the g_hInstance global.

/****

static HINSTANCE g_hInstance;

HINSTANCE AFXISAPI AfxGetResourceHandle()

{

return g_hInstance;

}

BOOL WINAPI DllMain(HINSTANCE hInst, ULONG ulReason,

LPVOID lpReserved)

{

if (ulReason == DLL_PROCESS_ATTACH)

{

g_hInstance = hInst;

}

return TRUE;

}

****/

Now that you've browsed MY.CPP, let's break it down piece by piece.

Listing 9.5 MY.CPP-CWinApp Declaration

////////////////////////////////////////////////////////////////////

///

// The one and only CWinApp object

// NOTE: You may remove this object if you alter your project to no

// longer use MFC in a DLL.

CWinApp theApp;

When you looked at the code in Listing 9.5, you probably did a double take. Don't worry, you're not seeing things. A CWinApp object is needed by all MFC programs. As long as your extension uses MFC, you'll need this object.

Listing 9.6 shows CMyExtension's Parse Map.

Listing 9.6 MY.CPP-CMyExtension's Parse Map

////////////////////////////////////////////////////////////////////

///

// command-parsing map

1 BEGIN_PARSE_MAP(CMyExtension, CHttpServer)

2 // TODO: insert your ON_PARSE_COMMAND() and

3 // ON_PARSE_COMMAND_PARAMS() here to hook up your commands.

4 // For example:

5 ON_PARSE_COMMAND(Default, CMyExtension, ITS_EMPTY)

6 DEFAULT_PARSE_COMMAND(Default, CMyExtension)

7 END_PARSE_MAP(CMyExtension)

By default, AppWizard creates a parse map for each MFC- based ISAPI extension. ISAPI parse maps are the way MFC ISAPI developers commonly chart requests from Web clients to specific functions in their extension DLL. Line 1,

BEGIN_PARSE_MAP(CMyExtension, CHttpServer)

is self-explanatory. This is where your parse map's definition begins. The first parameter, CMyExtension, specifies the owner of this parse map. The second must be CHttpServer, which is the base class. Now, move down to line 5.

ON_PARSE_COMMAND(Default, CMyExtension, ITS_EMPTY)

This is where the command-to-function mapping takes place. The first parameter, which in this case is Default, identifies the command name and member function it corresponds with. When using a parse map in ISAPI, each command must have a comparable handler function of the same name.

The second parameter, CMyExtension, represents the class the function is mapped to. The third parameter, ITS_EMPTY, specifies the number and types of arguments the function accepts, which in this case is none.

Line 6 is where we define the command to be used when one is not specified.

DEFAULT_PARSE_COMMAND(Default, CMyExtension)

The parameters this macro accepts are almost identical to ON_PARSE_COMMAND. In fact, the only difference is that you don't have to specify the number and type of the arguments.

The last line in Listing 9.6 is line 7.

END_PARSE_MAP(CMyExtension)

BEGIN_PARSE_MAP starts the definition of the parse map and END_PARSE_MAP ends the definition. The only parameter this macro takes is the name of the class that owns this parse map.

Although we've covered each of the entries in Listing 9.6, there's one parse map macro not represented: ON_PARSE_COMMAND_PARAMS. This macro is not part of the parse map created when a new ISAPI extension is started because the only command handler, Default, takes no parameters.

To illustrate this macro, I rewrite our parse map, as follows.

BEGIN_PARSE_MAP(CMyExtension, CHttpServer)

// TODO: insert your ON_PARSE_COMMAND() and

// ON_PARSE_COMMAND_PARAMS() here to hook up your commands.

// For example:

5 ON_PARSE_COMMAND(Default, CMyExtension, ITS_PSTR)

6 ON_PARSE_COMMAND_PARAMS("Name")

DEFAULT_PARSE_COMMAND(Default, CMyExtension)

END_PARSE_MAP(CMyExtension)

The only lines you should be concerned with are 5 and 6. Notice how, when I add ON_PARSE_COMMAND_PARAMS, I also change ON_PARSE_COMMAND. Earlier, I said that ON_PARSE_COMMAND's third parameter specifies the number and types of arguments the function accepts.

But because we added an argument to ON_PARSE_COMMAND_PARAMS, our command handler is no longer empty. The ITS_PSTR entry means the parameter is a pointer to a string. In MSVC 4.2, ON_PARSE_COMMAND recognizes six different constants as representing data types, as shown in Table 9.2.

Table 9.2 ON_PARSE_COMMAND Data Types

Constant

Type

ITS_EMPTY

N/A

ITS_PSTR

string pointer

ITS_I2

short

ITS_I4

long

ITS_R4

float

ITS_R8

double

ON_PARSE_COMMAND_PARAMS is where the parameters accepted in ON_PARSE_COMMAND are specified by the name of the HTML form's input element. These two macros work hand-in-hand. In fact, the only time you shouldn't have the ON_PARSE_COMMAND_PARAMS macro after ON_PARSE_COMMAND, is when the argument parameter of ON_PARSE_COMMAND has a value of ITS_EMPTY.

Here are some simple rules for using these two macros when ON_PARSE_COMMAND does not have an argument value of ITS_EMTPY.

The last entry in the above list may be confusing, so let's look at some examples. For clarity, BEGIN_PARSE_MAP, END_PARSE_MAP, and DEFAULT_PARSE_COMMAND are not present.

ON_PARSE_COMMAND(WriteToFile, CSampleExtension, ITS_PSTR ITS_PSTR)

ON_PARSE_COMMAND_PARAMS("Name Country")

By now you should be able to recognize immediately what this code does. WriteToFile represents the function and CSampleExtension is the class. WriteToFile takes two string pointers; Name and Country.

This is simple. And since each parameter has the same data type, the order is irrelevant. Now, let's add another parameter:

ON_PARSE_COMMAND(WriteToFile, CSampleExtension, ITS_PSTR ITS_PSTR

ITS_I2)

ON_PARSE_COMMAND_PARAMS("Age Name Country")

A browser sends a command:

/scripts/sample.dll?WriteToFile&Age=21&Name=Joe&Country=US

Instead of assigning Age the integer 21, this example assigns it the string value of 21. Also, Country is not assigned the string value of US but the integer value of 0.

To fix this problem, we change the order of the variable names in ON_PARSE_COMMAND_PARAMS to match their respective data types declared in ON_PARSE_COMMAND:

ON_PARSE_COMMAND(WriteToFile, CSampleExtension, ITS_PSTR ITS_PSTR ITS_I2)

ON_PARSE_COMMAND_PARAMS("Name Country Age")

Now the same command yields the anticipated results:

Name=Joe

Country=US

Age=21

This ends our overview of the parse map. We'll return to it once we reach the actual command handler. For now, let's keep inching our way down the source file.

Listing 9.7 is where your extension object comes to life. As in other object-oriented programs you may have written, before you can access the methods in a class, you must have an object created from that class.

Listing 9.7 MY.CPP-CMyExtension Declaration

////////////////////////////////////////////////////////////////////

///

// The one and only CMyExtension object

CMyExtension theExtension;

Listing 9.8 begins simply enough. Lines 1 through 3 are the constructor for CMyExtension. Lines 4 through 6 are the destructor. As in other C++ programs, you can use these components to initialize and destroy any elements your class uses.

Unless you're already familiar with programming in a multithreaded environment, you should understand what's involved before using constructors and destructors in an ISAPI DLL. This is covered in Chapter 18, "Making Your Extensions Thread-Safe." For now, though, the classes we create don't use their constructors and destructors, as shown in Listing 9.8.

Listing 9.8 MY.CPP-CMyExtension's Startup

////////////////////////////////////////////////////////////////////

///

// CMyExtension implementation

1 CMyExtension::CMyExtension()

2 {

3 }

4 CMyExtension::~CMyExtension()

5 {

6 }

7 BOOL CMyExtension::GetExtensionVersion(HSE_VERSION_INFO* pVer)

8 {

9 // Call default implementation for initialization

10 CHttpServer::GetExtensionVersion(pVer);

11 // Load description string

12 TCHAR sz[HSE_MAX_EXT_DLL_NAME_LEN+1];

13 ISAPIVERIFY(::LoadString(AfxGetResourceHandle(),

IDS_SERVER, sz, HSE_MAX_EXT_DLL_NAME_LEN));

14 _tcscpy(pVer->lpszExtensionDesc, sz);

15 return TRUE;

16 }

Lines 7 through 16 show the GetExtensionVersion()function. This is one of two functions that all ISAPI extensions, MFC and non-MFC, must export. The other, HttpExtensionProc(), is a virtual function that AppWizard does not automatically override.

GetExtensionVersion() is called by your server once when your extension is loaded and does two tasks. The first is to check the extension's ISAPI specification version number and compare it to the server's. The second is to give the server a short text description of the extension.

The default HttpExtensionProc() is shown in listing 9.9.

Listing 9.9 ISAPI.CPP-HttpExtensionProc

DWORD CHttpServer::HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB)

{

DWORD dwRet = HSE_STATUS_SUCCESS;

BOOL bDefault = FALSE;

LPTSTR pszPostBuffer = NULL;

LPTSTR pszQuery;

LPTSTR pszCommand = NULL;

int nMethodRet;

LPTSTR pstrLastChar;

DWORD cbStream = 0;

BYTE* pbStream = NULL;

CHttpServerContext ctxtCall(pECB);

pECB->dwHttpStatusCode = 0;

ISAPIASSERT(NULL != pServer);

if (pServer == NULL)

{

dwRet = HSE_STATUS_ERROR;

goto CleanUp;

}

// get the query

if (_tcsicmp(pECB->lpszMethod, szGet) == 0)

{

pszQuery = pECB->lpszQueryString;

}

else if (_tcsicmp(pECB->lpszMethod, szPost) == 0)

{

pszCommand = pECB->lpszQueryString;

pszPostBuffer = new TCHAR[pECB->cbAvailable + 1];

pszQuery = GetQuery(&ctxtCall, pszPostBuffer,

pECB->cbAvailable);

}

else

{

ISAPITRACE1("Error: Unrecognized method: %s\n",

pECB->lpszMethod);

dwRet = HSE_STATUS_ERROR;

goto CleanUp;

}

// trim junk that some browsers put at the very end

pstrLastChar = pszQuery + _tcslen(pszQuery) -1;

while ((*pstrLastChar == ' ' || *pstrLastChar == '\n' ||

*pstrLastChar == '\r') && pstrLastChar > pszQuery)

{

*pstrLastChar-- = '\0';

}

// do something about it

if (!pServer->InitInstance(&ctxtCall))

dwRet = HSE_STATUS_ERROR;

else

{

pECB->dwHttpStatusCode = HTTP_STATUS_OK;

try {

nMethodRet = pServer->CallFunction(&ctxtCall,

pszQuery, pszCommand);

}

catch (...)

{

ISAPITRACE1("Error: command %s caused an unhandled

exception!\n",

pszQuery);

nMethodRet = callNoStackSpace;

}

// was an error caused by trying to dispatch?

if (nMethodRet != callOK && pECB->dwHttpStatusCode ==

HTTP_STATUS_OK)

{

dwRet = HSE_STATUS_ERROR;

switch (nMethodRet)

{

case callNoStream:

pECB->dwHttpStatusCode = HTTP_STATUS_NO_CONTENT;

break;

case callParamRequired:

case callBadParamCount:

case callBadParam:

pECB->dwHttpStatusCode = HTTP_STATUS_BAD_REQUEST;

break;

case callBadCommand:

pECB->dwHttpStatusCode =

HTTP_STATUS_NOT_IMPLEMENTED;

break;

case callNoStackSpace:

default:

pECB->dwHttpStatusCode =

HTTP_STATUS_SERVER_ERROR;

break;

}

}

// if there was no error or the user said they handled

// the error, prepare to spit out the generated HTML

if (nMethodRet == callOK ||

OnParseError(&ctxtCall, nMethodRet) == TRUE)

{

cbStream = ctxtCall.m_pStream->GetStreamSize();

pbStream = ctxtCall.m_pStream->Detach();

}

}

CleanUp:

// if there was an error, return an appropriate status

TCHAR szResponse[64];

BuildStatusCode(szResponse, pECB->dwHttpStatusCode);

DWORD dwSize = cbStream - ctxtCall.m_dwEndOfHeaders;

BYTE* pbContent = NULL;

BYTE cSaved;

if (pbStream != NULL)

{

cSaved = pbStream[ctxtCall.m_dwEndOfHeaders];

pbStream[ctxtCall.m_dwEndOfHeaders] = '\0';

pbContent = &pbStream[ctxtCall.m_dwEndOfHeaders];

}

if (!ctxtCall.ServerSupportFunction(

HSE_REQ_SEND_RESPONSE_HEADER, szResponse, 0, (LPDWORD)

pbStream) &&

::GetLastError() != 10054) // WSAECONNRESET

{

pECB->dwHttpStatusCode = HTTP_STATUS_SERVER_ERROR;

dwRet = HSE_STATUS_ERROR;

#ifdef _DEBUG

DWORD dwCause = ::GetLastError();

ISAPITRACE1("Error: Unable to write headers: %8.8X!\n",

dwCause);

#endif

}

else

{

if (pbContent != NULL)

{

// write a newline to separate content from headers

*pbContent = cSaved;

DWORD dwNewLineSize = 2;

if (!ctxtCall.WriteClient(_T("\r\n"),

&dwNewLineSize, 0) ||

!ctxtCall.WriteClient(pbContent, &dwSize, 0))

{

dwRet = HSE_STATUS_ERROR;

pECB->dwHttpStatusCode = HTTP_STATUS_SERVER_

ERROR;

ISAPITRACE("Error: Unable to write content

body!\n");

}

}

else

ISAPITRACE("Error: No body content!\n");

}

if (pbStream != NULL)

ctxtCall.m_pStream->Free(pbStream);

if (dwRet == HSE_STATUS_SUCCESS)

pECB->dwHttpStatusCode = HTTP_STATUS_OK;

if (pszPostBuffer != NULL)

delete [] pszPostBuffer;

return dwRet;

}

HttpExtensionProc() is the second function that all ISAPI extensions export. Unlike GetExtensionVersion(), which is only called once, HttpExtensionProc() is called by the server each time a client makes a request to your DLL.

The server gives your DLL the necessary connection information through the EXTENSION_CONTROL_BLOCK (ECB) structure. This structure is shown in listing 9.10.

Listing 9.10 HTTPEXT.H-ISAPI EXTENSION_CONTROL_BLOCK Structure

typedef struct _EXTENSION_CONTROL_BLOCK {

DWORD cbSize; // size of this struct.

DWORD dwVersion; // version info of this spec

HCONN ConnID; // Context number not to be

modified!

DWORD dwHttpStatusCode; // HTTP Status code

CHAR lpszLogData[HSE_LOG_BUFFER_LEN];// null terminated

log info specific to this Extension DLL

LPSTR lpszMethod; // REQUEST_METHOD

LPSTR lpszQueryString; // QUERY_STRING

LPSTR lpszPathInfo; // PATH_INFO

LPSTR lpszPathTranslated; // PATH_TRANSLATED

DWORD cbTotalBytes; // Total bytes indicated from

client

DWORD cbAvailable; // Available number of bytes

LPBYTE lpbData; // pointer to cbAvailable

bytes

LPSTR lpszContentType; // Content type of client data

BOOL (WINAPI * GetServerVariable) ( HCONN hConn,

LPSTR

lpszVariableName,

LPVOID lpvBuffer,

LPDWORD lpdwSize );

BOOL (WINAPI * WriteClient) ( HCONN ConnID,

LPVOID Buffer,

LPDWORD lpdwBytes,

DWORD dwReserved );

BOOL (WINAPI * ReadClient) ( HCONN ConnID,

LPVOID lpvBuffer,

LPDWORD lpdwSize );

BOOL (WINAPI * ServerSupportFunction)( HCONN hConn,

DWORD dwHSERRequest,

LPVOID lpvBuffer,

LPDWORD lpdwSize,

LPDWORD

pdwDataType );

} EXTENSION_CONTROL_BLOCK, *LPEXTENSION_CONTROL_BLOCK;

Just as all ISAPI extension DLLs must export GetExtensionVersion() and HttpExtensionProc(), the ECB is a common thread between MFC and non-MFC ISAPI extensions. The ECB is how the server and extension communicate-if your access to the ECB is the CHttpServerContext object created in HttpExtensionProc():

CHttpServerContext ctxtCall(pECB);

Each command handler you create takes a pointer to the CHttpServerContext object created in HttpExtensionProc(). This pointer gives your functions easy access to the connection-specific information in the ECB and more.

Line 2 in listing 9.11 shows the bond between your parse map and command handlers.

Listing 9.11 ISAPI.CPP-The Default HttpExtensionProc()

1 try {

2 nMethodRet = pServer->CallFunction(&ctxtCall,

pszQuery, pszCommand);

3 }

4 catch (...)

5 {

6 ISAPITRACE1("Error: command %s caused an unhandled

exception!\n",

7 pszQuery);

8 nMethodRet = callNoStackSpace;

9 }

CHttpServer::CallFuntion() is what the framework uses to find and execute command handlers. Since CallFunction() is a virtual function, you can override it and customize how the query string is parsed.

From There to Here

We now know how to use the parse map macros and how our parse map gets connected to our extension. Before we can act on the data sent by a client, however, we need to connect our parse map to our command handling functions. We do this with the first parameter of ON_PARSE_COMMAND, the command handlers themselves.

Listing 9.12 shows CMyExtension's default command handler.

Listing 9.12 MY.CPP-CMyExtension's Default Command Handler

////////////////////////////////////////////////////////////////////

///

// CMyExtension command handlers

1 void CMyExtension::Default(CHttpServerContext* pCtxt)

2 {

3 StartContent(pCtxt);

4 WriteTitle(pCtxt);

5 *pCtxt << _T("This default message was produced by the Internet");

6 *pCtxt << _T(" Server DLL Wizard. Edit your

CMyExtension::Default()");

7 *pCtxt << _T(" implementation to change it.\r\n");

8 EndContent(pCtxt);

9 }

Recall that Default's ON_PARSE_COMMAND macro had an argument value of ITS_EMPTY when AppWizard generated the ISAPI framework for us. If the macro says it was empty, why does our function show a single parameter?

For our handlers to have access to connection-specific information, each one must take a pointer to the CHttpServerContext object created in HttpExtensionProc(). This pointer is not shown in the parse map because when our command handler is called, the MFC framework automatically passes the CHttpServerContext pointer to our function.

This object is what allows the DLL to handle multiple connections with multiple threads instead of with multiple processes. If we were to edit our parse map's arguments parameter, we would also have to edit the declaration of the command handling function.

To illustrate, let's again look at the parse map I used earlier:

ON_PARSE_COMMAND(WriteToFile, CSampleExtension, ITS_PSTR ITS_PSTR

ITS_I2)

ON_PARSE_COMMAND_PARAMS("Name Country Age")

Given the above macros, our WriteToFile() handler would need to take four, not three, parameters:

void CSampleExtension::WriteToFile(CHttpServerContext* pCtxt,

LPTSTR pstrName, LPTSTR pstrCountry

INT iAge)

If we change our parse map macros to reflect only two parameters, we would also need to change our function once again.

ON_PARSE_COMMAND(WriteToFile, CSampleExtension, ITS_PSTR ITS_PSTR)

ON_PARSE_COMMAND_PARAMS("Name Country")

void CSampleExtension::WriteToFile(CHttpServerContext* pCtxt,

LPTSTR pstrName, LPTSTR pstrCountry)

Notice how the arguments parameter types of ON_PARSE_COMMAND correspond with the types in our command handler. Just as you need to make sure the names in ON_PARSE_COMMAND_PARAMS are in line with the arguments in ON_PARSE_COMMAND, you need to make sure the declarations following the CHttpServerContext pointer in your command handlers match the types in the arguments section of ON_PARSE_COMMAND.

In addition, because you will be manipulating the data sent from a Web browser, the function declaration must also match the order in which you expect to get them. For example, in the above command handler, we are expecting a name as the first parameter following the CHttpServerContext pointer. If we expected a Country instead, we would have to change not only the ON_PARSE_COMMAND_PARAMS macro but also the position of the variable we expect to hold the Country value.

ON_PARSE_COMMAND(WriteToFile, CSampleExtension, ITS_PSTR ITS_PSTR)

ON_PARSE_COMMAND_PARAMS("Country Name")

void CSampleExtension::WriteToFile(CHttpServerContext* pCtxt,

LPTSTR pstrCountry, LPTSTR pstrName)

Although this method is tedious and needs attention to detail, it keeps you from having to write your own parsing algorithm. MFC does not require you to use parse maps in your ISAPI extension. Instead, the parse maps are supplied as generic tools to speed the development cycle. For more information on parse maps, see Microsoft Visual C++ Books Online.

Now that we understand a little more about MFC's ISAPI implementation, we're ready to create our form processor. I'll point out common errors and solutions as we go.

The form we'll process is shown in Fig. 9.1.

Fig. 9.1

The target HTML form.

Once again, create a new Project Workspace in Developer Studio. From the New Project Workspace dialog, select the ISAPI Extension Wizard type and enter the MembrApp Project Name, as shown in Fig. 9.2. Click Create.

Fig. 9.2

The New Project Workspace dialog box.

The one-step ISAPI extension wizard appears. The default settings are just right for our purposes.

We want an extension object and not a filter object so the class name of CMembrAppExtension is fine. We'll use the MFC library as a shared DLL to make our extension compact (see Figure 9.3).


Fig. 9.3

One-step Extension Wizard.

Click Finish. The wizard brings up a confirmation dialog box. Pressing OK allows the wizard to create the extension's skeleton code.

Just as we saw earlier in this chapter, the wizard takes care of many details. It creates a CWinApp object to cover the DllMain startup/shutdown processing. It also creates an exported GetExtensionVersion() and HttpExtensionProc(), which you are familiar with by now.

The wizard creates a derived CHttpServer object, in this example called CMembrAppExtension, shown in Listing 9.13. This object has a parse map, a GetExtensionVersion member function, and a Default function, which does very little at the moment.

Listing 9.13 CMembrAppExtension-a CHttpServer Object

void CMembrAppExtension::Default(CHttpServerContext* pCtxt)

{

StartContent(pCtxt);

WriteTitle(pCtxt);

*pCtxt << _T("This default message was produced by the

Internet");

*pCtxt << _T(" Server DLL Wizard. Edit your

CMembrAppExtension::Default()");

*pCtxt << _T(" implementation to change it.\r\n");

EndContent(pCtxt);

}

Now that we have constructed the framework using the wizard, we'll build the extension itself. Before we can do this, though, we need to iron out a couple of wrinkles.

First, a DLL needs a host application to run with. Second, because we're using Microsoft Internet Information Server (IIS), our host application is configured to run only as a service. If you are using a different ISAPI Web server, you may need to ask your vendor for instructions.

You need to set up your ISAPI extension to run with IIS and Visual C++ in debug mode in this sequence:

  1. Make you have set administrative privileges, including the "Act as part of the operating system" and "Log on as a service" for your account.
  2. To do this, go to the User Manager, Policies/User-Rights, and select the Show Advanced User Rights check box. Select each of the privileges and add your logon to the list of groups and users granted these rights.
  3. Go to the Control Panel Services icon, scroll down to the World Wide Web Publishing Service, and stop the service. Only one copy of IIS can successfully run at one time and we'll be running IIS from our debugger.
  4. Back at the CMembrAppExtension extension, select Build/Settings and select the Link tab.
  5. Type in the full path name of the DLL in the Output File Name text box. Select and copy this path name to the clipboard.
  6. Select the Debug tab and go to the Additional DLLs category. Under Modules, paste the full path name into the first entry for Local Name. This ensures that the symbol table for the DLL is preloaded for debugging.
  7. While in the Debug tab dialog, go to the General category.
  8. In the Executable for debug session text box, type in c:\inetsrv\server\INetInfo.Exe or the path for IIS if you installed it in a different directory.
  9. In Program Arguments, type in -e W3Svc. This allows IIS to start as an application.
  10. Save your work. Compile and run the CMembrAppExtension. Wait a few moments for IIS to initialize.

The next steps enable you to run your extension with IIS. If you are using a different ISAPI Web server, you may need to ask your vendor for instructions.

  1. Go to the Internet Service Manager and notice that the State for your computer's WWW service says Stopped. Even though IIS is running under the debugger, this is what it says.
  2. Double-click on the Services computer name to see its properties. When you see the Properties dialog, you know that IIS is running in debug mode with your extension.
  3. Add a virtual directory that points to your debug directory. Select an alias of /MembrApp and be sure to select the Execute checkbox in the Access group (see Fig. 9.4). Turn Read permissions off by making sure the READ checkbox is not on.

Fig. 9.4

Directory properties for the MembrApp debug session.

  1. Now bring up your favorite browser and enter the URL of the directory you entered above. For example, enter http://127.0.0.1/MembrApp/MembrApp.Dll.
  2. At last! We see the output of our extension DLL as follows:
    This default message was produced by the


    Internet Server DLL Wizard. Edit your


    CMembrAppExtension::Default()implementation


    to change it.

From GET to POST

In this section, you'll change the Default function to handle GET and POST requests separately. A GET request directs the user to an HTML file holding a basic form. A POST request acknowledges receipt.

You'll also create an HTML file holding the form. Creating a separate HTML file rather than coding HTML into the source code is often preferable. It is certainly more readable for our purposes.

So far, we have taken the supplied CHttpServer member functions StartContent, WriteTitle, and EndContent at face value. They are simple functions that hold the basic elements of an HTML page. To distinguish between a GET and a POST command, we must look at contents of CHttpServerContext, which we discussed earlier.

Change the Default function above as follows:

void CMembrAppExtension::Default(CHttpServerContext* pCtxt)

{

StartContent(pCtxt);

WriteTitle(pCtxt);

CString method = pCtxt->m_pECB->lpszMethod;

if (0 == method.CompareNoCase("POST"))

*pCtxt << _T("Thanks for the POST!\r\n");

else

*pCtxt << _T("<A HREF=\"/club.htm\">Click here to

continue.</A>\r\n");

EndContent(pCtxt);

}

The CHttpServer-derived class, which in this case is CMembrAppExtension, is instantiated at DLL startup. Each time a request to the DLL is processed by the Web server, the CHttpServer command handler is called with a request context as a parameter. Using stack-based data this way is standard for passing data so that it is thread-safe.

The request context is an instance of the CHttpServerContext class, which holds useful functions and data that are used when processing a request.

In our latest version of the Default function, we access the lpszMethod method string of CHttpServerContext's EXTENSION_CONTROL_BLOCK. We could have called the GetServerVariable function with the REQUEST_METHOD argument.

Both requests yield the same result, whether the request was a GET, as when we typed in the http://127.0.0.1/MembrApp/ MembrApp.dll? URL, or whether the request was a POST, as when we submit the club.htm HTML form shown in Listing 9.14.

Listing 9.14 Code for HTML Form

<HTML>

<HEAD>

<TITLE>Membership Application</TITLE>

</HEAD>

<BODY>

<CENTER>

<H3>Application for Membership </H3>

</CENTER>

<FORM action="MembrApp.dll?" method="post">

<P><INPUT type=submit value="Access-Member-Area">

<INPUT type=reset value="Clear Form"></P>

</BODY>

</HTML>

You may have noticed that this form POSTs precisely nothing. Now that we have come this far, it is time to start processing some real data.

Adding Some Form Elements

In this section, you'll create a new function to handle the form's submission. To do this, we use the ON_PARSE_COMMAND family of macros covered earlier.

Recall that DEFAULT_PARSE_COMMAND sends a request without a command to the function in its first argument. Requests without a command have nothing following the question mark in their URL.

The wizard sets up a default DEFAULT_PARSE_COMMAND to pass control to the Default function we changed above. The ON_PARSE_COMMAND and ON_PARSE_COMMAND_PARAMS work together to process non-Default commands.

Remember that ON_PARSE_COMMAND takes three parameters. The first is the name of the function to be called. The second is the CHttpServer class (here CMembrAppExtension). The third defines the number and type of the arguments to the function.

This third parameter can cause some confusion. It can have the ITS_EMPTY argument. Or it can have a combination of the ITS_PSTR (a string) and ITS_I4 (a long integer) arguments, among others.

The confusion arises because this parameter must have at least one argument and can have several. But if this parameter has several arguments, these arguments must not be separated by commas or compile errors will result. The ITS_EMPTY parameter shows that there are no arguments.

At runtime, this parameter provides crucial information for a special assembler function, _AfxParseCall, borrowed from the low-level OLE Idispatch implementation. This function pushes the right number of arguments onto the stack just before calling your handler function. It is coded differently for each NT-compatible processor type.

The ON_PARSE_COMMAND macros process both the URL query arguments (following the second question mark) and the posted contents of a form.

ON_PARSE_COMMAND_PARAMS gives the names and possible defaults for the arguments.

If you mismatch the number of parameters declared in ON_PARSE_COMMAND, you either get a stack leak from _AfxParseCall or the dreaded "Document contains no data" error message. So be careful to build your HTML forms, create the parameter maps, and declare the handler functions together so that all the parameters match up.

Change the Form

In your club.htm form, add the following lines right before the input-type-submit button directive:

Your email address (required):<BR>

<INPUT name="Email" size=40>

<P>

This permits the user to enter an email address of up to 40 characters.

Change the Parse Map

Add the following lines to the parse map:

ON_PARSE_COMMAND(HandleApp, CMembrAppExtension, ITS_PSTR)

ON_PARSE_COMMAND_PARAMS("Email=unknown")

This pair of commands shows that the CMembrAppExtension:: HandleApp function handles one parameter of the string type, the parameter's HTML name is Email, and its default value is Unknown.

Note that although we declare a default value for Email, defaults generally apply only to URL queries. This is because POSTing a form with an empty Email text field overrides any default.

For example, if the user submits the above form without filling out any fields, the value for Email is an empty string and not Unknown.

Declare and Use the Handler Function

Add the function in Listing 9.15 to your CMembrAppExtension class:

Listing 9.15 The Handler Function

void CMembrAppExtension::HandleApp(CHttpServerContext* pCtxt,

LPCTSTR pstr)

{

StartContent(pCtxt);

WriteTitle(pCtxt);

CString method = pCtxt->m_pECB->lpszMethod;

if (0 == method.CompareNoCase("POST"))

{

*pCtxt << _T("Thanks for the POST, ");

CString email = pstr;

*pCtxt << _T(email);

*pCtxt << _T("!\r\n");

}

else

*pCtxt << _T("This forms processor expects to be Posted

to!\r\n");

EndContent(pCtxt);

}

In Listing 9.15 above, we collect the form data into a convenient string and output a message to the user.

Now you are ready to compile and run the extension again. Be sure that the Club.htm file resides in your /wwwroot (root HTML) directory. Start your browser, request the URL of http://localhost/club.htm, and fill in the form. If all goes well, when you enter your email address of email@net.com, the extension replies "Thanks for the POST, email@net.com!"

Congratulations!

Adding a Radio Button

In this section, we add another input element, the radio button. We learn about error messages that commonly occur during development of an HTML file and its MFC ISAPI forms handler. This leads us to the conclusion that HTML files and their MFC ISAPI forms handlers should be carefully designed and planned in advance.

Change the Form

In your club.htm form, add the following lines right before the input-type-submit button directive:

<P>

<INPUT type="radio" name="need" value="Need_now">I need it now! <BR>

<INPUT type="radio" name="need" value="sounds_interesting">Sounds

interesting. <BR>

<INPUT type="radio" name="need" value="dont_care">Couldn't care

less. <BR>

<P>

Save the changed form and reload the form in your browser.

Without making any changes to MembrApp (it should still be running), type in your email address again and select the Sounds Interesting radio button.

Submit the form and observe (if you are using Netscape) the "Document contains no data" browser error message. Other browsers, such as Microsoft Internet Explorer (IE), might reply "Unable to open file." Sometimes the browser just produces a blank screen.

Needless to say, this could cause some confusion on the part of the person visiting your Web site. It is not particularly helpful for the developer either when you are trying to track down the cause of an ISAPI problem.

For example, your production Webmaster may innocently decide to add a couple of fields to a form for which you have supplied an MFC forms handler. The Webmaster's problem report might be that nothing significant had been changed but that now "nothing works." Be aware that changing the elements of a form is a significant event in the life of an ISAPI MFC DLL.

What Went Wrong?

After CHttpServer::HttpExtensionProc is called and CHttpServer::CallFunction collects its data in pszMethod (HandleApp) and pszParams (Email=email@net.com& need=sounds_interesting), CHttpServer::Lookup finds the parse map entry you entered between BEGIN_PARSE_MAP and END_PARSE_MAP.

Then CHttpServer::PushDefaultStackArgs looks at the arguments supplied, pushing them onto the stack in preparation for the processor-dependent _AfxParseCall. But it finds that the number of arguments does not match the parse map definition. So CallMemberFunc (and CallFunction, in turn) returns callBadParamCount.

A 400 Bad Request response is generated. And because CallMemberFunc encountered an error, it creates no HTML content to be returned.

The difficulties are compounded because CHttpServer::OnParseError fails to load a string from the resource table and just outputs a TRACE debug message. So Listing 9.16 is a quick override of that function, which at least will give some indication of what is going on:

Listing 9.16 MembrApp.CPP-OnParseError Override

BOOL CMembrAppExtension::OnParseError(CHttpServerContext* pCtxt,

int nMethodRet)

{

UNUSED(nMethodRet);

CString errString;

if (pCtxt->m_pStream != NULL)

{

LPCTSTR pszObject = NULL;

switch (pCtxt->m_pECB->dwHttpStatusCode)

{

case HTTP_STATUS_BAD_REQUEST:

errString = "HTTP_BAD_REQUEST";

if (pCtxt->m_pECB->lpszQueryString)

pszObject = pCtxt->m_pECB->lpszQueryString;

else

pszObject = pCtxt->m_pECB->lpszPathInfo;

break;

case HTTP_STATUS_AUTH_REQUIRED:

errString = "HTTP_AUTH_REQUIRED"; break;

case HTTP_STATUS_FORBIDDEN:

errString = "HTTP_FORBIDDEN"; break;

case HTTP_STATUS_NOT_FOUND:

errString = "HTTP_NOT_FOUND"; break;

case HTTP_STATUS_SERVER_ERROR:

errString = "HTTP_SERVER_ERROR"; break;

case HTTP_STATUS_NOT_IMPLEMENTED:

errString = "HTTP_NOT_IMPLEMENTED";

pszObject = pCtxt->m_pECB->lpszQueryString;

break;

default:

errString = "HTTP_NO_TEXT";

pszObject = (LPCTSTR)

pCtxt->m_pECB->dwHttpStatusCode;

break;

}

CHttpServer::StartContent(pCtxt);

if (pszObject != NULL)

{

*pCtxt << pszObject;

*pCtxt << "\r\n";

*pCtxt << errString;

}

else

*pCtxt << errString;

CHttpServer::EndContent(pCtxt);

}

return TRUE;

}

Change the Parse Map

Now that we have learned about keeping the form and the DLL synchronized, let's change the parse map again to handle the new radio button.

Add another ITS_PSTR to the ON_PARSE_COMMAND macro for HandleApp, remembering to leave just a space and no comma between the arguments. Compile and run the program, and resubmit the form (see Figure 9.5).

Fig. 9.5

Assertion Failed!

CHttpServer::ParseDefaultParams found a problem. Indeed, we forgot to match up ON_PARSE_COMMAND and ON_PARSE_COMMAND_PARAMS. If we continue beyond the assertion failure, our new OnParseError routine tells the browser:

HandleApp HTTP_BAD_REQUEST

Bad request! OK, let's fix ON_PARSE_COMMAND_PARAMS by adding radioButton. Now compile, run, and reload.

Uh-oh. This time a parameter has a bad format. Of course, we forgot to change the HandleApp function to accommodate the extra parameter we defined in ON_PARSE_COMMAND_PARAMS, as shown in Listing 9.17.

Listing 9.17 MembrApp.CPP-Handle App to Handle radiobutton

void CMembrAppExtension::HandleApp(CHttpServerContext* pCtxt,

LPCTSTR pstr, LPCTSTR radio)

{

StartContent(pCtxt);

WriteTitle(pCtxt);

CString method = pCtxt->m_pECB->lpszMethod;

if (0 == method.CompareNoCase("POST"))

{

*pCtxt << _T("Thanks for the POST, ");

CString email = pstr;

*pCtxt << _T(email);

*pCtxt << _T("!\r\n");

CString msg;

msg = "You interest is ";

msg += radio;

msg += "\r\n";

*pCtxt << _T(msg);

}

else

*pCtxt << _T("This forms processor expects to be Posted

to!\r\n");

EndContent(pCtxt);

}

Again, compile, run, reload.

HandleApp HTTP_BAD_REQUEST

Hmmm. What is it this time? Turns out that the name radioButton in ON_PARSE_COMMAND_PARAMS does not match the name of the radio button in the form. Change the radio button's name to radioButton and reload. (Changing the form rather than changing ON_PARSE_COMMAND_PARAMS means we don't have to recompile.)

Voila!

The extension replies to the browser:

"Thanks for the POST, email@net.com! Your interest level in the club is sounds_interesting"

Granted, your usual development process doesn't include mistakes at every turn as it does here. But now we know what causes most errors and how to fix them.

I hope you agree that HTML files and their MFC ISAPI forms handlers should be carefully designed and planned in advance.

Other Form Input Elements

Now that we know the pitfalls to avoid, all that remains is to beef up our code and the HTML file so that we can handle the other elements in our example (Fig. 9.1). Since all input elements in a form are returned as strings, there is no difference in the handling of items such as check boxes and list boxes.

Change the Form

Our final version of the form club.htm looks like the code in listing 9.18.

Listing 9.18 Club.htm-Final HTML

<HTML>

<HEAD>

<TITLE>Member Application Form</TITLE>

</HEAD>

<BODY>

<CENTER>

<H3> Application for Membership</H3>

</CENTER>

<FORM action=" MembrApp.dll?HandleApp" method="post">

Your email address (required):<BR>

<INPUT name="Email" size=40>

<P>

From what you have heard of

The Camping Club

would you say:

<br>

<INPUT type="radio" name="radioButton" value="Need_now">I need it

now! <BR>

<INPUT type="radio" name="radioButton" value="sounds_interesting">

Sounds interesting. <BR>

<INPUT type="radio" name="radioButton" value="dont_care">Couldn't

care less. <BR>

<P>

Would you be interested in these other clubs? :

<UL>

<LI>

<INPUT NAME="TheGolfingClub"

type=checkbox checked>

<font size = +2>

The Golf Club

</font> - For the golf aficionado.

</LI>

<LI>

<INPUT NAME="TheHikingClub"

type=checkbox checked>

<font size = +2>

The Hiking Club

</font> - For lovers of the great outdoors.

</LI>

</UL>

<P><INPUT type=submit value="Sign Me Up">

<INPUT type=reset value="Clear the Form"></P>

</FORM>

</BODY>

</HTML>

Change the Parse Map

We need to add two more arguments to ON_PARSE_COMMAND and match up their names in ON_PARSE_COMMAND_PARAMS, as follows:

ON_PARSE_COMMAND(HandleApp, CMembrAppExtension, ITS_PSTR ITS_PSTR

ITS_PSTR ITS_PSTR)

ON_PARSE_COMMAND_PARAMS("Email radioButton=none_expressed

TheGolfClub=No TheHikingClub=No")

Now we have four string arguments, the last three of which have defaults.

Change the Handler Function

Along with adding the function parameters to handle the two new strings, let's clean up the code and make sure that the necessary email field is entered, as shown in Listing 9.19.

Listing 9.19 MembrApp.CPP-Final Handler

void CMembrAppExtension::HandleApp(CHttpServerContext* pCtxt,

LPCTSTR emailIn, LPCTSTR radio, LPCTSTR TheGolfClub,

LPCTSTR TheHikingClub)

{

StartContent(pCtxt);

WriteTitle(pCtxt);

CString method = pCtxt->m_pECB->lpszMethod;

if (0 == method.CompareNoCase("POST"))

{

CString email = emailIn;

if (email.IsEmpty())

*pCtxt << _T("Email address is required!");

else

{

CString msg;

msg += "Your email is ";

msg += emailIn;

msg += "<P>Your interest level is ";

msg += radio;

msg += "<P>The Golf Club: ";

msg += TheGolfClub;

msg += "<P>The Hiking Club: ";

msg += TheHikingClub;

*pCtxt << _T(msg);

}

}

else

*pCtxt << _T("This forms processor expects to be Posted to!\r\n");

EndContent(pCtxt);

}

Now our extension can process the form in Fig. 9.1!

Full source code and the form itself are on the companion CD to this book. I hope you now have a basis for developing fully functional, interactive applications on the Web.

From Here...

In this chapter, you learn about the framework provided by the ISAPI Extension Wizard and how to build on it using MFC. You use these ISAPI classes to build an ISAPI extension that can process a form.

To learn more about MFC and extending your Web server see the following chapters.


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