Delphi Clinic | C++Builder Gate | Training & Consultancy | Delphi Notes Weblog | Dr.Bob's Webshop |
|
Internet file formats can be divided into a few groups. First, we have the file transfer (or communication) file formats, for which a long time ago the uuencode/decode schema was invented, followed by xxencode/decode. This later evolved into the base64 encoding and MIME messaging scheme that a lot of mailers use today. A second type of internet file formats is the Hyper Text Markup Language (HTML), with all its versions and (often browser specific) enhancements a true group in itself. The third group of internet file formats is more an interface or protocol of communication again; the Common Gateway Interface (CGI), of which we can identify standard (or console) CGI and Windows CGI or WinCGI.
CGI
CGI stands for Common Gateway Interface, and is the communication protocol between a "Form" on a Web Browser (the client) and an application running on the Web Server (the server).
The application is usually called a CGI Script, although we can use Delphi to write CGI applications that are of course no scripts.
There are two kinds of CGI: standard or console CGI and the later Windows version called WinCGI.
console CGI
A standard or console CGI application communicates with the "Form" on the client side by using environment variables (control values), the standard input (the Form data) and standard output (the resulting dynamic HTML page).
WinCGI
A WinCGI application communicates with the "Form" on the client side by using a Windows .INI file instead of environment variables.
The Windows .INI file contains the control values, sometimes even the entire Form Data, and the filenames of the input, data and output file that needs to be generated.
Delphi and CGI
In this dynamic and growing on-line chapter I'll explain how to write some simple Delphi CGI applications, without having to use Web Modules or other Client/Server stuff at all.
First of all, CGI stands for Common Gateway Interface, and is just the name for the protocol to send information from the client (i.e. the web browser) to the server (i.e. the web server application).
On the client side, this is implemented by a CGI Form, which consists of nothing more but HTML tags.
One the server this consists of a CGI application, which is sometimes also called CGI script (for example on Unix machines, where Perl is used to implement CGI scripts).
In this chapter, I'll focus on writing CGI application for Windows NT web server, and we'll be using 32-bits Delphi (i.e. Delphi 2.x or 3.x) for this task, although the code will also compile without problems with C++Builder, of course.
A standard CGI application gets its input (like a search query) from the standard input, and must write its output (for example a HTML page) on the standard output.
This means we'll need to write a Delphi CONSOLE application.
If there's no input, we can also refer to CGI application as simply generating dynamic HTML pages (for example extracted from a table).
Dynamic Output
So, let's take a look at a "hello world" CGI application first.
This one would do nothing except print the string "hello, world" in a HTML page.
Before we can do that, however, there's one more thing: the CGI application must tell the world what (MIME) format the output actually is.
And in this case it's "text/html", which we must write as follows: content-type: text/html, followed by an empty line.
Which all results in our first "Hello, world!" CGI application that could look as follows:
program CGI1; {$APPTYPE CONSOLE} begin writeln('content-type: text/html'); writeln; writeln('<html>'); writeln('<body>'); writeln('Hello, world!'); writeln('</body>'); writeln('</html>') end.If you compile this program with Delphi 2 or 3 and run it from either IntraBob or a web browser connected to a web server where it's stored in an executable directory such as cgi-bin, then you'll get the "Hello, world!" output in a HTML page.
CGI Input
Now, we've seen how to create a CGI application that can generate a dynamic (or actually quite static) HTML page.
But what about input?
There's one more issue here: we need to check the DOS environment variable 'CONTENT LENGTH' to see how many characters from standard input we actually have to read (if we try to read one more, we'll just hang forever).
Of course, this is a simplification of the entire complete facts, but it'll do for our second working sample CGI application...
I've written a TBDosEnvironment component that you can use to access DOS environment variables:
unit DrBobDOS; interface uses SysUtils, WinTypes, WinProcs, Classes; type TBDosEnvironment = class(TComponent) public { Public class declarations (override) } constructor Create(AOwner: TComponent); override; destructor Destroy; override; private { Private field declarations } FDosEnvList: TStringList; procedure DoNothing(const Value: TStringList); protected { Protected method declarations } Dummy: Word; function GetDosEnvCount: Word; public { Public interface declarations } function GetDosEnvStr(const Name: String): String; { This function is a modified version of the GetEnvVar function that appears in the WinDos unit that comes with Delphi. This function's interface uses Pascal strings instead of null-terminated strings. } published { Published design declarations } property DosEnvCount: Word read GetDosEnvCount write Dummy; property DosEnvList: TStringList read FDosEnvList write DoNothing; end; implementation constructor TBDosEnvironment.Create(AOwner: TComponent); var P: PChar; begin inherited Create(AOwner); FDosEnvList := TStringList.Create; {$IFDEF WIN32} P := GetEnvironmentStrings; {$ELSE} P := GetDosEnvironment; {$ENDIF} while P^ <> #0 do begin FDosEnvList.Add(StrPas(P)); Inc(P, StrLen(P)+1) { Fast Jump to Next Var } end; end {Create}; destructor TBDosEnvironment.Destroy; begin FDosEnvList.Free; FDosEnvList := nil; inherited Destroy end {Destroy}; procedure TBDosEnvironment.DoNothing(const Value: TStringList); begin end {DoNothing}; function TBDosEnvironment.GetDosEnvCount: Word; begin if Assigned(FDosEnvList) then Result := FDosEnvList.Count else Result := 0; end {GetDosEnvCount}; function TBDosEnvironment.GetDosEnvStr(const Name: String): String; var i: Integer; Tmp: String; begin i := 0; Result := ''; if Assigned(FDosEnvList) then while i < FDosEnvList.Count do begin Tmp := FDosEnvList[i]; Inc(i); if Pos(Name,Tmp) = 1 then begin Delete(Tmp,1,Length(Name)); if Tmp[1] = '=' then begin Delete(Tmp,1,1); Result := Tmp; i := FDosEnvList.Count { end while-loop } end end end end {GetDosEnvStr}; end.
Here's a list of the Environment Variables (provided by Deepak Shenoy) that are available to a CGI program. Even ISAPI programs can use these variables:
GATEWAY_INTERFACE | CGI version that the web server complies with. |
SERVER_NAME | Server's IP address or host name. |
SERVER_PORT | The Port on the server that received the HTTP request. |
SERVER_PROTOCOL | Name and version of the protocol being used by the server to process requests. |
SERVER_SOFTWARE | Name (and, normally, version) of the server software being run. |
AUTH_TYPE | Authentication scheme used by the server (NULL , BASIC etc) |
CONTENT_FILE | File used to pass data to a CGI program (Windows HTTPd/WinCGI only). |
CONTENT_LENGTH | Number of bytes passed to Standard Input (STDIN) as content from a POST request. |
CONTENT_TYPE | Type of data being sent to the server. |
OUTPUT_FILE | Filename to be used as the location for expected output (Windows HTTPd/WinCGIonly). |
PATH_INFO | Additional relative path information passed to the server after the script name, but before any query data. |
PATH_TRANSLATED | Same information as PATH_INFO, but with virtual paths translated into absolute directory information. |
QUERY_STRING | Data passed as part of the URL, comprised of anything after the ? in the URL. |
REMOTE_ADDR | End user's IP address or server name. |
REMOTE_USER | User name, if authorization was used. |
REQUEST_LINE | The full HTTP request line provided to the server (availability varies by server). |
REQUEST_METHOD | Specifies whether data for the HTTP request was sent as part of the URL (GET) or directly to STDIN (POST). |
SCRIPT_NAME | Name of the CGI script being run. |
There are some more, but they're not as important as these. A few Environment Variables that are especially important to our CGI application, and an outline for the processing of a standard CGI application is as follows:
REQUEST_METHOD - determine whether we get data by POST or by GET
QUERY_STRING - if we used GET
CONTENT_LENGTH - if we used POST, and now read "CONTENT_LENGTH" characters from the standard input (which ends up with the "Query", like QUERY_STRING does when using the GET protocol).
In all cases, the standard CGI application must write (HTML) output to the standard output file, so we'll use a CONSOLE type application.
Now, with the TBDosEnvironment component you can create a dynamic instance, obtain the above three Environment Variables, and get the input you need. After that, it's up to you to write the code to generate the output you want for that particular input.
Easy, right? For an example of yet another very small (39Kb) standard CGI application, check out the mini website Search Engine on my website. The (short) source code will be presented in an article for The Delphi Magazine, but I can tell you that the basic CGI communication protocol is no more complex than what I've outlined so far...
Input Queries
Today, we'll deal with "reading queries values" in a standard CGI application written in 32-bits Delphi (i.e. Delphi 2.x or 3.x).
Basically, it's a two-step process.
The first step involves HTML and the special CGI Form-tags, the second step involves retrieving the data from within the CGI application on the web server.
A HTML CGI Form is defined within <form>...</form> tags.
The opening tag also contains the method to send data (GET or POST) and the action, which is the URL of the CGI application that is to be executed on the web server.
For example:
<form method="POST" action="http://www.ebob42.com/cgi-bin/debug.exe"> ... </form>This denoted a HTML CGI Form that will POST the data to my web server, and then execute the program debug.exe (from cgi-bin directory). For now, we don't have to concern ourselves with differences between POST and GET (I always use POST). We do note, however, that there's nothing to POST, so far. We need to specify so-called input fields within the CGI Form. For this, we can pick a number of very basic standard Windows controls, all predefined, like an editbox, a memo field, a listbox, a drop-down combobox, radiobuttons, checkboxes and finally the "action" buttons (reset or submit).
<input type="text" name="login" size="8">The standard CGI application, on its turn, needs to check the DOS Environment variable REQUEST-METHOD to see if it is GET or POST. In case of a POST, we need to check the CONTENT-LENGTH to determine the number of characters that we need to read (from standard input). This standard input is containing the data (such as "login-xxxxxxxx") for our CGI application.
unit DrBobCGI; {$I-} interface var ContentLength: Integer = 0; function Value(const Field: ShortString): ShortString; { use this function to get the CGI inputquery values } implementation uses SysUtils, Windows; var Data: String = ''; function Value(const Field: ShortString): ShortString; var i: Integer; begin Result := ''; i := Pos(Field+'=',Data); if i > 0 then begin Inc(i,Length(Field)+1); while Data[i] <> '&' do begin Result := Result + Data[i]; Inc(i) end end end {Value}; var P: PChar; i: Integer; Str: ShortString; type TRequestMethod = (Unknown,Get,Post); var RequestMethod: TRequestMethod = Unknown; initialization P := GetEnvironmentStrings; while P^ <> #0 do begin Str := StrPas(P); if Pos('REQUEST_METHOD=',Str) > 0 then begin Delete(Str,1,Pos('=',Str)); if Str = 'POST' then RequestMethod := Post else if Str = 'GET' then RequestMethod := Get end; if Pos('CONTENT_LENGTH=',Str) = 1 then begin Delete(Str,1,Pos('=',Str)); ContentLength := StrToInt(Str) end; if Pos('QUERY_STRING=',Str) > 0 then begin Delete(Str,1,Pos('=',Str)); SetLength(Data,Length(Str)+1); Data := Str end; Inc(P, StrLen(P)+1) end; if RequestMethod = Post then begin SetLength(Data,ContentLength+1); for i:=1 to ContentLength do read(Data[i]); Data[ContentLength+1] := '&'; { if IOResult <> 0 then { skip } end; i := 0; while i < Length(Data) do begin Inc(i); if Data[i] = '+' then Data[i] := ' '; if (Data[i] = '%') then { special code } begin Str := '$00'; Str[2] := Data[i+1]; Str[3] := Data[i+2]; Delete(Data,i+1,2); Data[i] := Chr(StrToInt(Str)) end end; if i > 0 then Data[i+1] := '&' else Data := '&' finalization Data := '' end.FWIW, I've written over a dozen CGI applications over the past year, and all are now using this DrBobCGI unit, resulting in easy maintenance and tiny and fast executables.
Now, for a real world example: a standard CGI guestbook application (which asks for your name and a ne-lone comment) written in only a few lines of Delphi.
First the CGI Form itself:
<html> <body> <H2>Dr.Bob's Guestbook</H2> <form method="POST" action="http://www.ebob42.com/cgi-bin/guest.exe"> Name: <input type=text name="name"><br> Comments: <textarea cols="42" lines="4" name="comments"></textarea> <p> <input type="submit" value="Send Comments to Dr.Bob"> </form> </body> </html>Now the Delphi CONSOLE application:
program CGI; {$I-} {$APPTYPE CONSOLE} uses DrBobCGI; var guest: Text; Str: String; begin Assign(guest,'book.htm'); // assuming that's the guestbook if FileExists('book.htm') then Append(guest) else begin Rewrite(guest); // create it for the first time writeln(guest,'<html>'); writeln(guest,'<body>') end; writeln(guest,'Date: ',DateTimeToStr(Now),'<br>'); writeln(guest,'Name: ',Value('name'),'<br>'); writeln(guest,'Comments: ',Value('comments'),'<hr>'); reset(guest); while not eof(guest) do // now output guestbook itself begin readln(guest,Str); writeln(Str) end; close(guest); writeln('</body>'); writeln('</html>') end.Which results in a display like the following:
Question:
I have two submit buttons on a HTML page, which implement a go back one page and go forward one page.
I need to determine which button was pressed inside my CGI so that I can perform the appropriate operation.
Dr.Bob Says:
You can just assign a unique value to each "type=submit" button, as you can see by the form below:
<html> <body> Edit the information and press the SAVE button<br> To Delete information, press the DELETE button<br> <p> <form method="post" action="http://www.ebob42.com/cgi-bin/debug.exe"> <hr> <input type=text name=name> <p> <input type=reset value="RESET"> <input type=submit name=action value="SAVE"> <input type=submit name=action value="DELETE"> </form> </body> </html>You should get the "Action=SAVE" (or Action=DELETE) after you press on that button.