whats new ¦  programming tips ¦  indy articles ¦  intraweb articles ¦  informations ¦  links ¦  interviews
 misc ¦  tutorials ¦  Add&Win Game

For more information about IntraWeb visit the IntraWeb Website


For more information about the Indy Project visit the Indy Project Page

Advertising

29 Visitors Online


 
Introduction to Web Development with Delphi
Author: Hadi Hariri
Homepage: http://www.atozedsoftware.com


Introduction

This paper is in an introduction to web development using Delphi’s webbroker technology. It assumes that the reader is familiar with the Delphi environment and Object Pascal, and has basic knowledge on TCP/IP concepts such as ports, IP addresses, etc.

The article is based on numerous examples. To properly understand the contents, it is recommended that the reader follow the examples step by step. The code was originally developed and tested under Delphi 5 but applies to later versions of Delphi as well. Being based on web development, a web server has to be installed. Although Windows 2000 and NT come with Internet Information Server (5 & 4 respectively), it is a complicated process to adapt IIS to be able to debug the examples, and sometimes proves unsuccessful. I recommend using OmniHTTPd, which is a small, fast and free (for non-commercial use) web server and is ideal for development. Indy 9 also contains a webbroker bridge and can be used to build webservers for deployment and testing of webbroker applications.

Installing OmniHTTPd

  1. Download the package from http://www.omnicron.ca/httpd/download.html
  2. If you have IIS installed, it will prompt you to switch the service to manual and close it down. Respond yes.
  3. You will then be prompted to install OmniHTTPd as a service. Reply NO to this question since for debugging purposes it has to run as an application.
  4. Once the process is complete, you can start the web server from the Application group you installed it in. It will sit in your tray.

By default, a local web server is installed and configured under the entry localhost. The paths are:

Server root: C:\Program Files\httpd\HTDOCS
ISAPI extensions: C:\Program Files\httpd\ISAPI

The other folders will not be used in this paper. For more information, consult the help file for OmniHTTPd. If using Windows 2000 or NT for development, make sure that the directory that ISAPI points to has execute permissions

Concepts and terminology

Initiating in the web development world can cause one to be faced with an overwhelming series of terms and hyped-words. Before proceeding to more involved concepts, a series of terminologies will be explained and cleared up.

Web Server: This is software that resides on a server (hardware) and delivers content to the WWW.  Examples of these are Microsoft Internet Information Server or Apache. By default these servers are configured to run on port 80 (or 443 in the case of SSL). The protocol used is HTTP, which stands for Hyper Text Transfer Protocol and is a TCP based protocol.

Web Server Application (or Extension): In its simplest form, a web server extension is a server-side (runs on the server) script that communicates with the client (browser) using HTML. Examples can be Perl, JavaScript, VBScript, etc.

CGI: Stands for Common Gateway Interface. The Common Gateway Interface is a standard for external gateway programs to interface servers such as HTTP servers.
Information is exchanged via the standard IO.

Win-CGI: Win-CGI is a specific platform version of CGI. It works on Win32 platforms and uses ini files to process input and output.

ISAPI/NSAPI: Internet Server Application Programming Interface is specific to IIS and other IIS compatible web server software. NSAPI is Netscape’s Server API. ISAPI applications are normally referred to as extensions.

ISAPI Filter: As the name indicates, it is a filter that sits between your web server and the client. Any request made by the client is filtered via the ISAPI filter. The main difference between a filter and an ISAPI extension is that the latter has to be called explicitly.

CGI and ISAPI

A CGI can either be a script or an executable. For the remainder of this paper, we will always assume that it is an executable. The following diagram demonstrates how the CGI works and its interaction with the client and the web server.

[Image]


When the client makes a new request to the CGI, the web server spawns a new executable. The executable takes the input parameters provided by the client, executes the application and sends the output back to the web server. The server then sends this information back to the client. Every new request, being it from the same client or not will spawn a new executable.

How does an ISAPI work? The process is the same as before, however, ISAPI is implemented as a DLL (Dynamic Link Library).  When the client makes a request, the web server loads the DLL in memory. The request is processed by the ISAPI extension and the result it returned to the client. However, when a second request is made, since the DLL is already loaded in memory, the only step is to process the request and reply to the client. As you can see the main difference between ISAPI and CGI is that with the CGI, the executable has to be loaded into memory, the request processed and then the executable is unloaded from memory. In the case of ISAPI, the DLL is loaded only once and never unloaded, unless it is explicitly unloaded or the web server is shut down. This is the main advantage of using ISAPI over CGI for web extensions.

WebBroker

Delphi’s webbroker technology makes web development very simple. Most of the “dirty work” associated with building CGI’s or ISAPI is done in the background by the webbroker framework. Delphi allows you to create 4 different types of web extensions:

  • ISAPI/NSAPI
  • CGI
  • Win-CGI

