| Delphi Clinic | C++Builder Gate | Training & Consultancy | Delphi Notes Weblog | Dr.Bob's Webshop |
| ||||||
As most of you will know by now, we can write all kinds of powerful internet applications with Delphi. However, as webmaster of a populair website, I often feel the need for internet support applications; tools that will help me in managing a website. For example a hit counter, a guestbook, a broken link detector, and an automatic FTP uploader (to upload new webpages) and downloader (to automatically download files from the net).
Since not everybody uses the Client/Server edition of Delphi 3, we'll only use "bare bones" techniques such as my DrBobCGI unit or the Microsoft WinINet DLL and import unit, available freely. If you did not read my Publish Your Database on the Web chapter (with the CGI basics), which introduced HTML and GCI basics, then I strongly suggest reading that first.
1. Hit Counter
A hit counter is one of the first needs for a popular website.
I'm always very interested in the number of people that visit my site from day to day, and it's always interesting to see how weekends or holidays can affect the "hitage" (a word I just invented) on a website.
To keep track of the number of visitors, I just create a one-line file called "counter", containing the number of hits.
The only other thing we need is a very simple CGI program that reads this file, increments the number, and writes the new number back to the file (overwriting the previous value).
Of course, it would be nice to report the number of hits as well, either as a nice picture, or as a simple text message.
{$APPTYPE CONSOLE}
{$I-}
var
f: Text;
i: Integer;
begin
System.Assign(f,'counter');
reset(f);
if IOResult = 0 then readln(f,i)
else i := 0;
Inc(i);
rewrite(f);
writeln(f,i);
close(f);
if IOResult <> 0 then { skip };
writeln('Content-type: text/html');
writeln;
writeln('<HTML>');
writeln('<BODY>');
writeln('<CENTER>');
writeln('You are user <B>',i,'</B> of Dr.Bob''s Delphi Clinic');
writeln('</CENTER>');
writeln('</BODY>');
writeln('</HTML>')
end.
The program above reports the current "hitage" in a text message, which can be used in a frame-setting to report the hit count as follows:
<HTML>
<FRAMESET ROWS="64,*">
<FRAME SRC="http://www.ebob42.com/cgi-bin/hitcount.exe?"NAME="Head">
<FRAME SRC="guest.htm"NAME="Main">
</FRAMESET>
</HTML>
This was a very simple CGI application.
One that didn't even input, but merely had to update a remote file on the web server and return a dynamic webpage.
Let's now focus on a slightly more complex CGI application - one that does require input - such as a basic guestbook for a website.
2. Guestbook
For a true CGI example: a guestbook application (which asks for your name and a ne-lone comment) written in only a few lines of Delphi, we need only slightly more.
First the CGI Form itself:
<HTML> <BODY> <H2>Dr.Bob's Guestbook</H2> <FORM ACTION="http://www.ebob42.com/cgi-bin/guest.exe" METHOD=POST> Name: <INPUT TYPE=text NAME=name><BR> Comments: <TEXTAREA COLS=42 LINES=4 NAME=comments> <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,'guest'); // assuming that's the guestbook
Append(guest);
if IOResult <> 0 then // open new guestbook
begin
Rewrite(guest);
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);
writeln('Content-type: text/html');
writeln;
while not eof(guest) do // now output guestbook itself
begin
readln(guest,Str);
writeln(Str)
end;
close(guest);
writeln('</BODY>');
writeln('</HTML>')
end.
And of course, you're free to test Dr.Bob's Guestbook right here and now:
Note that, to keep things simple, we didn't use a database to store the guestbook comments. Of course, this would require installing the BDE on the web server.
3. Broken Link Detector
Any serious website and webmaster should always be on the look-out for broken links.
And while broken external links can happen (because another website goes out-of-business, for example), there's no excuse for a broken "internal" link.
And for this I've written a simple program called HTMLINKS that can be used to scan .HTM files while they're still on your local machine (i.e. before you upload them to your web server).
The .HTM files from the current directory and all subdirectories are recursively read and parsed for occurances of "<A HREF=" or "<FRAME SRC=".
If the destination webpage is a local one (i.e. without "http://" part), then the file is opened using the relative filepath.
If the destination webpage cannot be found, then we're dealing with a broken internal link, which should be fixed at all cost !!
Note that this program skips all "file://", "ftp://", "mailto:", "news:" and ".exe?" items when they occur inside the "HREF" part.
Of course, you're free to extend HTMLINKS to check for these as well, although one should realise that a connection to the internet is needed to be able to check for the truly external links.
FWIW, I've written such a external broken link detector as an article for The Delphi Magazine, for which more details can be found on my website.
For the local broken link detector, the source code is as follows:
{$APPTYPE CONSOLE}
{$I-,H+}
uses
SysUtils;
var
Path: String;
procedure CheckHTML(const Path: String);
var
SRec: TSearchRec;
Str: String;
f: Text;
begin
if FindFirst('*.htm', faArchive, SRec) = 0 then
repeat
Assign(f,SRec.Name);
Reset(f);
if IOResult = 0 then { no error }
while not eof(f) do
begin
readln(f,Str);
while (Pos('<A HREF="',Str) > 0) or
(Pos('FRAME SRC="',Str) > 0) do
begin
if Pos('<A HREF="',Str) > 0 then
Delete(Str,1,Pos('HREF="',Str)+8-3)
else
Delete(Str,1,Pos('FRAME SRC="',Str)+10);
if (Pos('#',Str) <> 1) and
(Pos('http://',Str) <> 1) and
(Pos('mailto:',Str) <> 1) and
(Pos('news:',Str) <> 1) and
(Pos('ftp://',Str) <> 1) and
(Pos('.exe?',Str) = 0) then { skip external links & exe }
begin
if Pos('file:///',Str) = 1 then Delete(Str,1,8);
if (Pos('#',Str) > 0) and
(Pos('#',Str) < pos('"',Str)) then Str[Pos('#',Str)] := '"';
if not FileExists(Copy(Str,1,Pos('"',Str)-1)) then
writeln(Path,'\',SRec.Name,': [',Copy(Str,1,Pos('"',Str)-1),']')
end
end
end;
Close(f);
if IOResult <> 0 then { skip }
until FindNext(SRec) <> 0;
FindClose(SRec);
// check sub-directories recursively
if FindFirst('*.*', faDirectory, SRec) = 0 then
repeat
if ((SRec.Attr AND faDirectory) = faDirectory) and
(SRec.Name[1] <> '.') then
begin
ChDir(SRec.Name);
CheckHTML(Path+'\'+SRec.Name);
ChDir('..')
end
until FindNext(SRec) <> 0;
FindClose(SRec)
end {CheckHTML};
begin
writeln('HTMLinks 4.0 (c) 1997-2000 by Bob Swart (aka Dr.Bob - www.drbob42.com)');
writeln;
FileMode := $40;
GetDir(0,Path);
CheckHTML(Path)
end.
4. FTP Upload/Download
Sometimes we just want to download a file from the internet.
And in internet terms, this means we'd want to use an FTP (file transfer protocol) client.
And if you're like me, you don't just use (or trust) any FTP client, no, if you're like me, you want to write your own...
FTP
As I said in the introduction, FTP stands for File Transfer Protocol, which is formally described in RFC 959.
The FTP communication model can be implemented using sockets, although this is a rather low-level approach (and if you read the specs, you'll soon realise that writing your FTP client program from scratch based on sockets is not an easy task).
On the other hand, we'll have the NetManage TFTP component in Delphi 2.01 (and up) and C++Builder.
However, this is where the trust comes in.
I've tried to use this component several times, and it's just plain buggy (it chops off files bigger than 10 Kb for example).
I can understand why Microsoft (who initially developed the Internet Solutions Pack) didn't want them and sold them to NetManage, who wouldn't manage them and now sold them to NetMasters.
The trouble is that the Internet Solutions Pack - while free - consists of a set of rather limited internet development ActiveX controls, and so far each company that offers them also has a better solution (usually not free).
So support and documentation has always been lacking...
So, back to the low-level socket approach? Not quite.
This time it's Microsoft who's coming to our rescue (that must be a rather unique experience for some of us, eh?)
WININET
Some time ago, Microsoft released WININET, which is nothing more than a layer on top of low-level Internet APIs especially for Win32 programmers.
WININET offers a higher level interface to otherwise low-level protocols such as HTTP and FTP.
The use is really easy, and the best thing is: the WININET.PAS unit with the API definitions in ObjectPascal is already included with your copy of Delphi 2.x or higher!
There's also a big document describing all WININET APIs in detail, which can be found on Microsoft's website (the location has changed from time to time, so it's best to use their search engine to locate it if you want to download the latest copy).
DrBobFTP
WININET uses something which they call an "internet handle" (much like Windows uses Windows handles), and all APIs either need or return such a handle.
For example, to open a new WININET session, we need to call InternetOpen, which returns a handle which we must use (and pass to other APIs) until the end of our session.
To close any internet handle, we must always call InternetCloseHandle (so after we've received a handle we can use, we should immediate write a try-finally block, where we close the handle in the finally part again).
To open a remote file (or URL) on the internet, we must call InternetOpenURL, which again returns an internet handle.
Now, to download that remote file (URL) to our local machine, we only have to make a number of calls to InternetReadFile, which works a bit like BlockRead, in that it copies data from the remote file to a data buffer.
We can use BlockWrite to write the data from the buffer to a local file, and hence with only three WININET functions (four if you count the closing InternetCloseHandle function), we can write a basic but fast FTP client program as follows:
program DrBobFTP;
{$APPTYPE CONSOLE}
{$I+}
uses
Windows, WinINet;
procedure CopyURL(const URL, OutputFile: String);
const
BufferSize = 1024;
var
hSession, hURL: HInternet;
Buffer: Array[0..Pred(BufferSize)] of Byte;
BufferLength: DWORD;
f: File;
begin
hSession := InternetOpen('DrBob',INTERNET_OPEN_TYPE_PRECONFIG,nil,nil,0);
try
hURL := InternetOpenURL(hSession, PChar(URL), nil,0,0,0);
try
Assign(f, OutputFile);
Rewrite(f,1);
repeat
InternetReadFile(hURL, @Buffer, BufferSize, BufferLength);
write('.');
BlockWrite(f, Buffer, BufferLength)
until BufferLength = 0;
Close(f);
writeln('OK') { if we get here, we succeeded }
finally
InternetCloseHandle(hURL)
end
finally
InternetCloseHandle(hSession)
end
end;
begin
if ParamCount < 2 then
begin
writeln('Usage: DrBobFTP URL Filename');
writeln('Example: DrBobFTP http://www.drbob42.com/ftp/headconv.zip hc.zip')
end
else
CopyURL(ParamStr(1), ParamStr(2))
end.
Of course, in order to execute this little program you also need the WININET.DLL, which can be found on the Microsoft website.
Enhancements?
If you read the WININET documentation, you'll see that there are also APIs for FindFile, so you can actually create an overview of the available remote files.
And based on that information, you can start to write your own web robot that can download part of a website (for example those files that are new of have changed since your last visit).
All automatically, and without GUI (but fast).
As a matter of fact, I'm working on a similar kind of tool myself, called RobotBob for now, which will soon "keep a robotic eye" on the Borland website, helping me to detect any news and interesting events regarding Borland development tools...
5. HTML Syntax Highlighting
A final tool that I use on an almost daily basis is called HTMLHIGH, and can be used to add syntax highlighting to code fragments inside <PRE>...</PRE> pairs of HTML pages.
A command-line version of this tool can be found next to this paper on the CD-ROM.
Without source code at this time, as I plan to turn it into a more user-friendly Wizard and want to write another article around it.