This chapter examines the ISAPI and CGI technologies. You can use CGI to create applications that extend a Web server, and you can use ISAPI to create DLLs that extend a Web server. In particular, ISAPI allows you to write scripts and filters and to interact dynamically with a user of your browser.
ISAPI technology is specific to the Internet Information Server that ships with Windows NT and to the Personal Web Server that ships with Microsoft FrontPage. It is, however, merely a specification, and other servers could conform to it if they wish. Several different vendors, including Borland, have created technology that allows ISAPI-based technology to be used in conjunction with NSAPI, which is a similar technology to ISAPI. (The NSAPI/ISAPI bridge is available in Delphi 3.0, for example.)
ISAPI programming is very similar to CGI programming. The only major difference is that you're creating a DLL instead of an executable. DLLs are advantageous because they can be loaded into the address space of the Web server. This capability gives them a leg up over CGI when you're considering performance. CGI, on the other hand, is a very simple specification to use, and it adapts itself easily to database applications.
ISAPI's reliance on the Windows platform might be a serious limitation in some other context, but because C++Builder also relies on Windows, discussing the topic at length in this book makes sense. Another feature to recommend ISAPI is its extreme simplicity. Friendly, powerful, easy-to-use APIs are the bread and butter of this book, and ISAPI fits the bill beautifully. Writing BCB database applications with ISAPI is, however, a bit tricky.
The first half of this chapter deals with ISAPI, and the second half deals with CGI. I will show how to retrieve data from both ISAPI DLLs and CGI applications, though my treatment of the subject is more complete in the section on CGI. If you are interested primarily in ISAPI, you should also take the time to read about the CGI database applications in the second half of this chapter.
The C++ code in this chapter relies on the presence of several HTML files that are quoted in full in this chapter and that are also available on the CD that ships with this book. However, the C++ code will not function properly unless the HTML files quoted in this chapter are in the correct location on your hard drive. See the README.TXT file that comes with the CD for additional information on setting up your system correctly to run this code. Some of these files are located in the root of the Chap26 directory on the CD. You will also need to use several databases' aliases described in the readme file.
As I implied in the "Overview" section for this chapter, the code discussed here requires one of the following:
At the time of this writing, it is not clear whether the Personal Web Server (PWS) will be available from other sources besides FrontPage. I downloaded the beta copy I used while writing this book directly from Microsoft's Web site. Checking to see if this copy is still available in that form is probably worthwhile.
You should also check to see if the Netscape or WebSite servers are now supporting the ISAPI API. An unfortunate and rather unseemly economic battle between Microsoft and its various competitors may slow down or even halt the spread of ISAPI as a standard, but checking to see if the battle has cooled somewhat is still worthwhile.
At any rate, the PWS is a useful piece of software for home users to explore. It will turn any Windows 95 machine into a Web server. I'm not sure how robust it will be under the strain of more than a few contiguous users. However, it is ideal if you want to set up a Web server in your home or in a small office. WebSite, from O'Reilly, is another fine product to turn to, particularly if you want to select a well-tested, robust server that can carry a heavy load.
You should also have a second computer equipped with a Web browser. This second computer can be running any operating system and can use virtually any software that supports Web browsing. I can think of no reason why you can't test most of this code on a single machine running a server, of course, but you will hardly get into the spirit of this enterprise if you're limited to that kind of setup.
I assume that most readers working in a business setting will have an intranet setup that will allow them to experiment with this technology. If you're working at home, I cannot stress too often the incredible value of setting up a network in your house. Network cards are very inexpensive these days. One of the ones I use in my home cost about $30 new. Network cable is also inexpensive, and both Windows 95 and Windows NT come equipped with all the software you need to set up a network that supports both file browsing with Windows Explorer and also TCP/IP.
When I first set up a network in my home, I thought I was pushing the extreme edge of modern technology. Now I simply take it for granted and can't understand how in the world I ever got along without it. Old computers don't have to die; they can just become Web servers. Small hard drives are extended easily by sharing storage space across multiple machines. After all, you don't need a separate copy of every application or every file on each machine. You can just share drives back and forth between machines, thereby saving a tremendous amount of space.
Most importantly, you can study and experiment with your network at your leisure and then apply that knowledge at work. A home network is an ideal place to educate yourself regarding this valuable technology.
The best place to go for information on ISAPI is the Microsoft MSDN or the Microsoft Internet SDK. These two sources provide most of the information you need that can't be found in this book.
Here's a place you can go on the Web if you want to find out more about the ISAPI specification:
http://www.microsoft.com/win32dev/apiext/isalegal.htm
Of course, I can't guarantee that this Web page will still be in existence when you read this book. However, two relatively stable sites on the Web that should serve as links to this spot are
http://www.microsoft.com/intdev/ http://www.microsoft.com/win32dev/
Check in at both these sites on fairly regular intervals to get updates on Win32 and Internet technology.
As stated in the Microsoft documentation, ISAPI allows you to "Write server-side scripts and filters to extend the capabilities of Microsoft Internet Information Server and other ISAPI Web servers."
ISAPI is a very easy-to-use yet extremely powerful technology that allows you to extend the reach of the Internet Information Server or the Personal Web Server. This tool allows you to make your Web site do pretty much whatever you want it to do. For example, it provides a means for you to
In the past, the best way to extend a Web server was to create CGI applications. These powerful tools were limited by their executable format. When you sent in a CGI-based request from a browser to a server, the CGI application in question usually had to be loaded into memory, which took a long time. Also, the CGI technology could be a bit awkward to use under some circumstances.
ISAPI is a method of writing DLLs that replace CGI applications. You can also write filters with ISAPI, though this subject is not covered in this book. ISAPI has the advantage of being easier to use than CGI, plus it can be much faster and make much better use of system resources. In particular, the following points help explain why ISAPI DLLs are better than CGI applications:
In this chapter, I will concentrate on writing DLLs that return datasets or that simply communicate with the user who is running a browser. I will not explore filters at all. For information on filters, you should go to the Microsoft Web site or browse the MSDN.
The file Httpext.h contains the key declarations used with ISAPI. This file should ship with C++Builder and is available with versions of the Microsoft SDK dated later than July 1996. It should also appear in the \include\vcl directory as ISAPI.HPP. Because it is a Windows 95- or Windows NT-based technology, you must be using a 32-bit compiler to access this technology. You can't use it from a 16-bit compiler, nor is it available on Windows 3.1.
Httpext.h contains the interface to the ISAPI technology created by Microsoft. At the time of this writing, C++Builder has no custom interface for ISAPI, and I will describe only how to use Microsoft's existing technology. However, ISAPI is extremely easy to use, and the addition of a custom object is not necessary for most users.
Three functions can serve as entry points to ISAPI DLLs. The first two listed here are mandatory, whereas the third is optional:
When you're creating an ISAPI DLL, you must export the first two of the three preceding functions. Implementing these two functions is the key to all ISAPI programming.
In C++Builder, DEF files are frowned upon. You should therefore make sure that Httpext.h has been modified to export both of these functions with __declspec(dllexport):
BOOL WINAPI __declspec(dllexport) GetExtensionVersion(HSE_VERSION_INFO *pVer); DWORD WINAPI __declspec(dllexport) HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB);
TerminateExtension is new in ISAPI 2.0. Its declaration looks like this:
BOOL WINAPI TerminateExtension( DWORD dwFlags );
TerminateExtension is called just before a connection is broken. It provides a place for you to deallocate memory allocated inside your DLL.
These three routines all contain the word Extension. This term is used because ISAPI DLLs extend the Internet Information Server or the Personal Web Server. (Remember, the Internet Information Server is Microsoft's Web server. If you want to turn an NT Server into a Web server, you use this tool. It ships with NT 4.0 and is installed automatically during the setup of that operating system.)
The GetExtensionVersion function must be exported from your DLL; otherwise, the server will not load your DLL. The only job of this function is to report the version of ISAPI that you expect to support.
You can always just cut and paste the GetExtensionVersion code into your DLLs. You need to change the function only slightly when you want to change the description passed in the lpszExtensionDesc field of the HSE_VERSION_INFO struct:
BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer) { pVer->dwExtensionVersion = MAKELONG(HSE_VERSION_MINOR, HSE_VERSION_MAJOR); strcpy(pVer->lpszExtensionDesc, "C++ Builder ISAPI DLL"); return (TRUE); };
The parameter passed to this function is declared in Httpext.h as follows:
typedef struct _HSE_VERSION_INFO { DWORD dwExtensionVersion; // Version info CHAR lpszExtensionDesc[HSE_MAX_EXT_DLL_NAME_LEN]; // Description } HSE_VERSION_INFO, *LPHSE_VERSION_INFO;
The two fields of the record are self-explanatory, with the first containing the ISAPI version number and the second holding a user-defined string describing the purpose of the DLL. The following are some constants declared in the DLL that are used in the preceding code:
#define HSE_MAX_EXT_DLL_NAME_LEN 256 #define HSE_VERSION_MAJOR 1 // major version of this spec #define HSE_VERSION_MINOR 0 // minor version of this spec
That's all you need to do to set up the first of the two mandatory functions in an ISAPI DLL. The next step, using HttpExtensionProc, is a bit more complex, so I will treat it in its own section.
The HttpExtensionProc routine is the entry point for the DLL. It serves the same purpose that the main() routine does in a C program, or that the main begin..end pair does in a Delphi program.
Here is a very simple example of an HttpExtensionProc routine:
DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB) { char ResultString[500]; DWORD resultLen; char *IsapiLogText = "ISAPI1 - Simple ISAPI Extension DLL"; strcpy(pECB->lpszLogData, IsapiLogText); pECB->dwHttpStatusCode = 200; char *HtmlInfo = "<HTML>" "<HEAD><TITLE>C++ Builder ISAPI DLL </TITLE></HEAD>" "<H1>ISAPI1 Test Results</H1>" "<BODY bgcolor=\"#0000FF\" text=\"#00FFFF\">" "Hello from a C++ Builder ISAPI DLL!<BR></BODY>" "</HTML>"; sprintf(ResultString, "HTTP/1.0 200 OK\nContent-Type: text/html\n" "Content-Length: %d\nContent:\n\n %s", 500, HtmlInfo); resultLen = lstrlen(ResultString); fprintf(out, ResultString); pECB->WriteClient(pECB->ConnID, ResultString, &resultLen, 0); return (HSE_STATUS_SUCCESS); }
If you queried a DLL containing this function from a browser, you would get a page back with this message:
ISAPI1 Test Results Hello from a C++ Builder ISAPI DLL!
In the next few paragraphs, I will describe the key points of the code shown here. However, I'll help you develop a complete understanding of this routine slowly in the next few sections of the chapter.
The HTML code for querying the DLL might look something like this:
<a href="/scripts/isapi1.dll">ISAPI1 Example</a>
Most of the body of the function is taken up with simple HTML code that provides basic information to the user:
char *HtmlInfo = "<HTML>" "<HEAD><TITLE>C++ Builder ISAPI DLL </TITLE></HEAD>" "<H1>ISAPI1 Test Results</H1>" "<BODY bgcolor=\"#0000FF\" text=\"#00FFFF\">" "Hello from a C++ Builder ISAPI DLL!<BR></BODY>" "</HTML>";
You also need to fill in a few fields of the EXTENSION_CONTROL_BLOCK:
char *IsapiLogText = "ISAPI1 - Simple ISAPI Extension DLL"; strcpy(pECB->lpszLogData, IsapiLogText); pECB->dwHttpStatusCode = 200;
The lpszLogData field contains the string that will be written to the log on your server. With the Personal Web Server, this log is kept by default in the Windows directory, though you can change this in the Administration section of the server applet found in the Control Panel.
The status code in this example is set to 200, which means "OK." Other possible values include the following:
HTTP_STATUS_BAD_REQUEST HTTP_STATUS_AUTH_REQUIRED HTTP_STATUS_FORBIDDEN HTTP_STATUS_NOT_FOUND HTTP_STATUS_SERVER_ERROR HTTP_STATUS_NOT_IMPLEMENTED
More information on the EXTENSION_CONTROL_BLOCK is provided in the section called "Working with the EXTENSION_CONTROL_BLOCK."
Notice the function pointer called WriteClient in the struct. You can call this function to send information back to the browser. When calling this function, you use the value in the ConnID field of the EXTENSION_CONTROL_BLOCK struct. ConnID is filled in for you automatically when the HttpExtensionProc function is called.
Before you look at the EXTENSION_CONTROL_BLOCK struct, let me show you a complete ISAPI DLL that uses the HttpExtensionProc function shown in this section.
The source code in Listing 26.1 shows how to create the simplest possible ISAPI
DLL. The goal is to remove all the complications from the code, and just include
enough information to make sure everything is working correctly.
Listing 26.1. The ISAPI1 example.
/////////////////////////////////////// // FILE: ISAPI1.CPP // PROJECT: ISAPI1.DLL // copyright (3) 1996 by Charlie Calvert // // This example shows how to use ISAPI, which is similar to creating a // CGI application. The code should return a simple string to an HTML // browser such as the Internet Explorer. // // Here is the HTML you output in a browser to call this ISAPI DLL: // // <HTML> // <HEAD> // <TITLE>CharlieC Home Page</TITLE> // </HEAD> // <BODY> // <H1>My Home Page </H1> // <P> // This is the home page for my home computer. // <P> // <A HREF="/scripts/isapi1.dll" >ISAPI One</A><BR> // </BODY> // </HTML> #include <vcl\vcl.h> #include <string.h> #include <stdio.h> #pragma hdrstop #include "..\..\utils\Httpext.h" USERES("Isapi1.res"); FILE *out; // GetExtensionVersion callback definition BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer) { fputs("Version", out); pVer->dwExtensionVersion = MAKELONG(HSE_VERSION_MINOR, HSE_VERSION_MAJOR); strcpy(pVer->lpszExtensionDesc, "C++ Builder ISAPI DLL"); return (TRUE); }; DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB) { AnsiString ResultString; DWORD resultLen; AnsiString IsapiLogText = "ISAPI1 - Simple ISAPI Extension DLL"; strcpy(pECB->lpszLogData, IsapiLogText.c_str()); AnsiString HtmlInfo = "<HTML>" "<HEAD><TITLE>C++ Builder ISAPI DLL </TITLE></HEAD>" "<H1>ISAPI1 Test Results</H1>" "<BODY bgcolor=\"#0000FF\" text=\"#00FFFF\">" "You are talking to a C++Builder ISAPI DLL." "<BR></BODY>" "</HTML>"; pECB->dwHttpStatusCode = 200; ResultString = Format( "HTTP/1.0 200 OK\nContent-Type: text/html\n" "Content-Length: %d\nContent:\n\n %s", OPENARRAY(TVarRec, (HtmlInfo.Length(), HtmlInfo))); resultLen = ResultString.Length(); fprintf(out, ResultString.c_str()); pECB->WriteClient(pECB->ConnID, ResultString.c_str(), &resultLen, 0); return (HSE_STATUS_SUCCESS); } #pragma argsused int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void*) { switch (reason) { case DLL_PROCESS_ATTACH: out = fopen("c:\\test.txt", "w+"); fprintf(out,"hello"); break; case DLL_PROCESS_DETACH: fprintf(out,"goodbye"); fclose(out); break; default: break; } return (TRUE); }
To use this DLL, you should copy it into a subdirectory of the scripts directory beneath the root for your Web. On my NT 4.0 machine, the subdirectory looks like this:
c:\winnt\system32\inetsrv\scripts\mystuff\isapi1.dll
In this case, I have created the directory called MYSTUFF, and it is used solely for storing ISAPI DLLs I have created. Your mileage may, of course, differ on your machine, depending on where you put the InetSrv directory and various other factors.
To call this DLL, you should add the following hyperlink to one of your HTML pages:
<A HREF="/scripts/mystuff/isapi1.dll" >ISAPI One</A><BR>
For example, here is a complete sample page:
<HTML> <HEAD><TITLE>An ISAPI Page</TITLE></HEAD> <BODY> <H1>My ISAPI Page</H1> <P>This is the home page for ISAPI on my computer.<P> <A HREF="/scripts/mystuff/isapi1.dll" >ISAPI One</A><BR> </BODY> </HTML>
When the user clicks the hyperlink, the ISAPI1 DLL will be called and the string "Hello from C++ Builder" will appear in the user's browser. If you did not put the ISAPI1.DLL in the MYSTUFF directory, then you should change the preceding HTML code to reflect that fact. Notice that the path you assign is relative to the InetSrv directory and does not, and should not, contain the entire path to your DLL.
Note that if you copy the ISAPI1.DLL into the MYSTUFF directory multiple times, you will need to shut down the WWW portion of the Internet server before each copy. The rule is that you can copy the DLL the first time for free, but after you have used it, it belongs to the server, and you need to shut down the WWW services on the server before you can copy an updated version of the file over the first copy. You can use the Internet Service Manager application to shut down the WWW services on the NT Server. This application should be in the Microsoft Internet Server group created in Windows Explorer or Program Manager (NT 3.51) at the time of the installation of the Internet Information Server. You can use the PWS applet in the Control Panel if you're using the Personal Web Server on Windows 95 or on the Windows NT Workstation.
By this point in the chapter, you should be able to create your first ISAPI DLL and call it from a Web browser on a second machine. The rest of this chapter explores ISAPI in more depth.
The following fairly complex record is passed as the sole parameter to HttpExtensionProc:
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];// log info 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 lpdwDataType ); } EXTENSION_CONTROL_BLOCK, *LPEXTENSION_CONTROL_BLOCK;
Notice that this record contains the ConnID field referenced previously and passed as the first parameter to WriteClient.
The first parameter of this record is used for version control. It should be set to the size of the EXTENSION_CONTROL_BLOCK. If Microsoft changes this structure, then they can tell which version of the structure they are dealing with by checking the size of the record as recorded in this field. You should never change any of the first three fields of this record; these fields are filled out ahead of time by ISAPI and can only be referenced, not changed, by your program.
The most important field of this record is probably the lpszQueryString, which contains information about the query passed in from the server. For example, suppose you have created a DLL called ISAPI1.DLL. To call this DLL, you would create an HREF that looks like this in one of your browser pages:
<A HREF="/scripts/mystuff/test1.dll">Test One</A>
If you want to query the DLL, you would edit the preceding line so that it looks like this:
<A HREF="/scripts/mystuff/test1.dll?MyQuery">Test One</A>
Given the second of the two HTML fragments listed here, your DLL would get called with the string "MyQuery" in the lpszQueryString parameter. Notice in particular the use of the question mark, followed by the query string itself.
You could, of course, change the query string at will. For example, you could write
<A HREF="/scripts/mystuff/test1.dll?ServerName">Test One</A>
To this query, the DLL might reply with the name of the server. You have no limits on what you can pass in this parameter, but the string after the question mark cannot have any spaces in it. If you need to use spaces, replace them with a plus sign: Instead of "Server Name", write "Server+Name". The string can be anything you want, and it is up to you to parse the information from inside the DLL as you like.
When you return information from the server back to the browser, you use the WriteClient function pointer that is part of this record.
Writers of CGI applications will notice that the syntax for sending query strings is familiar. Indeed, ISAPI follows many of the conventions of CGI, and most of the fields in the EXTENSION_CONTROL_BLOCK are simply borrowed directly to initialize this pointer; it is passed to you gratis by the Internet Information Server.
Another key field in the EXTENSION_CONTROL_BLOCK is the lpbData field, which contains any additional information sent to you by the browser. In particular, it is used to pass information associated with a Submit button. For example, if you have an HTML form with a number of fields in it, the information from these fields will be sent in the pointer called lpbData after the Submit button is clicked. The section of this chapter called "Getting Information from a Submit Button" focuses on how to handle this situation.
So far I have zeroed in on three key fields of the EXTENSION_CONTROL_BLOCK:
The best way to get a feeling for how the rest of the fields of EXTENSION_CONTROL_BLOCK
work is simply to mirror them back to
yourself in a browser. In other words, you
can create an HTML page that allows the user to call a custom ISAPI DLL. The purpose
of this ISAPI DLL is simply to snag the contents of each field of the EXTENSION_CONTROL_BLOCK,
format them in
HTML, and send them back to the browser. This will turn your browser
into a rather jazzy debugger that shows each of the fields in the EXTENSION_CONTROL_BLOCK.
Listing 26.2 contains the source to a DLL called IsapiVars that performs
this task.
Listing 26.2. The IsapiVars code
that mirrors back the parameters sent in the EXTENSION_CONTROL_BLOCK.
/////////////////////////////////////// // IsapiVars.cpp // Mirror back the information sent to an ISAPI DLL by the server // Copyright (c) 1997 by Charlie Calvert // #include <vcl\vcl.h> #pragma hdrstop #include "..\..\utils\Httpext.h" USERES("IsapiVars.res"); BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer) { pVer->dwExtensionVersion = MAKELONG(HSE_VERSION_MINOR, HSE_VERSION_MAJOR); strcpy(pVer->lpszExtensionDesc, "ISAPI Variables DLL"); return (TRUE); }; #define SIZE 2048 DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB) { char ResultString[SIZE * 2]; char HtmlInfo[SIZE]; char Buffer[SIZE]; DWORD StrSize; DWORD resultLen; char *IsapiLogText = "ISAPIVars from C++ Builder"; strcpy(pECB->lpszLogData, IsapiLogText); pECB->dwHttpStatusCode = 200; sprintf(HtmlInfo, "<HTML><TITLE>Fields of EXTENSION_CONTROL_BLOCK</TITLE>" "<H1>Test server results</H1><BODY>" "Size = %d<BR>" "Version = %.8x<BR>" "ConnID = %.8x<BR>" "Method = %s<BR>" "Query = %s<BR>" "PathInfo = %s<BR>" "PathTranslated = %s<BR>" "TotalBytes = %d<BR>" "AvailableBytes = %d<BR>" "ContentType = %s<BR><BR>" "<H1>Calls to GetServerVariable</H1>", pECB->cbSize, pECB->dwVersion, pECB->ConnID, pECB->lpszMethod, pECB->lpszQueryString, pECB->lpszPathInfo, pECB->lpszPathTranslated, pECB->cbTotalBytes, pECB->cbAvailable, pECB->lpszContentType); StrSize = sizeof(Buffer); pECB->GetServerVariable(pECB->ConnID, "REMOTE_ADDR", &Buffer, &StrSize); AnsiString VarString("REMOTE_ADDR = " + AnsiString(Buffer) + "<BR>"); StrSize = sizeof(Buffer); pECB->GetServerVariable(pECB->ConnID, "REMOTE_HOST", &Buffer, &StrSize); VarString += "REMOTE_HOST = " + AnsiString(Buffer) + "<BR>"; StrSize = sizeof(Buffer); pECB->GetServerVariable(pECB->ConnID, "REMOTE_USER", &Buffer, &StrSize); VarString += "REMOTE_USER = " + AnsiString(Buffer) + "<BR>"; StrSize = sizeof(Buffer); pECB->GetServerVariable(pECB->ConnID, "SERVER_NAME", &Buffer, &StrSize); VarString += "SERVER_NAME = " + AnsiString(Buffer) + "<BR>"; StrSize = sizeof(Buffer); pECB->GetServerVariable(pECB->ConnID, "SERVER_PORT", &Buffer, &StrSize); VarString += "SERVER_PORT = " + AnsiString(Buffer) + "<BR>"; StrSize = sizeof(Buffer); pECB->GetServerVariable(pECB->ConnID, "SERVER_PROTOCOL", &Buffer, &StrSize); VarString += "SERVER_PROTOCOL = " + AnsiString(Buffer) + "<BR>"; StrSize = sizeof(Buffer); pECB->GetServerVariable(pECB->ConnID, "SERVER_SOFTWARE", &Buffer, &StrSize); VarString += "SERVER_SOFTWARE = " + AnsiString(Buffer) + "<BR>"; StrSize = sizeof(Buffer); pECB->GetServerVariable(pECB->ConnID, "HTTP_ACCEPT", &Buffer, &StrSize); VarString += "HTTP_ACCEPT = " + AnsiString(Buffer) + "<BR>"; StrSize = sizeof(Buffer); pECB->GetServerVariable(pECB->ConnID, "URL", &Buffer, &StrSize); VarString += "URL = " + AnsiString(Buffer) + "<BR><BR><BR>"; StrSize = sizeof(Buffer); pECB->GetServerVariable(pECB->ConnID, "ALL_HTTP", &Buffer, &StrSize); VarString += "ALL_HTTP = " + AnsiString(Buffer) + "<BR>"; strcat(HtmlInfo, VarString.c_str()); sprintf(ResultString, "HTTP/1.0 200 OK\nContent-Type: text/html\n" "Content-Length: %d\nContent:\n\n %s </HTML>", SIZE, HtmlInfo); StrSize = strlen(ResultString); pECB->WriteClient(pECB->ConnID, ResultString, &StrSize, 0); return (HSE_STATUS_SUCCESS); } int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void*) { return 1; }
To call this DLL, you should create an HTML script that contains the following line:
<A HREF="/scripts/mystuff/isapivars.dll">Test One</A> <BR>
Of course, the actual path shown in your code may be somewhat different from what I show here.
The HttpExtensionProc for this DLL is broken into two sections. The first retrieves all the main fields from the EXTENSION_CONTROL_BLOCK, and the second goes to town on one particular field, which is a function called GetServerVariable.
The code that parses the main fields of the EXTENSION_CONTROL_BLOCK is fairly straightforward:
sprintf(HtmlInfo, "<HTML><TITLE>Fields of EXTENSION_CONTROL_BLOCK</TITLE>" "<H1>Test server results</H1><BODY>" "Size = %d<BR>" "Version = %.8x<BR>" "ConnID = %.8x<BR>" "Method = %s<BR>" "Query = %s<BR>" "PathInfo = %s<BR>" "PathTranslated = %s<BR>" "TotalBytes = %d<BR>" "AvailableBytes = %d<BR>" "ContentType = %s<BR><BR>" "<H1>Calls to GetServerVariable</H1>", pECB->cbSize, pECB->dwVersion, pECB->ConnID, pECB->lpszMethod, pECB->lpszQueryString, pECB->lpszPathInfo, pECB->lpszPathTranslated, pECB->cbTotalBytes, pECB->cbAvailable, pECB->lpszContentType);
This code is nothing more than a simple call to sprintf. The goal is simply to take the fields of the EXTENSION_CONTROL_BLOCK and mirror them back to the user's browser. To do so, all you need to do is set up a legitimate HTML document and then add a human-readable version of the struct to the body of the form.
The output
from this part of the code looks like the screen shot shown in Figure
26.1.
FIGURE 26.1.
The main fields of the EXTENSION_CONTROL_BLOCK shown in
a browser.
Working with GetServerVariable is a bit more complicated. As a result, I will give this description a whole section so that it can have plenty of room in which to knock about.
You can use GetServerVariable to retrieve information from a server just as you would request information inside a CGI application. Here is an example of calling the routine:
#define SIZE 2048; ... char Buffer[SIZE]; ... StrSize = sizeof(Buffer); pECB->GetServerVariable(pECB->ConnID, "REMOTE_ADDR", &Buffer, &StrSize); AnsiString VarString("REMOTE_ADDR = " + AnsiString(Buffer) + "<BR>");
This function takes a connection ID in the first parameter, a constant in the second parameter, a buffer in the third parameter, and the length of the buffer in the fourth parameter:
BOOL WINAPI GetServerVariable( HCONN hConn, LPSTR lpszVariableName, LPVOID lpvBuffer, LPDWORD lpdwSizeofBuffer );
As a rule, you will have to reset the fourth parameter before each call to this function because the function itself returns the length of the string found in the lpvBuffer parameter in the lpdwSizeOfBuffer parameter. You definitely don't want to raise any exceptions or cause any errors to occur in your DLL, so I suggest playing it safe when calling this function.
The preceding code first sets the length of the buffer that will hold the information retrieved from the server. It then calls the server and asks for information. In this case, it asks for the content length of the information sent by the server.
You can pass the following strings in the second parameter of GetServerVariable:
AUTH_TYPE | Type of authentication used. |
CONTENT_LENGTH | Number of bytes you can expect to receive from the client. |
CONTENT_TYPE | Type of information in the body of a POST request. |
PATH_INFO | Trailing part of the URL after the script name. |
PATH_TRANSLATED | PATH_INFO with any virtual pathname expanded. |
QUERY_STRING | Info following the "?" in the URL. |
REMOTE_ADDR | IP address of the client (could be a gateway or firewall). |
REMOTE_HOST | Hostname of the client or agent of the client. |
REMOTE_USER | Username supplied by the client and authenticated by the server. |
UNMAPPED_REMOTE_USER | Username before ISAPI mapped to an NT user account. |
REQUEST_METHOD | The HTTP request method. |
SCRIPT_NAME | The name of the script program being executed. |
SERVER_NAME | The server name. |
SERVER_PORT | The TCP/IP port on which the request was received. |
SERVER_PORT_SECURE | If the request is on the secure port, then this will be 1; otherwise, it is 0. |
SERVER_PROTOCOL | Usually HTTP/1.0. |
SERVER_SOFTWARE | The name of the server software. |
ALL_HTTP | All headers not already parsed into one of the previous variables. |
HTTP_ACCEPT | The special-case HTTP header. |
URL | New for version 2.0. The base portion of the URL. |
You can see examples of the type of information returned by calling GetServerVariable
in Figure 26.2. Note that this screen shot simply shows the lower half of the
window
shown in Figure 26.1.
FIGURE 26.2.
The results of making several repeated calls to GetServerVariable.
Note that many of the preceding pieces of information are automatically passed in the EXTENSION_CONTROL_BLOCK record. Therefore, you usually do not need to call GetServerVariable, but you can if you need to, particularly if you want to retrieve information with ReadClient and need to know how much information to read.
Most of the time, you don't need to call ReadClient. However, if the amount of data being sent by the browser is larger than 48KB, you will need to call ReadClient to get the rest of the data.
Before completing the discussion of how this DLL works, I want to take a moment to get some housekeeping chores out of the way. In particular, I want to mention the DLLEntryPoint routine, which appears at the bottom of the DLL.
All DLLs have an entry point that is called automatically. You don't have to respond to this entry point, but doing so is usually a good idea. In this case, I simply open a text file for debugging purposes:
#pragma argsused int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void*) { switch (reason) { case DLL_PROCESS_ATTACH: out = fopen("c:\\test.txt", "w+"); fprintf(out,"hello"); break; case DLL_PROCESS_DETACH: fprintf(out,"goodbye"); fclose(out); break; default: break; } return (TRUE); }
Nothing about the code shown here is mandatory. You don't have to open a text file and write to it; the code serves no other purpose than to give you a simple means of debugging your DLL. In particular, it creates a text file on the server to leave a record of your DLL's behavior. This file is helpful, particularly if you're learning ISAPI and have problems simply creating a valid DLL that exports the key functions.
NOTE: You can debug an ISAPI DLL by using several different means. One rather fancy method is to load the entire server into the stand-alone debugger, load your ISAPI DLL, and then set a break point inside it.
Although effective, the technique described in the preceding paragraph can be overkill. My suggestion is not to be too proud about resorting to the old-fashioned technique of creating a text file and writing to it. You can produce very detailed reports in this fashion, and they can show you exactly what is going on in your DLL. The only flaw in this system is that entering all those fprintf statements takes a bit of time.
The example shown here has a more valuable purpose then merely showing one rather simple-minded way to debug an ISAPI DLL. In particular, it reminds you of the proper way to handle the entry point of a DLL. In addition to the DLL_PROCESS_ATTACH and DLL_PROCESS_DETACH notifications are two others called DLL_THREAD_ATTACH and DLL_THREAD_DETACH. Because multiple clients could be using your DLL at the same time, DLL_THREAD_ATTACH can be a very important entry point to use when you're debugging or constructing your DLL.
Often you get information sent to you from an HTML form that has a Submit button on it. As long as this information is shorter than 49KB, you can assume that it will be available in the lpbData field of TExtensionControlBlock. Otherwise, you will need to call ReadClient. Here is how you would typically read the information from the pointer passed in this field:
AnsiString S; if (pECB->lpbData != NULL) { S = (char *)pECB->lpbData; S = Parse(S); } else S = "Error occurred on get from lpbData field";
This code first checks to see if lpbData is non-NULL. This type of conservative coding is a necessity in ISAPI, as you don't want errors to be occurring way over on the server side, where it is hard to see what is going on. The fragment shown here then typecasts lpbData so that you can place its contents in a variable of type AnsiString. It then passes the string to a user-defined function called Parse that handles the string passed by the server. If something goes wrong, the string variable is set equal to an error message and then returned to the user so he or she can view it in a browser.
If you want to see exactly what information is available in the lpbData field, you can use the following two functions to echo the data back to your Web browser:
AnsiString SetupHeader(AnsiString &ResultString, AnsiString S, int &Len) { char *HeaderInfo = "HTTP/1.0 200 OK\nContent-Type: text/html\n" "Content-Length: %d\nContent:\n\n %s </HTML>"; ResultString.SetLength(S.Length() + strlen(HeaderInfo) + 1); Len = ResultString.Length(); sprintf(ResultString.c_str(), HeaderInfo, Len, S); return ResultString; } //HttpExtensionProc callback definition DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB) { AnsiString ResultString; int ResultLen; char *IsapiLogText = "Mirror lpbData"; strcpy(pECB->lpszLogData,IsapiLogText); pECB->dwHttpStatusCode = 200; AnsiString S; if (pECB->lpbData != NULL) { S = (char *)pECB->lpbData; } else S = "Error occurred get lpbData field"; SetupHeader(ResultString, S, ResultLen); pECB->WriteClient(pECB->ConnID, ResultString.c_str(), &(DWORD)ResultLen, 0); return (HSE_STATUS_SUCCESS); }
The first function, called SetupHeader, is just a utility routine that automates the process of setting up a header. It forces you to pass in the variable that is sent in the third parameter of WriteClient. I do this simply to help remind myself that I have to initialize this variable before passing it to the server.
The second routine simply mirrors the lpbData field back to the user of the DLL. On the CD that accompanies this book, you will find a DLL called MirrorData and an HTML form called MirrorDataTest.htm, which looks like this:
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> <meta name="GENERATOR" content="Microsoft FrontPage 2.0"> <title>MirrorDataTest</title> </head> <body bgcolor="#0000FF" text="#00FFFF"> <h1>View lpbData Test</h1> <form action="/scripts/Books/BUnleash/MirrorData.dll" method="POST"> <p>Enter some information: </p> <p><textarea name="SendData" rows="16" cols="74"></textarea></p> <p>Enter More Information</p> <p><input type="text" size="76" name="SendData"></p> <p><input type="submit" name="SendData" value="Submit"></p> </form> </body> </html>
You can use this code to test the HttpExtensionProc that mirrors back the value of lpbData. Note that the HTML form contains two fields for entering text, the first called SendData and the second called MoreData. If you have only one field and you type Fast into it, the DLL would mirror back the following to the user:
SendData=Fast&SendData=Submit
If you have two fields, as in the HTML code shown here, then the following would be mirrored back if the first field contains Fast and the second contains Loose:
SendData=Fast&MoreData=Loose&SendData=Submit
If you break down this text into three separate fields, they would look like this:
SendData=Fast &MoreData=Loose &SendData=Submit
This text says, in effect, the SendData field has the word Fast in it, the MoreData field has the word Loose in it, and the SendData button is a Submit button.
Assume that you have an HTML form with the code shown in Listing
26.3.
Listing 26.3. The HTML code for
a Web page that uses ISAPI to interact with the user.
<html> <head> <title>Talking ISAPI Test</title> </head> <body bgcolor="#0000FF" text="#00FFFF"> <h1>Talking ISAPI Test</h1> <p>Press this button to see more of the simple ISAPI test:</p> <form action="/scripts/Books/BUnleash/IsapiTalk.dll" method="POST"> <p>Enter your name: <input type="text" size="20" name="SendName"></p> <p><input type="submit" name="SendName" value="Submit"></p> </form> </body> </html>
This code will produce a form that contains a text area where the user can enter his or her name and a button called Submit. Given this form, you can expect the lpbData field to contain the following string, assuming the user enters the word Sammy in the name field:
SendName=Sammy&SendName=Submit
To understand what is happening here, note the BODY of the HTML statement composed on the server as reflected in the following excerpt from the SetUpResString function shown previously:
`<BODY>lpbData = %s </BODY>' +
If you study the code in the HttpExtensionProc function, you will see that it uses the Format routine to substitute the value of ECB.lpbData for the %s variable in the preceding piece of code. (If you don't understand how Format works, see the BCB documentation, or my references to this method in Chapter 3, "C++Builder and the VCL.")
After you get the information from the form in the lpbData parameter, you can parse it and return information to the user. For example, you could extract the number 23 from the preceding example and then square it and return it to the user. Doing so would in effect allow you to get information from the user, namely a number, perform a mathematical action on the number, and then return the result to the user. This means you're creating dynamic, interactive Web pages on-the-fly, which is the current sine qua non of Internet programming!
Listing 26.4 shows the complete code for a program that will reply to a user who
enters his or her name into a page of a Web browser and submits it to the DLL. The
HTML that
accompanies this program is shown in Listing 26.5.
Listing 26.4. The code for the ISAPITalk
DLL.
/////////////////////////////////////// // IsapiTalk.cpp // Mirror back the information sent to an ISAPI DLL by the server // Copyright (c) 1997 by Charlie Calvert // #include <vcl\vcl.h> #include <fstream.h> #include <dir.h> #pragma hdrstop #include "..\..\utils\Httpext.h" USERES("IsapiTalk.res"); BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer) { pVer->dwExtensionVersion = MAKELONG(HSE_VERSION_MINOR, HSE_VERSION_MAJOR); strcpy(pVer->lpszExtensionDesc, "C++ Builder ISAPI DLL"); return (TRUE); }; // Remember: IOStream deals with OS paths, not relative path to server! BOOL GetResultString(AnsiString &Result, AnsiString Path) { fstream InFile; AnsiString FileName(Path + "\\BUnleash\\TalkTest\\TalkReply.htm"); char ch; char S[500]; InFile.open(FileName.c_str(), ios::in, filebuf::openprot); if (!InFile) { // If we couldn't get file, then what directory were we in? Result = "<H>Error reading stream!</H>"; FileName = ExtractFilePath(FileName); if (chdir(FileName.c_str()) == 0) { getcurdir(0, S); Result += S; } else Result += "Could not get file, nor find current directory!"; return FALSE; } while (InFile.get(ch)) Result += ch; return TRUE; } AnsiString Parse(AnsiString &S) { int i = S.Pos("&"); S.SetLength(i - 1); S = strrev(S.c_str()); i = S.Pos("="); S.SetLength(i - 1); S = strrev(S.c_str()); return S; } //HttpExtensionProc callback definition DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB) { AnsiString Header("HTTP/1.0 200 OK\nContent-Type: text/html\nContent-Length:%d\nContent:\n\n"); AnsiString ResultString; int resultLen; char *IsapiLogText = "ISAPI Talk"; strcpy(pECB->lpszLogData,IsapiLogText); pECB->dwHttpStatusCode = 200; // build the HTML result string AnsiString S; if (pECB->lpbData != NULL) { S = (char *)pECB->lpbData; S = Parse(S); } else S = "Error occurred get lpbData field"; AnsiString Date(DateToStr(Now())); AnsiString Time(TimeToStr(Now())); GetResultString(ResultString, pECB->lpszPathTranslated); ResultString = Format(ResultString, OPENARRAY(TVarRec, (S, Date, Time))); resultLen = ResultString.Length(); Header = Format(Header, OPENARRAY(TVarRec, (resultLen))); pECB->WriteClient(pECB->ConnID, ResultString.c_str(), &(DWORD)resultLen, 0); return (HSE_STATUS_SUCCESS); } int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void*) { return 1; }
Listing 26.5. The HTML file, called TalkReply.htm, used in the reply generated by the ISAPITalk DLL.
<html> <head> <title>This is a simple htm file</title> </head> <body bgcolor="#0000FF" text="#00FFFF"> <h1>Talking HTML Reply</h1> <p> </p> <p>Welcome, %s, to the talking HTML file!</p> <p>It's remarkable that you should drop by today, %s, at %s (PST), as I was just thinking about you. </p> </body> </html>
This program allows an HTML browser to work interactively with a Web server. Screen shots of before and after a query is made are shown in Figure 26.3 and Figure 26.4, respectively.
When you're working with this program, note that I assume you have placed the TalkReply.htm program in the following location relative to the root of your Web:
\\BUnleash\\TalkTest\\TalkReply.htm
The ISAPITalk DLL might receive the following string from the user who clicks the Submit button, asking that a number be squared:
SendName=Sammy&SendName=Submit
FIGURE 26.3. Preparing to query the ISAPITalk DLL.
FIGURE 26.4. The results of querying the ISAPITalk DLL.
Given this input, the preceding code would return the following string to the user across the Internet:
Welcome, Sammy, to the talking HTML file! It's remarkable that you should drop by today, 2/15/97, at 10:39:29 PM (PST), as I was just thinking about you.
In short, the user enters the string "Sammy", and the ISAPI DLL mirrors this information back and even adds in the current date and time as it is reported on the server side. This process sounds trivial, but the key issue here is that this activity is taking place dynamically on the Internet.
The function that parses the data sent by the user looks like this:
AnsiString Parse(AnsiString &S) { int i = S.Pos("&"); S.SetLength(i - 1); S = strrev(S.c_str()); i = S.Pos("="); S.SetLength(i - 1); S = strrev(S.c_str()); return S. }
The code uses the strrev function from the Standard C Library to expedite the task of parsing out all the unnecessary data sent by the server.
The ISAPITalk program does not imbed any HTML code inside the C++ code that makes the DLL run. Instead, it reads in an HTML file and uses the VCL Format function to fill in the blank fields of the form shown in Listing 26.5.
The code for reading in the HTML file goes to considerable lengths to properly handle any errors that occur:
if (!InFile) { // If we couldn't get file, then what directory were we in? Result = "<H>Error reading stream!</H>"; FileName = ExtractFilePath(FileName); if (chdir(FileName.c_str()) == 0) { getcurdir(0, S); Result += S; } else Result += "Could not get file, nor find current directory!"; return FALSE;
}
The issue here is that the file you want to load might be missing or might be in another directory than the one you suppose. The code shown here attempts to find the directory in which the DLL expects the HTML file to reside and to mirror that information back to the user on the off chance that an error occurs. This information can help you fix any broken links in your program without too much fussing around.
NOTE: The reason the ISAPITalk program reads in the HTML for the reply form from a file is simply that I don't like embedding HTML code inside C++ code. No technical reason prevents me from embedding HTML inside C++, but doing so does tend to be confusing. In particular, embedding the code makes it difficult for you to generate an attractive form that has just the proper look you want to produce. If you separate the HTML from your C++ code, you can use your favorite HTML editor to produce just the look you want.
The discussion of the ISAPITalk program has one final leg that needs to be completed before the race is done. This last portion of the journey involves getting the proper path to the HTML file that you read in while generating the reply form.
When you're writing ISAPI applications, you need to distinguish between the relative path you use when talking to the Web server and the absolute OS path you use when executing functions inside your DLL. Server paths should always be relative to the root of your Web, but OS paths are concerned with the current drive and directory.
If you embed the name of an HTML file in a hyperlink that is part of a Web path, you're dealing with relative server paths. If you call the C library chdir or getcurdir functions, then you're working with an OS path. For example, I need to pass an OS-based path to the function in the ISAPITalk program that reads an HTML file from disk.
Finding out the current OS path is relatively easy because ISAPI passes the absolute path to the root of the Web in the lpszPathTranslated field of the EXTENSION_CONTROL_BLOCK. For example, if the root of your Web is C:\WEBSHARE\WWWROOT, that path is passed to you in the lpszPathTranslated field.
The relative path you use in your Web pages is something that you usually determine while laying out the Web itself. Sometimes you have to refer to this path in your code. In particular, you will often reference this path if you're creating HTML on-the-fly. Most of the time, you simply have to determine the correct path by looking at the position of your files relative to the root of your Web.
Here is an example of a relative path used in an HTML file:
\\BUnleash\\TalkTest\\TalkReply.htm
Here is the OS path to the same file:
C:\WEBSHARE\WWWROOT\\BUnleash\\TalkTest\\TalkReply.htm
Whatever you do, don't ever embed full OS pathnames in either your HTML or your ISAPI DLLs. If you do, then you will have to edit your code and your HTML whenever you change the location of your Web site. That is much too much work. Furthermore, from a security point of view, telling the users of your Web any more than they have to know about the actual layout of your hard drive is probably not a good idea.
To find out more about this subject, you can examine the ISAPITalk DLL on the CD that accompanies this book. This program provides an example of finding out the exact path to a file on the server so that you can load it into your DLL.
That's most of what I want to say about ISAPI in this chapter. This information should be enough to get you up and running and having some fun with this great technology.
In this section you will see a simple example for retrieving data from an ISAPI DLL. The CGI examples featured later in this book go into more depth on database techniques to use in this type of program. You can easily convert the CGI code into code that can be used with ISAPI.
The sample DLL shown in Listing 26.6 retrieves all the data from
the Country table
in the BCDEMOS database and shows it to the user in an HTML table.
Listing 26.6. The IsapiData program
shows how to retrieve data from a database and display it in a
browser.
/////////////////////////////////////// // IsapiData // Return Database rows from an ISAPI DLL // Copyright (c) 1997 by Charlie Calvert // Thanks to David Intersimone and Roland Fernandez // #include <vcl\vcl.h> #include "..\..\utils\Httpext.h" #pragma hdrstop AnsiString TableToHtml(TDataSet *Table) { int i; AnsiString Result; Result = "<TABLE BORDER>\n"; for (i = 0; i < Table->Fie ldCount; i++) { Result = Result + Format("<TH>%s</TH>", OPENARRAY(TVarRec, (Table->Fields[i]->FieldName.c_str()))); } while (!Table->Eof) { Result = Result + "<TR>"; for (i = 0; i < Table->FieldCount; i++) { Result = Result + Format("<TD>%s</TD>", OPENARRAY(TVarRec, (Table->Fields[i]->AsString.c_str()))); } Result = Result + "</TR>\n"; Table->Next(); } return Result + "</TABLE>\n"; } AnsiString _stdcall _export GetData() { TTable *Table; AnsiString Result; Table = new TTable(Application); Table->DatabaseName = "DBDEMOS"; Table->TableName = "Country"; Table->Open(); Result = TableToHtml(Table); Table->Close(); delete Table; return Result; } BOOL WINAPI _export GetExtensionVersion(HSE_VERSION_INFO *pVer) { pVer->dwExtensionVersion = MAKELONG(HSE_VERSION_MINOR, HSE_VERSION_MAJOR); strcpy(pVer->lpszExtensionDesc, "ISAPI Variables DLL"); return (TRUE); }; void TextWrite(AnsiString S) { FILE *F; S = S + "\n"; F = fopen("c:\\foo.txt", "w+"); fprintf(F, S.c_str()); fclose(F); } void TextAppend(AnsiString S) { FILE *F; S = S + "\n"; F = fopen("c:\\foo.txt", "a+"); fprintf(F, S.c_str()); fclose(F); } DWORD WINAPI _export HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB) { AnsiString Header("HTTP/1.0 200 OK\nContent-Type: text/html\nContent-Length: " " %d\nContent:\n\n"); AnsiString ResultString; int resultLen; char *IsapiLogText = "ISAPI1 - Simple BC++ ISAPI Extension DLL"; strcpy(pECB->lpszLogData,IsapiLogText); pECB->dwHttpStatusCode = 200; ResultString = GetData(); Header = Format(Header, OPENARRAY(TVarRec, (ResultString.Length()))); Header = Header + ResultString; resultLen = Header.Length(); pECB->WriteClient(pECB->ConnID, Header.c_str(), &(DWORD)resultLen, 0); return (HSE_STATUS_SUCCESS); } int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void*) { switch (reason) { case DLL_PROCESS_ATTACH: // TextWrite("DllLoaded"); break; case DLL_PROCESS_DETACH: // TextAppend("Dll UnLoaded"); break; } return 1; }
BCB makes creating database-centered DLLs very simple. To open a table, all you have to do is write the standard code for creating and opening a database:
AnsiString _stdcall _export GetData() { TTable *Table; AnsiString Result; Table = new TTable(Application); Table->DatabaseName = "DBDEMOS"; Table->TableName = "Country"; Table->Open(); Result = TableToHtml(Table); Table->Close(); delete Table; return Result; }
In this example I create and open a table, being sure to set up the DatabaseName and TableName properties. I then call a special routine specifically designed to convert the data in a table into an HTML form. Finally I close and delete the table.
If you wanted to create a simpler test program, you could simply access the first field of data in the table:
Result = Table->Fields[0]->AsString.c_str()
As you will see in the CGI examples presented next, there is no reason why you cannot add a DataModule to this kind of program. In fact, you can extend ISAPI DLLs to the point where they handle relatively complex database chores such as processing queries or editing records. I want to thank David Intersimone and Roland Fernandez for showing me that these applications could be considerably simpler than I thought at first.
CGI programming is similar to ISAPI programming, except that you create executables rather than DLLs, and it works from inside any respectable server, rather than only from Microsoft's servers. To get input from a browser, you read the DOS environment variables.
Here is an example of reading a CGI variable:
char * S;
S = getenv("QUERY_STRING");
Here is an example of writing to a browser from a CGI application:
printf("This string will appear in a browser.");
As you can see, writing this code is not exactly rocket science. Because of its simplicity, I will whip right through several simple examples and then get on to some database applications. I don't mean to denigrate CGI because it is so simple to use. I don't like anything more than simple APIs, and CGI is one of the simplest. This technology is very powerful, and like ISAPI, it can quickly upgrade a Web site from something pretty average to something pretty spectacular.
CGI is so simple that I almost don't need to include the most basic
of possible
examples. However, enough things can go wrong with Web-based applications that you
need one simple example that you know will work. The code in Listing 26.7 provides
that example.
Listing 26.7. The SimpleCGI application.
/////////////////////////////////////// // SimpleHelp.cpp // Using an object to make CGI easier // Copyright (3) 1997 by Charlie Calvert // #include <vcl\vcl.h> #pragma hdrstop char S[] = "TO HIS COY MISTRESS<P>\n" "Had we but world enough and time,<BR>\n" "This coyness, lady, were no crime.<BR>\n" "We would sit down, and think which way<BR>\n" "To walk, and pass our long love's day.<BR>\n" "Thou by the Indian Ganges' side<BR>\n" "Shouldst rubies find: I by the tide<BR>\n" "Of Humber would complain. I would<BR>\n" "Love you ten years before the Flood:<BR>\n" "And you should if you please refuse<BR>\n" "Till the conversion of the Jews.<BR>\n" "My vegetable love should grow<BR>\n" "Vaster than empires, and more slow.<BR>\n" "An hundred years should go to praise<BR>\n" "Thine eyes, and on thy forehead gaze.<BR>\n" "Two hundred to adore each breast:<BR>\n" "But thirty thousand to the rest.<BR>\n" "An age at least to every part,<BR>\n" "And the last age should show your heart.<BR>\n" "For lady, you deserve this state;<BR>\n" "Nor would I love at lower rate.<BR>\n" "But at my back I always hear<BR>\n" "Time's winged chariot hurrying near:<BR>\n" "And yonder all before us lie<BR>\n" "Deserts of vast eternity.<BR>\n" "Thy beauty shall no more be found,<BR>\n" "Nor, in thy marble vault, shall sound<BR>\n" "My echoing song; then worms shall try<BR>\n" "That long preserved virginity:<BR>\n" "And your quaint honour turn to dust;<BR>\n" "And into ashes all my lust.<BR>\n" "The grave's a fine and private place,<BR>\n" "But none, I think , do there embrace.<BR>\n" "Now therefore, while the youthful hue<BR>\n" "Sits on thy skin like morning dew,<BR>\n" "And while thy willing soul transpires<BR>\n" "At every pore with instant fires,<BR>\n" "Now let us sport us while we may;<BR>\n" "And now, like am'rous birds of prey,<BR>\n" "Rather at once our time devour,<BR>\n" "Than languish in his slow chapped power<BR>\n" "Let us roll all our strength, and all<BR>\n" "Our sweetness, up into one ball:<BR>\n" "And tear our pleasures with rough strife,<BR>\n" "Through the iron gates of life.<BR>\n" "Thus, though we cannot make our sun<BR>\n" "Stand still, yet we will make him run.<BR>\n" "-- Andrew Marvell"; void main(void) { setvbuf(stdout, NULL, _IONBF, 0); printf("\nContent-type: text/html\n\n"); printf("<html>\n"); printf("<body>\n"); printf("<body bgcolor=\"\#0000FF\" text=\"\#00FFFF\" link=\"\#FFFF00\"" "vlink=\"\#00FF00\" alink=\"\#FFFFFF\">"); printf(S); printf("</body>\n"); printf("</html>\n"); }
This console application has no main form; its output appears only in a browser,
as shown in Figure 26.5.
Figure 26.5
The Simple CGI application.
To create the SimpleCGI application, start a new project and remove the main form. Save the project file in its own directory, and then choose Options | Project | Linker and set the Application Type to Console Application. You can also use File | New and select Console App from the App Expert dialog.
Remove all the WinMain business, and replace it with a simple DOS-style main block:
void main(void) { setvbuf(stdout, NULL, _IONBF, 0); printf("\nContent-type: text/html\n\n"); printf("<html>\n"); printf("<body>\n"); printf("...the sessions of sweet silent thought..."); printf("</body>\n"); printf("</html>\n"); }
This complete CGI application needs only be compiled and then copied to a server.
You call a CGI application exactly as you would an ISAPI DLL:
<p><a href="/scripts/books/bunleash/SimpleCgi.exe">SimpleCgi.exe</a>
Two complete HTML files for use with this chapter are available in the root of the Chap26 directory on the CD that accompanies this book. You might, of course, have to edit the paths shown in these files.
The variables passed to a CGI application can be retrieved from the DOS environment. You see these same types of variables when you type Set at the DOS prompt. You can use the getenv or _environ function to retrieve this data.
NOTE: Ghastly function names like getenv and the deplorable _environ are the result of a bygone time when compilers had a limited ability to work with long identifiers. Furthermore, there was a need to try to save every byte in the applications being produced.
Despite the ugly function names, those were romantic and exciting times. It was a period when all PC-based computer technology was new, and the industry was experiencing dynamic growth.
Despite the romance of that era, our much more complex contemporary programming environments require more careful habits. In modern programming, these functions would have easier-to-understand names like GetEnvironment and Environment.
I sometimes catch myself wondering how many wasted hours have been lost, on a global scale, as a result of the horrible naming conventions and ludicrous capitalization standards that used to exist. On the other hand, I also can't help but look back fondly, on the smaller, more manageable APIs that existed in those times, and on the highly charged, and much more friendly, atmosphere that prevailed.
The CGIVars application found on the CD that accompanies this book shows how to
iterate through all the environment variables passed to a CGI application. The output
from that program
is shown in Figure 26.6, and the source for the program is shown
in Listing 26.8 through Listing 26.10.
FIGURE 26.6.
The output from the CGIVars
application.
Listing 26.8. The CGIVars application shows how to retrieve all the variables passed to a CGI application.
/////////////////////////////////////// // CGIVars.cpp // Using an object to make CGI easier // Copyright (c) 1997 by Charlie Calvert // #include <vcl\vcl.h> #pragma hdrstop #include "CGIHelp1.h" USEUNIT("..\..\Utils\CGIHelp1.cpp"); void main(void) { TCGIHelp CGI; CGI.Header("World Enough and Time"); CGI.ShowEnv(); }
Listing 26.9. The TCGIHelp object provides support for CGI application. This is the header for the module.
/////////////////////////////////////// // CGIHelp1.h // An object to make CGI easier // Copyright (c) 1997 by Charlie Calvert // #ifndef CGIHelp1H #define CGIHelp1H enum TColorType {ctLightBlue, ctRedYellow, ctBlueYellow, ctGreenBlack}; class TCGIHelp { public: TCGIHelp(); ~TCGIHelp(); void HTMLHeader(); void Header(AnsiString S); AnsiString GetQuery(); void Colors(TColorType ColorType); void ShowEnv(); }; #endif
Listing 26.10. The main source file for the TCGIHelp utility.
/////////////////////////////////////// // CGIHelp1.cpp // An object to make CGI easier // Copyright (c) 1997 by Charlie Calvert // #include <vcl\vcl.h> #pragma hdrstop #include "CGIHelp1.h" TCGIHelp::TCGIHelp() { setvbuf(stdout, NULL, _IONBF, 0); printf("\nContent-type: text/html\n\n"); printf("<html>\n"); printf("<body>\n"); } TCGIHelp::~TCGIHelp() { printf("</body>\n"); printf("</html>\n"); } void TCGIHelp::Header(AnsiString S) { AnsiString Temp = "<H1>" + S + AnsiString("</H1>"); printf(Temp.c_str()); } AnsiString TCGIHelp::GetQuery() { char *Query; Query = getenv("QUERY_STRING"); if (Query) return Query; else return ""; } void TCGIHelp::Colors(TColorType ColorType) { switch(ColorType) { case ctLightBlue: printf("<body bgcolor=\"\#0000FF\"" "text=\"\#00FFFF\"" "link=\"\#FFFF00\"" "vlink=\"\#00FF00\"" "alink=\"\#FFFFFF\">"); break; case ctRedYellow: printf("<body bgcolor=\"\#FF0000\"" "text=\"\#FFFF\"" "link=\"\#FFFF00\"" "vlink=\"\#00FF00\"" "alink=\"\#FFFFFF\">"); break; case ctBlueYellow: printf("<body bgcolor=\"\#0000FF\"" "text=\"\#FFFF\"" "link=\"\#FFFF00\"" "vlink=\"\#00FF00\"" "alink=\"\#FFFFFF\">"); break; case ctGreenBlack: printf("<body bgcolor=\"\#00FF00\"" "text=\"\#000000\"" "link=\"\#FFFF00\"" "vlink=\"\#00FF00\"" "alink=\"\#FFFFFF\">"); break; } } void TCGIHelp::ShowEnv() { int i =0; while (_environ[i]) printf("%s<BR>", _environ[i++]); }
The main body of this console application is simple:
void main(void) { TCGIHelp CGI; CGI.Header("Current Environment"); CGI.ShowEnv(); }
The code is brief because I'm using a small helper object that gets many of the worst chores out of the way for me:
class TCGIHelp { public: TCGIHelp(); ~TCGIHelp(); void HTMLHeader(); void Header(AnsiString S); AnsiString GetQuery(); void Colors(TColorType ColorType); void ShowEnv(); };
This object performs the basic chores needed to run a CGI application. It is also the kind of simple object that can be improved and enlarged as you see fit. In other words, you should feel free to add methods to this object as you find a need for them.
The constructor for TCGIHelp takes care of sending off a MIME header that browsers examine so they can know what to do with the HTTP code that is being streamed toward them:
TCGIHelp::TCGIHelp() { setvbuf(stdout, NULL, _IONBF, 0); printf("\nContent-type: text/html\n\n"); printf("<html>\n"); printf("<body>\n"); }
The equally simple destructor outputs the lines that mark the end of an HTML form:
TCGIHelp::~TCGIHelp() { printf("</body>\n"); printf("</html>\n"); }
Other functions perform mundane tasks such as automatically formatting a header:
void TCGIHelp::Header(AnsiString S) { AnsiString Temp = "<H1>" + S + AnsiString("</H1>"); printf(Temp.c_str()); }
If you look at the source, you will see a method called colors that sets the background and foreground colors for a form. A simple enumerated type can be passed to the function so that you can set up the colors you need:
enum TColorType {ctLightBlue, ctRedYellow, ctBlueYellow, ctGreenBlack};
You can create more colors if you like. Each color statement sets the background color, the text color, and the link colors for the times before, during, and after they have been selected:
printf("<body bgcolor=\"\#0000FF\"" "text=\"\#00FFFF\"" "link=\"\#FFFF00\"" "vlink=\"\#00FF00\"" "alink=\"\#FFFFFF\">");
The numbers representing the colors work just like the standard Windows RGB colors:
#0000FF: Blue #00FF00: Green #FF0000: Red
You can find more details in Chapter 7, "Graphics."
The method that actually iterates through the environment is extremely simple:
void TCGIHelp::ShowEnv() { int i =0; while (_environ[i]) printf("%s<BR>", _environ[i++]); }
Not much I can say about that one. It's just simple C 101 code from your first programming course.
If you want to retrieve an individual item from the environment, you can use this method:
AnsiString TCGIHelp::GetQuery() { char *Query; Query = getenv("QUERY_STRING"); if (Query) return Query; else return ""; }
The main purpose of this code is to return a friendly AnsiString rather than force the user to gamble on using a NULL-terminated string. This place would, of course, be a particularly bad one to try to shave a few clock cycles off an application by using NULL-terminated strings rather than AnsiStrings. The reasons for this are twofold:
Given these considerations, I chose to return AnsiStrings from this method.
If no environment variable called QUERY_STRING exists, then the GetQuery method returns an empty AnsiString; otherwise, it returns the string retrieved from the system. getenv returns NULL if it cannot find the environment string you request. The server is the one that sets up the environment that will prevail inside the session where your application is launched.
NOTE: This section is not the place for me to get into the whole subject of creating CGI applications that are as small and as efficient as possible. You should be aware, however, that considerable thought has gone into sensible ways of creating applications that will stay in memory so that they do not have to be launched each time they are called. You can find books on this subject, or you can turn to third parties such as HREF: www.href.com.
You can easily create a BCB-based CGI application that handles databases. All
you have to do is allocate memory for a table, open it, and wrap its contents inside
HTML
statements. An example is shown Figure 26.7.
FIGURE 26.7.
The output for the CGIData application.
On the CD that accompanies this book, I include two applications that show the basics of working with the TTable object inside a CGI application. I also include one slightly more complex example that shows how to work with TQuery, TDatamodule, and InterBase from inside a CGI application.
NOTE: For the code shown in this section, I assume that you have the BDE set up on your server and that you or the installation program has created a BCDEMOS alias for the BDE.
Later, I will show an application running against InterBase tables. In that case, you will need to have both InterBase and the BDE running on your server. You could run the local InterBase server in this case because you can, if you want, query InterBase from the same machine on which the server resides. In other words, you can run both the local InterBase and your Web server on the same machine.
If you're using InterBase, Oracle, or other SQL servers, you will find a limit to the number of simultaneous connections you can support, depending on the nature of the license for your database server. You can still have multiple connections to your Web server, but you might be able to service only a limited number of requests for data at any one time.
The source for the first of the CGI-based database applications is shown in Listing
26.11. This program depends on the
TCGIHelp object shown in Listing 26.9.
Listing 26.11. The source for the
simplest possible case example of using a database code in a CGI application.
/////////////////////////////////////// // IsapiVars.cpp // Mirror back the information sent to an ISAPI DLL by the server // Copyright (c) 1997 by Charlie Calvert // #include <vcl\vcl.h> #pragma hdrstop #include "..\..\utils\Httpext.h" USERES("IsapiVars.res"); BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer) { pVer->dwExtensionVersion = MAKELONG(HSE_VERSION_MINOR, HSE_VERSION_MAJOR); strcpy(pVer->lpszExtensionDesc, "ISAPI Variables DLL"); return (TRUE); }; #define SIZE 2048 DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB) { char ResultString[SIZE * 2]; char HtmlInfo[SIZE]; char Buffer[SIZE]; DWORD StrSize; DWORD resultLen; char *IsapiLogText = "ISAPIVars from C++ Builder"; strcpy(pECB->lpszLogData, IsapiLogText); pECB->dwHttpStatusCode = 200; sprintf(HtmlInfo, "<HTML><TITLE>Fields of EXTENSION_CONTROL_BLOCK</TITLE>" "<H1>Test server results</H1><BODY>" "Size = %d<BR>" "Version = %.8x<BR>" "ConnID = %.8x<BR>" "Method = %s<BR>" "Query = %s<BR>" "PathInfo = %s<BR>" "PathTranslated = %s<BR>" "TotalBytes = %d<BR>" "AvailableBytes = %d<BR>" "ContentType = %s<BR><BR>" "<H1>Calls to GetServerVariable</H1>", pECB->cbSize, pECB->dwVersion, pECB->ConnID, pECB->lpszMethod, pECB->lpszQueryString, pECB->lpszPathInfo, pECB->lpszPathTranslated, pECB->cbTotalBytes, pECB->cbAvailable, pECB->lpszContentType); StrSize = sizeof(Buffer); pECB->GetServerVariable(pECB->ConnID, "REMOTE_ADDR", &Buffer, &StrSize); AnsiString VarString("REMOTE_ADDR = " + AnsiString(Buffer) + "<BR>"); StrSize = sizeof(Buffer); pECB->GetServerVariable(pECB->ConnID, "REMOTE_HOST", &Buffer, &StrSize); VarString += "REMOTE_HOST = " + AnsiString(Buffer) + "<BR>"; StrSize = sizeof(Buffer); pECB->GetServerVariable(pECB->ConnID, "REMOTE_USER", &Buffer, &StrSize); VarString += "REMOTE_USER = " + AnsiString(Buffer) + "<BR>"; StrSize = sizeof(Buffer); pECB->GetServerVariable(pECB->ConnID, "SERVER_NAME", &Buffer, &StrSize); VarString += "SERVER_NAME = " + AnsiString(Buffer) + "<BR>"; StrSize = sizeof(Buffer); pECB->GetServerVariable(pECB->ConnID, "SERVER_PORT", &Buffer, &StrSize); VarString += "SERVER_PORT = " + AnsiString(Buffer) + "<BR>"; StrSize = sizeof(Buffer); pECB->GetServerVariable(pECB->ConnID, "SERVER_PROTOCOL", &Buffer, &StrSize); VarString += "SERVER_PROTOCOL = " + AnsiString(Buffer) + "<BR>"; StrSize = sizeof(Buffer); pECB->GetServerVariable(pECB->ConnID, "SERVER_SOFTWARE", &Buffer, &StrSize); VarString += "SERVER_SOFTWARE = " + AnsiString(Buffer) + "<BR>"; StrSize = sizeof(Buffer); pECB->GetServerVariable(pECB->ConnID, "HTTP_ACCEPT", &Buffer, &StrSize); VarString += "HTTP_ACCEPT = " + AnsiString(Buffer) + "<BR>"; StrSize = sizeof(Buffer); pECB->GetServerVariable(pECB->ConnID, "URL", &Buffer, &StrSize); VarString += "URL = " + AnsiString(Buffer) + "<BR><BR><BR>"; StrSize = sizeof(Buffer); pECB->GetServerVariable(pECB->ConnID, "ALL_HTTP", &Buffer, &StrSize); VarString += "ALL_HTTP = " + AnsiString(Buffer) + "<BR>"; strcat(HtmlInfo, VarString.c_str()); sprintf(ResultString, "HTTP/1.0 200 OK\nContent-Type: text/html\n" "Content-Length: %d\nContent:\n\n %s </HTML>", SIZE, HtmlInfo); StrSize = strlen(ResultString); pECB->WriteClient(pECB->ConnID, ResultString, &StrSize, 0); return (HSE_STATUS_SUCCESS); } int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void*) { return 1; }
Because you're working in an application, as opposed to a DLL, the TApplication object is available to you. This object helps set up the TSessions object that is required by database applications. As a result, the conservative and safe thing to do is to make sure that the Application->Initialize method is called so that the database tools are set up properly:
Application->Initialize();
NOTE: This step makes ISAPI database applications a bit tricky. ISAPI runs in a DLL, and you have to be careful setting up database code in a DLL. In particular, you have to be sure the Sessions object is properly initialized and that each table is assigned a Session.
After setting up the Application and Sessions objects, you are free to open up a table. This application has no data module and no form, so you must do all the work yourself:
Table = new TTable(Application); Table->DatabaseName = "BCDEMOS"; Table->TableName = "Customer"; Table->Open();
This code allocates memory for the table, specifies the database and table the application wants to use, and finally opens the table.
After the table is open, you can start retrieving data immediately. However, you might want to put off this step just long enough to set up an HTML table:
printf("<TABLE BORDER>\n"); printf("<TH>%s</TH><TH>%s</TH>", Table->Fields[0]->FieldName.c_str(), Table->Fields[1]->FieldName.c_str());
This code calls the FieldName property of the TField object to retrieve the name of each column in the database. This information is displayed in the top row of the HTML table. As you can see, this example works with only the first two columns. The CGIData2 program, shown in Listing 26.12, provides a generic solution for converting any table to HTML.
The following code iterates through the database, displaying all the data from the first two fields to the user:
while (!Table->Eof) { printf("<TR><TD>%s</TD><TD>%s</TD></TR>\n", Table->Fields[0]->AsString.c_str(), Table->Fields[1]->AsString.c_str()); Table->Next(); }
After the data is shown, I close the HTML table, close the TTable object, and then delete the TTable object:
printf("</TABLE>\n"); Table->Close(); delete Table;
CGI database programming is obviously simple. You can, however, make the process even easier. I will demonstrate these techniques in the next section.
The CGIData2 application shows a technique for converting any arbitrary table to HTML. The program also shows how to pass information to a CGI application so that the user can choose the table he or she wants to display. The combination of these two traits gives the user the ability to browse any table on your system over the Internet.
The output from the CGIData2 program is shown
Figure 26.8, and the source for
the program is shown in Listings 26.12 through 26.14. Note the URL passed to the
application in Figure 26.8. The information after the question mark specifies which
table the user wants to browse.
FIGURE 26.8.
The output from the CGIData2 application while looking at the Parts table
from BCDEMOS.
Listing 26.12. The source for the main module in the CGIData2 application.
/////////////////////////////////////// // CGIData2.cpp // Databases and CGI // Copyright (c) 1997 by Charlie Calvert // #include <vcl\vcl.h> #pragma hdrstop #include "cgiDBhelp1.h" USEUNIT("..\..\Utils\CGIHelp1.cpp"); USEUNIT("..\..\Utils\CGIDBHelp1.cpp"); void main(void) { TCGIDBHelp CGI; CGI.Colors(ctGreenBlack); char *Query; TTable *Table; Query = getenv("QUERY_STRING"); if (Query) { Application->Initialize(); Table = new TTable(Application); Table->DatabaseName = "BCDEMOS"; Table->TableName = Query; try { Table->Open(); CGI.Header(Table->TableName); CGI.PrintTable(Table); Table->Close(); delete Table; } catch(Exception &E) { printf("Could not open table: %s", E.Message.c_str()); } catch(...) { printf("Could not open table"); } } else { printf("<H1>No query string available</H1>\n"); } Application->Run(); }
Listing 26.13. The header for the TCGIDBHelp file.
/////////////////////////////////////// // CGIDBHelp1.h // An object to make CGI easier // Copyright (c) 1997 by Charlie Calvert // #ifndef CGIDBHelp1H #define CGIDBHelp1H #include "CGIHelp1.h" class TCGIDBHelp: public TCGIHelp { public: void PrintTable(TDataSet *Table); }; #endif
Listing 26.14. The TCGIDBHelp object provides support for CGI database applications.
/////////////////////////////////////// // CGIDBHelp1.cpp // An object to make CGI easier // Copyright (c) 1997 by Charlie Calvert // #include <vcl\vcl.h> #pragma hdrstop #include "CGIDBhelp1.h" void TCGIDBHelp::PrintTable(TDataSet *Table) { int i; printf("<TABLE BORDER>\n"); for (i = 0; i < Table->FieldCount; i++) { printf("<TH>%s</TH>", Table->Fields[i]->FieldName.c_str()); } while (!Table->Eof) { printf("<TR>"); for (i = 0; i < Table->FieldCount; i++) { printf("<TD>%s</TD>", Table->Fields[i]->AsString.c_str()); } printf("</TR>\n"); Table->Next(); } printf("</TABLE>\n"); }
This application is designed to allow the user to interact with it dynamically. In particular, this application retrieves the name of the table the user wants to see from the environment variable called QUERY_STRING:
Query = getenv("QUERY_STRING"); if (Query) { ... // Code omitted here Table->TableName = Query; } else { printf("<H1>No query string available</H1>\n"); }
These strings are passed to the application if you append a question mark to the URL that calls the CGI script and then append data after the question mark. For example, the following HTML code would call this script without specifying any table name:
<a href="/scripts/books/bunleash/CgiData2.exe">CGIData2.exe</a>
You might have to change the path specified in this code, but it shows the general
formula for the code you should write. If you click the reference
listed here, the
program would report an error, as shown in Figure 26.9.
FIGURE 26.9.
The CGIData2 program returns an error like this if the user does
not specify
the name of a table he or she wants to view.
If you want to ask about the Customer database, you can edit the string so that it looks like this:
/scripts/books/bunleash/CgiData2.exe?Customer">CGIData2.exe
This request retrieves the Customer table from the database. You do not, of course, have to hard-code the name of each table you want to query into your form. In fact, in this particular example, you don't even have to use an HTML form at all. If you want, you can merely edit the raw HTML string in the Address field of your browser, appending different files after the question mark to retrieve different sets of data. For example, you could ask for the Customer, Orders, Items, Events, Parts, or BioLife tables.
If CGIData2 finds a valid query string in the global environment, it tries to open a table. If it can't find a string, then it prints a message to the user explaining the problem:
printf("<H1>No query string available</H1>\n");
Figure 26.10 shows the message that the CGIData2 program returns to the browser
when the user enters an invalid table name.
FIGURE 26.10.
The output
from the CGIData2 program when the user asks for a nonexistent
table.
As you can see, the big danger here is that the user will pass in an invalid string. If this happens, an exception will be raised, which can hang your server, at least until you use a Task Manager to shut down the service.
NOTE: One way to ease the chore of debugging CGI applications is to run against a server on your own system. For example, if you are on Windows 95 and you use the Personal Web Server, then exceptions raised by your application appear in message boxes that you can read directly on your screen. You can then just click the OK button on the message so that the application will terminate. This option is not available if you're running against an NT server that resides on a separate machine. In fact, exceptions never appear on the screen on most servers, and your application will hang, with no way for you to click the OK button.
I'm sure most readers can guess that try..catch blocks are the way to handle exceptions that occur on a server. In particular, the following code catches exceptions that occur when you try to open a table or during the process of iterating through the records of a table:
try { Table->Open(); CGI.Header(Table->TableName); CGI.PrintTable(Table); Table->Close(); delete Table; } catch(Exception &E) { printf("Could not open table: %s", E.Message.c_str()); } catch(...) { printf("Could not open table"); } }
In this case, I am assuming that all errors will be caused because the user passed in an invalid string. However, the code will trap any VCL error and return a string specifying what went wrong. On the off chance that the exception is not raised by the VCL, I have a generic catch block that simply states that something went wrong and that the table could not be opened. For more information, see Chapter 5, "Exceptions."
Notice that I also call Application->Run(). Calling this method appears to be necessary if you want to handle exceptions properly in a database application.
The last chunk of code to look at from this application is the bit that converts a table to HTML:
void TCGIDBHelp::PrintTable(TDataSet *Table) { int i; printf("<TABLE BORDER>\n"); for (i = 0; i < Table->FieldCount; i++) { printf("<TH>%s</TH>", Table->Fields[i]->FieldName.c_str()); } while (!Table->Eof) { printf("<TR>"); for (i = 0; i < Table->FieldCount; i++) { printf("<TD>%s</TD>", Table->Fields[i]->AsString.c_str()); } printf("</TR>\n"); Table->Next(); } printf("</TABLE>\n"); }
This code is part of an object designed to work with database CGI applications. I separate it from the TCGIHelp object because database code adds so much to the size of a file. My theory is that you should include this file in your project only if you're explicitly using database objects; otherwise, stick with the smaller TCGIHelp file. TCGIDBHelp is a direct descendant of TCGIHelp, so it inherits all of methods from TCGIHelp.
The code shown here starts by iterating through all the fields in the database and places their names in the header for the table:
printf("<TABLE BORDER>\n"); for (i = 0; i < Table->FieldCount; i++) { printf("<TH>%s</TH>", Table->Fields[i]->FieldName.c_str()); }
With this task out of the way, the code then iterates through the entire table, filling out each row in its entirety:
for (i = 0; i < Table->FieldCount; i++) { printf("<TD>%s</TD>", Table->Fields[i]->AsString.c_str()); }
That's all I'm going to say about the CGIData2 program. Once again, you can see that publishing data over the Web is an easy operation. If you own BCB and run on an NT Server, you can publish data all day long with very little effort. If you have a smaller clientele, you can do the same thing with Windows 95 and the Personal Web Server, or else you can turn to more powerful tools such as the WebSite Server provided by O'Reilly.
O'Reilly is located at www.ora.com. Try http://www.ora.com/catalog/webpro/ for the professional version of the product and http://www.ora.com/catalog/web1.1/ for the inexpensive standard edition.
The real power of CGI applications is made clear when you start running SQL queries
against data. For example, I took the Music.gdb database from Chapter 16,
"Advanced InterBase Concepts," and let the user run some queries
against
it, as shown in Figure 26.11 and Figure 26.12. In particular, this application calls
some stored procedures designed to quickly pump out data that answers complex questions
about a database. The stored procedures in question were discussed in
Chapter 16.
Remember that this is an InterBase database, and that you must set up the alias for
it as described in the readme file on the CD that accompanies this book. I have hardcoded
the password "masterkey" into the Params
property of the TDatabase
object in the program's data module.
FIGURE 26.11.
Running a query against the Music database to see albums
rated
as PEACEFUL.
FIGURE 26.12. Running a query against the Music database to see albums with very high ratings.
The source for the CGIQuery
program is shown in Listing 26.15. A special HTML
page is needed to query this application. A screen shot of the page is shown in Figure
26.13, and the HTML source for the page is shown in Listing 26.16.
FIGURE 26.13.
The HTML page used to query the CGIQuery application.
Listing 26.15. The CGIQuery application lets the user ask a number of questions of the database.
/////////////////////////////////////// // CGIQuery.cpp // An object to make CGI easier // Copyright (c) 1997 by Charlie Calvert // #include <vcl\vcl.h> #pragma hdrstop #include "DMod1.h" #include "CGIDBHelp1.h" USERES("CGIQuery.res"); USEDATAMODULE("DMod1.cpp", DMod); USEUNIT("\SrcC\PUnleash\Utils\CGIDBHelp1.cpp"); USEUNIT("\SrcC\PUnleash\Utils\CGIHelp1.cpp"); AnsiString ParseQuery(AnsiString S) { int i = S.Pos("&"); S.SetLength(i - 1); S = strrev(S.c_str()); i = S.Pos("="); S.SetLength(i - 1); S = strrev(S.c_str()); return S.c_str(); } AnsiString ParseQuery1(AnsiString S) { S = strrev(S.c_str()); int i = S.Pos("="); S.SetLength(i - 1); return S.c_str(); } /////////////////////////////////////// // If user pressed a button on the HTML // form, them must parse input. // String will look like this: // Query=6&Query1=7 /////////////////////////////////////// AnsiString GetButtonQuery(AnsiString S) { AnsiString Query = ParseQuery(S); AnsiString Query1= ParseQuery1(S); try { return Format("select * from RatingRange(%s, %s)", OPENARRAY(TVarRec, (Query, Query1))); } catch(...) { return "You entered invalid data"; } } bool GetLetters(AnsiString(S)) { int i; FILE * out; out = fopen("c:\\sam.txt", "w+"); for (i = 0; i < S.Length(); i++) { fprintf(out, "Letters: %c\n", S[i]); } fclose(out); return True; } void RunQueries() { int i; TCGIDBHelp CGI; AnsiString S = CGI.GetQuery(); if (S.Length() == 0) S = "0"; if (S[1] != `Q') // Did user press button? If so first letter is `Q' { if (S != "") i = S.ToInt(); else i = 0; switch(i) { case 0: S = "select * from Album"; break; case 1: S = "select * from NineOrBetter"; break; case 2: S = "select * from GetLoudness(1)"; break; case 3: S = "select * from GetLoudness(2)"; break; case 4: S = "select * from GetLoudness(3)"; break; default: S = "select * from Artist"; } } else S = GetButtonQuery(S); DMod->Query1->SQL->Add(S); try { DMod->Query1->Open(); CGI.Header(DMod->Query1->SQL->Strings[0]); CGI.PrintTable(DMod->Query1); DMod->Query1->Close(); } catch(...) { printf("Invalid Query: %s", S.c_str()); } } WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { try { Application->Initialize(); Application->CreateForm(__classid(TDMod), &DMod); RunQueries(); Application->Run(); } catch (Exception &exception) { Application->ShowException(&exception); } return 0; }
Listing 26.16. The HTML file used to query the CGIQuery application.
<HTML> <TITLE>CGI QueryPage</TITLE> <HEAD> <H1>CGI QueryPage</H1> </HEAD> <body bgcolor="#0000FF" text="#00FFFF" link="#FFFF00" vlink="#00FF00" alink="#FFFFFF"> <BODY> <UL> <LI><a href="/scripts/books/bunleash/CgiQuery.exe">CGIQuery.exe</a> <LI><a href="/scripts/books/bunleash/CgiQuery.exe?1"> Albums rated nine or better</a> <LI><a href="/scripts/books/bunleash/CgiQuery.exe?2">Peaceful Albums</a> <LI><a href="/scripts/books/bunleash/CgiQuery.exe?3">Moderate Albums</a> <LI><a href="/scripts/books/bunleash/CgiQuery.exe?4">Loud Albums</a> </UL> <HR> <FORM METHOD="GET" ACTION="/scripts/books/bunleash/CGIQuery.exe"> Range Start<BR> <INPUT TYPE = "text" NAME="Query" SIZE=10 defaultvalue="1"><BR><BR> Range End<BR> <INPUT TYPE = "text" NAME="Query1" SIZE=10 defaultvalue="2"><BR><BR> <INPUT TYPE = "submit" VALUE="Submit"> <HR> </BODY> </HTML>
The most interesting
thing about the CGIQuery application is that it
uses a TDataModule, as shown in Figure 26.14. The data module allows you
to access all the visual database tools that can make BCB programming so simple.
For example, you can drop down
a TDataBase object, connect to a database,
set up your password in the Params property, and open up the database. Performing
these tasks visually is much easier than doing them in code. You can also set up
a one-to-many relationship
or perform filters and lookups.
FIGURE 26.14.
The data module for the CGIQuery application.
Using a data module in a CGI application allows
you to leverage the power of the
BCB RAD environment.
When you're using a data module, giving up altogether on the idea of using a Console application is probably simplest. Instead, keep the standard WinMain block set up by BCB:
WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { try { Application->Initialize(); Application->CreateForm(__classid(TDMod), &DMod); RunQueries(); Application->Run(); } catch (Exception &exception) { Application->ShowException(&exception); } return 0; }
This code calls Application::Initialize, CreateForm, and Run. As you can see, I do not include a standard TForm object in the application, which means that it has no visual interface. It just runs silently in the background, without ever showing its face to the user. Once again, notice that I call Run, which appears to be necessary when you include a data module and exception handling in your application.
I should perhaps add that during application development, I often include a form so that I can test my queries at design time or sometimes even at runtime. The form usually need not contain anything but a TDBGrid in which to display the results of the query. I include a TDataSource on the data module for use with a form during program testing. It will be deleted during the final phases of project development. Furthermore, you can simply remove the entire test form from your project when it's time to distribute your application.
The key point to emphasize here is that you can leverage the visual tools during CGI development, even if you end up stripping them out of the application when you ship. This capability helps show why BCB is the best environment to use even when you're not producing standard RAD applications.
NOTE: I once again want to stress that you can design CGI applications in a number of ways. I mean for the techniques I show here to be an introduction to the subject and to show you how to get a lot of work done quickly. If you find that this subject interests you, you should consider turning to third parties that have developed custom VCL components to make this task simpler and more efficient.
To understand the CGI Query application, you should first look at the HTML code that calls it:
<LI><a href="/scripts/books/bunleash/CgiQuery.exe">CGIQuery.exe</a> <LI><a href="/scripts/books/bunleash/CgiQuery.exe?1"> Albums rated nine or better</a> <LI><a href="/scripts/books/bunleash/CgiQuery.exe?2">Peaceful Albums</a> <LI><a href="/scripts/books/bunleash/CgiQuery.exe?3">Moderate Albums</a> <LI><a href="/scripts/books/bunleash/CgiQuery.exe?4">Loud Albums</a>
These options pass in various query strings, depending on the user's interests. For example, if the query string "1" is passed in, the following code gets called:
case 1: S = "select * from NineOrBetter"; break;
This line sets up a SQL statement that calls the stored procedure called NineOrBetter. The code for the stored procedure looks like this:
begin for select Artist.Last, Album.Album, Album.Rating from Album, Artist where Album.GroupCode = Artist.Code and Album.Rating >= 9 Order By Album.Rating Desc into :Last, :Album, :Rating do suspend; end
As I said earlier, you can turn to Chapter 16 for an in-depth explanation of this code. The key point is that it retrieves a list of albums rated 9 or better.
The code that runs the query looks like this:
DMod->Query1->SQL->Add(S); try { DMod->Query1->Open(); CGI.Header(DMod->Query1->SQL->Strings[0]); CGI.PrintTable(DMod->Query1); DMod->Query1->Close(); } catch(...) { printf("Invalid Query: %s", S.c_str()); }
The first line here sets up the query string. The TQuery object is then run, and the data from it is processed by the TCGIDBHelp object, as described in the examination of the CGIData2 application.
Besides calling the NineOrBetter stored procedure, the user's other choices include selecting a query that retrieves data about the relative volume of the albums in the database:
case 2: S = "select * from GetLoudness(1)"; break; case 3: S = "select * from GetLoudness(2)"; break; case 4: S = "select * from GetLoudness(3)"; break;
The GetLoudness stored procedure looks like this:
begin for select Artist.Last, Album.Album, Album.Rating, Loudness.Loudness from Album, Artist, Loudness where Album.GroupCode = Artist.Code and Album.Loudness = :LoudnessValue and Loudness.Code = :LoudnessValue Order By Album.Album Desc into :Last, :Album, :Rating, :LoudnessStr do suspend; end
This SQL code depends on the values from the Loudness table. The Loudness table contains three records, stating that Peaceful records have a code of 1, Moderate records have a code of 2, and Loud records have a code of 3. The stored procedure does a lookup into this table, retrieves the string specifying the loudness of a particular album, and displays it to the user.
Now that you understand how the system works, you can see that calling GetLoudness(1) retrieves a list of the Peaceful records, GetLoudness(2) gets the records of Moderate volume, and so on. All this information is then wrapped up in an HTML table and shown to the user, as shown in Figure 26.11.
The final point I want to make about this program is that it allows you to run queries based on data typed in by the user, as shown Figure 26.14. In particular, the user can type in a starting and ending range for the ratings of albums. If he or she then clicks the Submit button, a query will be formulated based on the user's input.
The standard HTML code that allows the user to submit the question looks like this:
<HR> <FORM METHOD="GET" ACTION="/scripts/books/bunleash/CGIQuery.exe"> Range Start<BR> <INPUT TYPE = "text" NAME="Query" SIZE=10 defaultvalue="1"><BR><BR> Range End<BR> <INPUT TYPE = "text" NAME="Query1" SIZE=10 defaultvalue="2"><BR><BR> <INPUT TYPE = "submit" VALUE="Submit"> <HR>
The FORM METHOD statement tells the browser what to do when the user clicks the Submit button. In particular, it says that the CGIQuery application should be called. The HTML then defines two edit controls and the Submit button itself.
When the user clicks the button, a string like the following is automatically generated and passed to the application after the question mark in the URL:
Query=6&Query1=7
This string specifies that the user wants to see records that have a rating between 6 and 7.
I write some very ugly code that checks to see if the user is passing in a string to query the ratings of a table:
AnsiString S = CGI.GetQuery(); if (S.Length() == 0) S = "0"; if (S[1] != `Q') // Did user press button? If so first letter is `Q' { ... // Handle queries against Loudness table, etc. } else S = GetButtonQuery(S);
As you can see, I've actually commented my code in this case, because there is no logical way for the reader to figure out why I am checking for the letter Q. Once you understand the system, you can see that the check is made because all but one type of query from the browser will consist of a simple integer. That one exceptional query is the kind that asks for record albums and CDs in a particular range, and I can identify that type of query because it always begins with a Q. This is, as I readily confess, horrible code, even by my standards. However, in this case I ask for your indulgence because of the rather primitive nature of all CGI technology.
NOTE: When I say that CGI technology is primitive, I am, of course, aware that network-based technology in and of itself is quite remarkable. However, the interface between a browser and an application is still very rudimentary, and will probably continue to be so in the foreseeable future. Because of the shortcomings of this technology, I take up DCOM in the next chapter. Of course, DCOM also has its limitations. In fact, sometimes DCOM shines, and sometimes CGI and ISAPI shine. The key point is knowing all the available technologies so that you can choose the best ones for a particular situation.
That's all I'm going to say about the CGIQuery program. You might want to examine parts of the application on your own. For example, you should take a look at the code that handles exceptions and make sure you understand what I'm doing when I parse the query string received from the browser.
I should perhaps add that various available technologies can relieve you of the task of parsing query strings and of retrieving strings from the environment. Much of the time, these tasks are so simple that you don't need complex code to get the job done. However, if you need to create really big, complex CGI applications, then you should get on the Web and see whether you can examine your available options for offloading some of these chores.
In this chapter, you learned about ISAPI and CGI. These technologies allow you to enhance a server with custom C++ code. In particular, they both allow you to access databases over the World Wide Web.
You saw that ISAPI is a DLL-based technology that runs inside the address space of the server. As a result, it is very highly optimized from a performance standpoint. CGI technology, on the other hand, is based on executables rather than DLLs. CGI applications, therefore, are usually slower than ISAPI applications. They are, on the other hand, much simpler to write, or more specifically, simpler to debug.
The key point to grasp about these technologies is that they can be used to make Web sites interactive. Though its performance is a bit slow, the Web can be a very powerful means of reaching a large audience that can extend across the entire globe. Many of the most exciting Web sites are powered by CGI or ISAPI, and BCB enables you to easily participate in this process.
©Copyright, Macmillan Computer Publishing. All rights reserved.