This papers focuses mainly on creating ISAPI extensions. However, nearly everything that is covered can be applied to CGI’s also. As mentioned previously ISAPI extensions are implemented as DLL’s. Although the advantage to this is shorter response time to a request, there are some disadvantages also (we will cover these in detail later on).

The TWebDispatcher is the main component behind Delphi’s webbroker technology. It takes care of the interaction between the web server and the extension (or application). HTTP requests are handled and converted to what are known as Web Actions. TWebModule is a descendant from TCustomWebDispatcher. When developing ISAPI extensions using Delphi, the TWebModule is where most of the code is implemented. You can think of TWebModule as a TDataModule with a built-in TWebDispatcher.

The first step in making a web extension using Delphi is to select the type of extension we want to create (in our case we will always select ISAPI). To do this, click on File -> New, select Web Server Application and then choose ISAPI from the dialog box:
 
Once you click on Ok, a new project will be opened. By default a new TWebModule is created. The first thing you will notice is that (as with datamodules) there are very few properties and events.

[Image]

These properties are:

  • Actions
  • Name
  • OldCreateOrder
  • Tag

The last three do not need further explanation as there are the same properties that are common to most (if not all) components. Actions will be explained further on.

There are four events:

  • AfterDispatch
  • BeforeDispatch
  • OnCreate
  • OnDestroy

AfterDispatch and BeforeDispatch are called after and before an action is executed. On. OnCreate and OnDestroy, again, are the same as those of other forms.

Actions constitute the entry point to the web application. When the client (browser) makes a request, is matched up to an Action and the appropriate method is executed. Before examining the properties, let us look at a typical request.

http://localhost/isapi/first.dll/theaction

This is a typical URI (Uniform Resource Locator) to request a service from a web extension.

Localhost, marked in red represents the address where the resource is located. This is normally a domain name such as www.mydomain.com or alternatively an IP address.

Isapi is the path were the web extension is located. Internet Information Server defaults this to scripts.

First.dll is the name of the web extension that the request is referring to.

TheAction is the actual request made to the web extension (ISAPI in our case).

[Image]


 
In Delphi, this URI is represented by a WebActionItem. A TWebActionItem is a TCollectionItem owned by the Actions property of the web module.

Each action is composed of a number of properties. The values these properties take are fundamental to the correct functioning of the application.

These properties are:

  • Default
  • Enabled
  • MethodType
  • Name
  • PathInfo
  • Producer
[Image]

Default indicates whether the action should be considered the default action.

Enabled specifies if the action is active or not.

MethodType can take one of five values: mtGet, mtPost, mtHead, mtPut or mtAny. The first four correspond to the HTTP methods of GET, POST, HEAD and PUT respectively. mtAny indicates that any method is accepted.

Name is what the action is named.

PathInfo indicates the value specified in the URI to access this action. In the previous example of a URI, theaction would correspond to the PathInfo.

Producer is the default producer that is assigned to this action. We will examine this property later on when we discuss PageProducers.

The TWebDispatcher “translates” this request and parses the parameters into a WebAction item. It then runs through all the existing WebActions and finds one with matching properties. If no matching action is found then the default one is executed. When finding a corresponding action, the PathInfo, Enabled and MethodType are compared to see if they match. If an action is marked as Default, these properties take no effect. There can only be on default action.

First ISAPI application

The first ISAPI application is to familiarize the reader with the steps required to build a web extension from scratch and then test it from the browser.

Once the Web Server application project has been created, add a new action by double-clicking on the Actions property. Call it DefaultAction and set the Default property to true. The remaining properties should not be modified. Switch to the events tab of the newly created action and double-click on the OnAction event.

