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.