procedure TWebModule1.WebModule1WebActionItem1Action(Sender: TObject;
  Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
begin
 Response.Content := ‘Hello Web!’;
end;

The action is send the string “Hello Web!” back to the client. The event signature has three parameters specific to web applications: Request, Response and Handled.

The code corresponding to this example is located in the Step1 folder. Once the project has been compiled, copy the resulting DLL into the ISAPI folder of the web server (default “C:\Program Files\….\ISAPI”) and launch the following URL for the browser:

http://localhost/isapi/step1.dll

This will launch the application and return the result. Whatever else is specified as the action will be ignored since there is only one action with no PathInfo and it is the default one.

As mentioned in the Introduction, one of the advantages of using OmniHTTPd is that it is very simple to debug the application. To do this, instead of launching OmniHTTPd as a standalone application and opening the browser to call the application, we pass the path to the executable of OmniHTTPd (OHTTPD.EXE) to the Run -> Parameters of the project.  Now you can set your breakpoints and watch how the application executes step by step. The other advantage is that when you stop the execution of your application, OmniHTTPd will shutdown and thus the DLL will be unloaded from memory (avoiding problems of overwriting a loaded file).

Before continuing on, let us just examine the actual code that Delphi generates for us when creating a new Web Server Application.

library Step1;

uses
  WebBroker,
  ISAPIApp,
  Main in 'Main.pas' {WebModule1: TWebModule};

{$R *.RES}

exports
  GetExtensionVersion,
  HttpExtensionProc,
  TerminateExtension;

begin
  Application.Initialize;
  Application.CreateForm(TWebModule1, WebModule1);
  Application.Run;
end.

Now let us look at what the CGI application would generate:

program Step1;

{$APPTYPE CONSOLE}

uses
  WebBroker,
  CGIApp,
  Main in 'Main.pas' {WebModule1: TWebModule};

{$R *.RES}

begin
  Application.Initialize;
  Application.CreateForm(TWebModule1, WebModule1);
  Application.Run;
end.

As you can see, there are very few differences between the two project files. In fact, using conditional defines, you can easily compile the same code and produce either a CGI executable or an ISAPI DLL. CGI ‘s are sometimes good for debugging since they do not require the server to be shutdown in order to replace them with a new version.

TWebRequest & TWebResponse

Along with Actions, TWebRequest and TWebResponse are the other two most important objects used in web development. They contain the request and response information respectively between the web server and the application. As seen previously, every WebActionItem passes these two objects to the event handler. Depending on the type of application that is being developed (ISAPI, CGI, Win-CGI), the TWebRequest is either TISAPIRequest, TCGIRequest or TWinCGIRequest. The same applies for TWebResponse.

TWebRequest contains the information sent by the web server to the application. This information can be accessed either by the properties of the object or via its methods. Properties include things like the UserAgent (the browser software), the Referer (the URI that made the request), RemoteAddr, QueryFields, CookieFields, etc. The number of properties are too numerous to mention them one by one. For more information and description on each property please refer to the online help. However, we will examine some of these in more detail later on.

Similar to the TWebRequest, the TWebResponse has a number of properties and methods used to send information back to the web server. In summary, information contained in the HTTP request to the web application is parsed into these two objects and can be read and set using the correct properties and/or methods.

Submitting information to the web application

A web application would not be of much use if one could not send it information, since there would not be much difference to a normal static page. Most web applications work off of user input. Examples of sites which require user input are online banking systems, web mail and in general forms that are filled in by the user for one purpose or another. Using properties and methods of the TWebRequest object, it is very easy to obtain user input.

HTTP provides two methods to send information via the request: POST and GET.  The main difference between these methods is the way in which the information is sent. With GET, the information is contained in environment variables where as with POST it is sent using a data stream connection to the server. Other differences include the limit of the amount of data that can be sent. GET is restricted to the number of characters allowed in the URL (which depends on the server). POST does not have any limitations. Normally, when using GET, the information is passed in the URL:

http://localhost/isapi/step1.dll/action?VarA=1&VarB=2&VarC=3

With POST, the information is contained in form fields in the actual HTML page. A HTML form is made up of a form tag and various input fields:

<form method="post" action="http://localhost/isapi/step2.dll/form">

Name: <input type="text" name="NAME_FIELD">

Surname: <input type="text" name="SURNAME_FIELD">

<input type="submit" name="Submit" value="Submit">

<input type="reset" name="reset" value="Reset">
</form>

The “method” indicates whether we are using a POST or a GET. Normally with forms, the default is POST. The “action” specifies the complete URI to access the corresponding WebActionItem. When the form is submitted, the TWebDispatcher parses the fields on the form into the ContentFields property of the TWebRequest object. This is a TStringList with the normal name and value pair. The project Step2 demonstrates how to access the values using this property. As before, make sure that the output directory in the project file points to the location where the ISAPI should be (or alternatively copy it manually). For this example (and for the ones following), we need to place the html page into the HTDOCS folder.

procedure TWebModule1.WebModule1FormAction(Sender: TObject;
  Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
begin
Response.Content := '<HTML><BODY>Hello '+Request.ContentFields.Values['NAME_FIELD'] +
                      ' ' + Request.ContentFields.Values['SURNAME_FIELD'] +
                      ' how are you today?</BODY></HTML>';
end;

As you can see from the code, the name that is passed to the ContentFields.Values property is the same as that on the HTML form. Content just sets the HTML content to return to the client. The same method can be used to access any type of HTML form field. With Checkboxes and RadioGroups there is a slight difference. In these cases, if the checkbox (or radiogroup) is marked, then  the field exists in the ContentFields list and the value is returned. On the other hand, if it is not checked, then the value is not passed into the ContentFields. To find out if a particular checkbox is checked on a form field just do:

if Request.ContentFields.IndexOfName('Tick1') <> -1 ….
The following URL sends information to the web server via the GET method:

'http://localhost/isapi/step3.dll/action?proto=http&target=www.borland.com

The QueryFields property can be used to access these parameters. It works in much the same way as ContentFields (Stringlist with a name, value pair).

procedure TWebModule1.WebModule1GetActionAction(Sender: TObject;
  Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
begin
  if (Request.QueryFields.Values['proto'] = 'ftp') or (Request.QueryFields.Values['proto'] = 'http') then
    if Request.QueryFields.Values['target'] <> '' then
      Response.SendRedirect(Request.QueryFields.Values['proto'] + '://' + Request.QueryFields.Values['target'])
    else
      Response.Content := '<HTML><BODY>Invalid URI</BODY></HTML>'
  else
    Response.Content := '<HTML><BODY>Invalid Protocol</BODY></HTML>';
end;

The Step3 example examines the values passed in via the proto and target parameter and redirects the client to the URL specified by the latter. Two protocols are accepted: ftp and http. To run this enter a URL in the web browser of the form:

http://localhost/isapi/step3.dll/get?proto=http&target=www.borland.com

The parameters are separated by the & symbol. The ? is what separate the action from the list of parameters.

So far we have used to ways to respond to the client: setting the Content property and using the SendRedirect method. SendRedirect takes as a parameter a URI and when called redirects the output to the specified URI. Content is normally used to return an HTML string to the client. However, other things can also be returned, such as files, images, etc. There is another way to return contents, which we will see later on, using Producers.

Step4 demonstrates how to return different types of content depending on the parameter passed in the URL.

procedure TWebModule1.WebModule1ResponseActionAction(Sender: TObject;
  Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
var
  LType,
  LValue: string;
  LHTML: TStrings;
  LFile: TFileStream;
begin
  LType := Request.QueryFields.Values['type'];
  LValue := Request.QueryFields.Values['value'];
  if AnsiSameText(LType, 'html') then begin
    LHTML := TStringList.Create;
    with LHTML do try
      Add('<HTML><TITLE>HTML Response</TITLE>');
      Add('<BODY>' + LValue);
      Add('</BODY></HTML>');
      Response.Content := LHTML.Text;
    finally
      FreeAndNil(LHTML);
    end;
  end else if AnsiSameText(LType, 'file') then begin
    if not FileExists(LValue) then begin
      Response.Content := '<HTML><BODY>Invalid File</BODY></HTML>';
    end else begin
      LFile := TFileStream.Create(LValue, fmOpenRead);
      Response.SetCustomHeader('Content-Disposition', 'filename=' + ExtractFileName(LValue));
      Response.ContentStream := LFile;
    end;
  end;
end;

The first part of the code does not really contain anything new. It uses the Content property to return HTML to the client. However, instead of just assigning the value directly, it fills in a StringList with some HTML code and returns the Text property. This can be used  as an alternative to Producers, to generate dynamic HTML. The second part of the if is new code.

      LFile := TFileStream.Create(LValue, fmOpenRead);
      Response.SetCustomHeader('Content-Disposition', 'filename=' +
     ExtractFileName(LValue));
      Response.ContentStream := LFile;

Here we are returning a file as the response to the request. To do this we read the file passed in as a parameter in the URL and assign the stream to the ContentStream property. It is not necessary to free the LFile variable. Doing so would prevent the code from functioning correctly. The Response will take care of freeing the object automatically once it has finished. SetCustomHeader adds a custom HTTP header to the response. In this case we are adding the header ‘Content-Disposition’ with the value ‘filename=…’. Since we set this value, when the browser prompts us for the location where we want to save the file, the filename will automatically appear in the dialog box.

Using the ContentStream property, we can return a wide variety of information back to the client. In this case it was a file on the disk but it can also be images, video, audio, etc.

Note that this example posses a very large security risk since anyone could access a file from the machine if they knew the filename (including the path). This is for demonstration purposes only and in real-life scenarios the correct permissions (and folder) would have to be set and created.

Producers

Although we have seen that the Content property can be used to return HTML to the client, it is not always comfortable to use this method. Web development is not always done exclusively by one person; it can involve a whole team that includes developers, content designers, web designers, etc. If we were to use the Content to return an HTML page to the client, we would need the source code for this HTML from the web designer. Any change in the page would require us to make a new build of the application and would carry along all the problems involved in this.

Fortunately there is an easier way to do this. Producers can be thought of content providers that return information to the client in HTML format. There are four types of producers are:

  • TPageProducer
  • TDataSetPageProducer
  • TQueryTableProducer
  • TDataSetTableProducer

All of them descend at some point from the TCustomContentProducer. Depending on the specific needs the appropriate one can be used. The last three are all data-aware producers.

[Image]

The two most important properties of a TPageProducer are the HTMLDoc and HTMLFile properties. They are mutually exclusive, i.e., setting one of them will clear the other one. They are used to provide the content to the PageProducer, which in turn will provide the contents to the TWebDispatcher (and this one to the client). The content of the PageProducer is normal HTML code. However, to allow pages with dynamic information, the PageProducer allows the insertion of special tags which are HTML-transparent. These tags can then later be replaced with values before streaming the contents back to the client.

The format of the tags are <#TAGNAME>. Returning to the example in step2 where the form values entered by the user were returned via the Content property, let us do the same this time but use a PageProducer to accomplish it.

In this example we are going to assign an HTML file to the HTMLFile property of the PageProducer. The contents of this file is:

<HTML><TITLE>Step 5</TITLE>

<BODY>

Hello <#NAME> <#SURNAME>, How are you?

</BODY>
</HTML>

We have inserted two tags, one named <#NAME> and the other <#SURNAME>. In the code we assign the file (step5_2.htm) to the HTMLFile property as indicated before. We then assign the PageProducer to the Producer property of the action we created (TagsAction). To run this example, type the URL for step5_1.htm in your browser (http://localhost/step5_1.htm). This will produce the same for as in Step 2. Now fill in the values and hit Run. If we were to run the application, the output would be:

Hello, How are you?

The reason for this is that even though the fields are contained in the ContentFields, we are not doing anything with them yet. In other words, we are not replacing our tags with these values.  To do this we can use the OnHTMLTag event of the PageProducer:

procedure TWebModule1.PageProducerHTMLTag(Sender: TObject; Tag: TTag;
  const TagString: String; TagParams: TStrings; var ReplaceText: String);
begin
  if TagString = 'NAME' then begin
    ReplaceText := Request.ContentFields.Values['NAME_FIELD'];
  end else if TagString = 'SURNAME' then begin
    ReplaceText := Request.ContentFields.Values['SURNAME_FIELD'];
  end;
end;

What we are doing in this code is examining the name of the Tag (defined by the value of the parameter TagString) and then replacing it with the appropriate value of the ContentFields by assigning this value to the ReplaceText variable.. If you notice, there is no Response and Request parameter. So how can we access the values of the ContentField, and where is Request defined? When a new request is made to the web application, the contents of the current Request (and Response) are copied to two properties of the TWebModule that have the same name. This makes it possible for us to have access to the current values from events in the web module. In this code we are accessing the properties of the TWebModule.

If you execute the code with this event handler in place you will see the tags replaced with the values passed into the web application. Another point to mention is that before Delphi 5, the Response had to be assigned in code. This would be done by assigning the PageProducer’s Content method to that of the Response:

Response.Content := PageProducer.Content;

PageProducer.Content is a method that parses the contents of the PageProducer, calls the event handler and returns the result as a string. As of Delphi 5, this can be assigned visually by setting the Producer property of the action to point to the PageProducer.
 
The source code for this HTML page could have also been inserted directly into the HTMLDoc property. However this eliminates the advantage of preventing rebuilds of the application if the page design changes.

Maintaining state information

Imagine a normal desktop application where the user has to authenticate him/herself before entering the application. Once authenticated, the application can then display specific options that are available for that user. This is because the application knows that the user has already validated him/herself. This is very normal behavior and used frequently when developing applications. It would be good to have this ability in web applications also. The problem is that HTTP is a stateless protocol. It does not maintain state information between successive requests. When a client makes a request, the web server does not know what the state of that particular client was prior to making the new request.

To overcome this lack of state information, we can add some session management. The three most common ways of accomplishing this is:

  • Fat URL
  • Hidden Fields
  • Cookies

Passing information from one request to another can be done using what is known as a Fat URL. The information is passed via the URL. For example, if we were to get the name in the previous example, and pass this information to other forms, we could add a parameter on the URL containing this value and then in successive requests access it using the QueryFields:

Response.SendRedirect(‘http://localhost/isapi/step5.dll/tags?name=’ +
 Request.ContentFields.Values[‘NAME_FIELD’]);

In the next form, we could then access the name field using:

LName := Request.QueryFields.Values[‘name’];

The disadvantage to this method is that the parameters are displayed on the URL and can be subject to modification. Also, if the number of parameters is large, it could get quite messy and also be restricted to a certain limit.

Another alternative is to use hidden fields. When using html tags to represent dynamic information (see Step5), the value is displayed as text in the resulting html page. We cannot access this value anymore. However, if we were to define what is known as a hidden field with the same value, we could later on read it using ContentFields:

<form method="post" action="http://localhost/isapi/step2.dll/form">

Name: <input type="text" name="NAME_FIELD">

Surname: <input type="text" name="SURNAME_FIELD">

<input type="submit" name="Submit" value="Submit">

<input type="reset" name="reset" value="Reset">
<input type=”hidden” name=”hidden_name” value=”<#HIDDEN_NAME>”>
</form>

In the OnHTMLTag we would add a new line:

if TagString = ‘<#HIDDEN_NAME>’ then begin
 ReplaceText := Request.ContentFields.Values[‘NAME_FIELD’];

In successive forms we could access the value by using the ContentFields.

Although this method eliminates the need of passing parameters on the URL, it introduces another problem.  When we have to work with multiple forms, it is quite cumbersome to add and keep track of hidden fields. It also makes the code look complicated.

The ideal solution is using Cookies. Cookies can be thought of “records” that hold information on the client side (browser) and can be set and read from the web application. They are held either in memory (while the web browser is open) or optionally can be saved to disk. There is a misconception that cookies pose a security risk. This is not entirely true. If used in the correct way cookies are very beneficial. When a cookie is set by a web application that is executed on a particular URL, only the same host can read the cookie. For example, a cookie set by www.borland.com cannot be read by www.microsoft.com.

Managing cookies with Delphi is very simple. Like the ContentFields and QueryFields, there is another property called CookieFields, which is a TStringList that contains a name, value pair. Project Step6 shows how to read and set cookies. To run the example (and see how the cookie actually works), open up the browser and point it to:

http://localhost/isapi/step6.dll/cookie

This executes the following code:

    LCookie := TStringList.Create;
    with LCookie do try
      LValid := Now + 10;
      Add('CookieStep6=Cookie for Step6 project');
      Add('SetAt=' + FormatDateTime('dd/mm/yyyy hh:mm', Now));
      Add('ValidUntil=' + FormatDateTime('dd/mm/yyyy hh:mm',
  LValid));
      Response.SetCookieField(LCookie, '', '/isapi', LValid, False);
      Response.Content := '<HTML><BODY>Cookie set</BODY></HTML>';
    finally
      FreeAndNil(LCookie);
    end;

What this does is set the cookie values in a stringlist and then call SetCookieField. The method takes 5 parameters. The first is the stringlist containg the cookies. The second parameter is the domain name. The third parameter is the path. LValid indicates how long the cookie will be valid for (in our case 10 days from now) and the last parameter indicates whether or not the cookie is set of a secure connection.

When we call the URL, the cookie will be set and a message saying “Cookie set” appears. Close down the browser and open it up again and call the same URL. This time, the code executed is:

    Response.Content := PageProducer.Content;

The PageProducer contains some tags in its HTMLDoc property that will be replaced in the OnHTMLTag with the values contained in the CookieFields. In this case we have used the HTMLDoc as opposed to the HTMLFile property. Also note that we do not assign the PageProducer to the Producer property of the action since we do not always want this to be the default content. After calling the same URL, the code will check to see if a cookie has been set and if so display its contents. If you look on your hard drive where your cookies are normally kept you will see a new file created for the cookie just set.

Cookies will be used extensively when we cover database applications below, to maintain state information and the users details. In real-life applications, the expiry date is very important. If we were to create an application that requires login and we were to store this information on the hard drive, anyone could later open up the browser and access the application without having to authenticate. To prevent this misuse, we pass –1 to the expiry parameter of the cookie. This will indicate that the cookie is only present while the browser window is open (it is only contained in memory and not on the hard drive).

Database-enabled applications

Web development would not be very much use if we could not interact with database to store and retrieve information off of.  The remainder of this article focuses on the development of an online database web application. For this example we are going to use the Interbase Customer database example that ships with Delphi. Access to the database will use the BDE.

All the concepts learned so far will be applied from now on. Some more advanced features will also be explained. Before proceeding, make sure that you have the BDE correctly configured with the IBLocal alias. Also, for practical reasons, the username and password for Interbase is hard-coded into the TDatabase component and defaulted to SYSDBA/MASTERKEY. If your Interbase installation has different credentials, modify these values accordingly.

This is a simple example of accessing a customer database using a web browser. For demonstration purposes the user has to validate him/herself with a username and password before accessing the database. All the HTML files that are used by PageProducers should be located in a private location on your hard drive where the web client does not have access. They only file that should be placed in the public HTTPDocs directory is the login.htm page used for authentication. This is also going to act as the entry point to the application.

The first step is to setup a TDatabase. Set the ALIAS to IBLocal and fill in the username/password details in the Params property. The next vital component required is a TSession. As mentioned in the section CGI and ISAPI, an ISAPI is a DLL. It is created once and destroyed once. Every request launches a new instance of the TWebModule but uses the same instance of the DLL. The disadvantage to this is that when developing applications, certain things have to be avoided such as global variables, resetting components, etc. When working with the BDE, a TSession is required to correctly manage multiple sessions.

The Database connection is opened when the DLL is first loaded into memory and closed when it is destroyed. This eliminates the overhead of opening/closing the connection with each request. To accomplish this, we place the following code in the OnCreate/OnDestroy of the webmodule.

procedure TWebModule1.WebModuleCreate(Sender: TObject);
begin
  Database.Open;
end;

procedure TWebModule1.WebModuleDestroy(Sender: TObject);
begin
  Database.Close;
end;

Although by default, the behavior of an ISAPI application is to call these two methods once only, there is a way to override this by writing

Application.CacheConnections := False;

in the project file. This will cause the ISAPI to be created/destroyed with each request. For all practical purposes, this should never be used because it would defeat one of the main reasons for developing ISAPI applications (lower overhead).

Since our application requires authentication, the user has to first login. To know if the user has logged in we can set a cookie that will be valid only while the browser is open. On each request (WebActionItem) we can then check to see if the cookie exists and if it does allow the user access. The problem is that we would need to repeat the same code for every action. By placing this code in the OnBeforeDispatch event of the webmodule, it will be called before any action. This way we keep the code centralized to one place. The event handler for the OnBeforeDispatch is like that of the WebActionItems.

procedure TWebModule1.WebModuleBeforeDispatch(Sender: TObject;
  Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
begin
  if AnsiSameText(Request.PathInfo, '/action') or
    AnsiSameText(Request.PathInfo, '/page') then begin
    if Length(Request.CookieFields.Values['Username']) > 0 then begin
      Handled := False;
    end else begin
      Handled := True;
      Response.Content := '<HTML><BODY>Unauthorized
     Access</HTML></BODY>';
    end;
  end else if AnsiSameText(Request.PathInfo, '/login') then begin
    Handled := False;
  end else begin
    Handled := True;
    Response.Content := '<HTML><BODY>Invalid Request</BODY></HTML>';
  end;
end;

The above code might look small but in actual fact it quite a lot for its size. First, it checks to see if the request is trying to access a restricted zone (depending on the action item). If so, it then checks to see if there is a cookie with the value of Username greater than 0. If that is the case, it will set the Handled property to False. What does this do? By setting the Handled parameter, we indicate whether the request has been completed or needs further processing. In this case, if the user has been authenticated (the cookie field exists), by setting Handled to false, it will then continue to process the request by either calling the /action or /page WebActionItem, depending on which one was requested. If on the other hand the cookie field is not present, it will set Handled to True to indicate that the request has been processed (handled) and displays a message indicating an unauthorized access attempt has been made.

The second IF statement checks to see if the PathInfo is /login. If that is the case, it means that the user has not logged in yet and the /login action should be executed (Handled = False). If the PathInfo does not coincide with either /login, /action or /page, then a message is displayed indicating that the request is invalid.

The login action checks to see if the username and password are correct and if so sets the cookie and then redirects the user to another request from the DLL that returns a private menu.

procedure TWebModule1.WebModule1LoginActionAction(Sender: TObject;
  Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
var
  LCookie: TStringList;
begin
  if (AnsiSameText(Request.ContentFields.Values['Username'], 'SYSDBA')) and
    (AnsiSameText(Request.ContentFields.Values['Password'], 'MASTERKEY'))
       then begin
      LCookie := TStringList.Create;
      with LCookie do try
        Add('Username=XXXXXXX');
        Response.SetCookieField(LCookie, '', '/isapi', -1, False);
        Response.Content := '<meta http-equiv="refresh"
 content="0;URL=http://localhost/isapi/step7.dll/page?value=menu.htm">';
      finally
        FreeAndNil(LCookie);
      end;
  end else begin
    Response.Content := '<HTML><BODY>Invalid Username/Password</BODY</HTML>';
  end;
end;

Everything in the code above has been explained before. However, if you notice, instead of using Response.SendRedirect to redirect the request, we use the Response.Content property. The reasons for this is that Response.SendRedirect does NOT work if called after Response.SetCookieField. To overcome this we use a little trick that consists of sending a “redirect” header to the client and making the client call the redirect.

The Page action returns a specific HTML file defined by the parameter “value”, to the client. In the previous code, the menu.htm page is returned. The reason, as mentioned previously, that these files are returned via the application and not as direct links is because they have to be kept private so that unauthorized people do no have access to them.

The first action that we are going to look at is the Customer listing. Although we could manually run through the records and create an HTML output, there is a simpler way to accomplish this, which requires no coding whatsoever. Drop a TQuery component on the webmodule and setup the properties to point to the database. In the SQL statement enter:

SELECT * FROM CUSTOMER

Now drop a TDataSetTableProducer  and link up the DataSet property to point to the CustomerQuery. Double-click on the component and a property editor will come up. By clicking on the icon to add all fields we can then individually go through them formatting the table to our personal taste and eliminating those fields not required. It would be more efficient to reduce the number of fields with the SELECT statement but for practical reasons we have not done so.

The only thing left now is to add a few lines of code:

procedure TWebModule1.WebModule1ActionActionAction(Sender: TObject;
  Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
var
  LValue: string;
begin
  LValue := Request.QueryFields.Values['value'];
  if AnsiSameText(LValue, 'list') then begin
    CustomerQuery.Open;
    try
      Response.Content := ListProducer.Content;
    finally
      CustomerQuery.Close;
    end;
  end;
end;

We assign the Response.Content, the result of the ListProducer.Content method. Before doing so however, we have to open the CustomerQuery and close it after the contents has been assigned to the Response.

To modify/delete a record, we must first display the list of all available records. This is done in the same way as previously:

  end else if AnsiSameText(LValue, 'display') then begin
    CustomerQuery.Open;
    try
      Response.Content := DisplayProducer.Content;
    finally
      CustomerQuery.Close;
    end;
  end;

However, in this case we assign an event to the OnFormatCell. We use this to add a checkbox in the first column and a link in the second column. Also, in the header and footer of the producer we add a form tag with the appropriate action.

procedure TWebModule1.DisplayProducerFormatCell(Sender: TObject; CellRow,
  CellColumn: Integer; var BgColor: THTMLBgColor; var Align: THTMLAlign;
  var VAlign: THTMLVAlign; var CustomAttrs, CellData: String);
begin
  if CellRow > 0 then begin
    if CellColumn = 0 then begin
    CellData := '<input type="checkbox" name="checkbox' +
 CustomerQuery.FieldByName('CUST_NO').AsString + '" value="T">';
    end else if CellColumn = 1 then begin
      CellData := '<a
 href="http://localhost/isapi/step7.dll/action?value=modify&record=' + CellData + '">' +
        CellData + '</a>';
    end;
  end;
end;

The checkboxes are named as the string “checkbox” + the Customer n?. This value is obtained from the CustomerQuery. This will useful to delete the appropriate record.

When the user clicks on the checkboxes and then submits the form, the following code is executed:

  end else if AnsiSameText(LValue, 'delete') then begin
    LSQL := CreateDeleteStatement;
    if Length(LSQL) > 0 then begin
      try
        GenericQuery.SQL.Clear;
        GenericQuery.SQL.Add(LSQL);
        GenericQuery.ExecSQL;
       
 Response.SendRedirect('http://localhost/isapi/step7.dll/page?value=m
  enu.htm');
      except
        on E:Exception do
          Response.Content := '<HTML><BODY>' + E.Message + '</BODY></HTML>';
      end;
    end;

The SQL statement is generated by running through the ContentFields and examining the checkboxes.

To modify a record, we click on the link of the Customer number field. Using a TDataSetPageProducer we can design an html page (in our case a form that will later used to submit the changes) with tags that have the same name as the underlying fieldnames in the database. The TDataSetPageProducer is linked to a TDataSet which will select the appropriate record.

  end else if AnsiSameText(LValue, 'modify') then begin
    LRecord := Request.Queryfields.Values['record'];
    if Length(LRecord) > 0 then begin
      RecordProducer.HTMLFile := GHTMLPath + 'record.htm';
      RecordQuery.ParamByName('CUST_NO').AsString := LRecord;
      RecordQuery.Open;
      try
        Response.Content := RecordProducer.Content;
      finally
        RecordQuery.Close;
      end;
    end;
  end else if AnsiSameText(LValue, 'update') then begin
    LSQL := CreateUpdateStatement;
    GenericQuery.SQL.Clear;
    GenericQuery.SQL.Add(LSQL);
    try
      GenericQuery.ExecSQL;
    
 Response.SendRedirect('http://localhost/isapi/step7.dll/page?value=menu.htm');
    except
      on E:Exception do
        Response.Content := '<HTML><BODY>' + E.Message + '</BODY></HTML>';
    end;
  end;

The first IF statement is the one that calls the TDataSetPageProducer (named RecordProducer in our case). It first sets the parameter of the query (the record number) and then opens the query and returns the result as the Content property. The second IF statement is what performs the actual update once the record has been presented to the user. A simple SQL update statement is generated and executed.

Other Web Development

If you have previous experience in web development using other CGI frameworks, ASP, PHP or other you will find webbroker quite natural. However if you are new to web development and prefer a more RAD approach, you should check out the IntraWeb framework. IntraWeb allows you to develop web applications in a manner similar to developing normal Delphi applications. That is simply by creating forms and adding your controls and Delphi code without requiring you to implement many of the lower level details as with webbroker.

Sample Code

The code discussed in this article is available for download here.

About the Author

Hadi Hariri is a Senior Developer and Project Manager at Atozed Software and is also Project Co-coordinator for Internet Direct (Indy), the Open-source project of TCP/IP components that is included in both Kylix and Delphi 6. Having formerly worked for an ISP and software development company, he has extensive knowledge with Internet and client/server applications as well as network administration and security. Hadi is married and lives in Spain, where he has been a major contributing author to a leading Delphi magazine and has spoken at Borland Conferences and user groups.



Copyright © by SwissDelphiCenter.ch
All trademarks are the sole property of their respective owners