summaryrefslogtreecommitdiff
path: root/misc/pascal/tests/src/806-cgicook.pas
blob: a7d2fa5fa8235da25d3e493ff1f92baa7d35916e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
(********************************************************************************************
** PROGRAM     : cgicook
** VERSION     : 1.0.0
** DESCRIPTION : Demonstrates how to use cookies in CGI programs.
** AUTHOR      : Stuart King
** COPYRIGHT   : Copyright (c) Irie Tools, 2002. All Rights Reserved.
** NOTES       :
**    This sample program is distributed with Irie Pascal, and was written to illustrate
** how to use cookies (i.e. how to read, write, and delete cookies). To make best use of
** this sample you should have a basic understanding of Pascal as well as a basic
** understanding of the Common Gateway Interface (CGI).
**
**    Before describing how to use cookies, here is a very brief description of what
** cookies are and what they are used for. Cookies are named pieces of information that a
** website can ask a client (usually a browser) to store on its behalf. This information
** can be sent back to the website (and any other website in the cookies domain) whenever
** the browser sends a request to the website. Cookies are usually stored on the client's
** hard drive and can persist for weeks, months, or even years. This makes cookies very
** useful for 'remembering' information about website visitors.
** IMPORTANT: The cookies sent between the client and the website are sent in text format
** and are visible to any snooper, so sensitive information should either be encrypted
** or sent only over a secure connection.
**
**    Cookies are written by sending a "Set-Cookie" response header back to the client. The
** syntax for the "Set-Cookie" header is given below:
**
** Set-Cookie: name=value; domain=.domain.com; path=/path;
**            expires=Day, dd-Mon-yyyy hh:mm:ss GMT; secure
** Where
**   "name" is the name of the cookie.
**   "value" is the information stored by the cookie and must be specified. If you want
**           to delete a cookie then specify an expiry date that has already passed.
**   "domain" restricts the domains that will receive the cookie. The client should only send
**            the cookie to matching domains. Domains are matched from right to left. So
**            "domain=.irietools.com" matches "www.irietools.com" and "info.irietools.com".
**            If "domain" is specified it must match the domain of the website setting the
**            cookie. "domain" can't specify a top-level domain like ".com" or ".edu.jm".
**            If "domain" is not specified then the domain name of the website setting
**            the cookie is used.
**   "path" restricts the URLs within a domain that will receive the cookie. Paths are
**          matched from left to right and trailing /'s are ignored. If "path" is not
**          specified then the full path of the request is used.
**   "expires" specifies when the cookie should expire. The format of the expiry info
**          must be exactly as shown above:
**          "Day" is the three letter day of the week (Mon, Tue, Wed, Thu, Fri, Sat, or Sun).
**          "dd" is the two digit day of the month (01-31).
**          "Mon" is the  three letter abbreviated month name
**             (Jan, Feb, Mar, Apr, Jun, Jul, Aug, Sep, Oct, Nov, Dec).
**          For example: "Mon, 16-Sep-2002 17:03:48 GMT".
**          If "expire" is not specified then the cookie is stored memory until the client
**          exits.
**   "secure" indicates that the cookie should only be sent over a secure connection.
**
**   The browser sends cookies back to the website in the form of Cookie headers. The
** format for Cookie headers is given below:
**
** Cookie: Cookie1; ...; CookieN
** Where
**    each Cookiue is given as: name=value
**
**    Only the "name" and the "value" of the cookies is returned, the other information
** such as the "domain" is not returned. CGI programs can't read the Cookie headers
** directly, so the webserver will make the Cookie header information available to the
** CGI program in the HTTP_COOKIE environment variable.
**********************************************************************************************)
program cookies;
const
	MAX_BUFFER = 4096;
	MAX_COOKIE_DATA = 200;
	//SCRIPT_NAME = '/irietools/cgibin/cookies.exe';
type
	positive = 0..maxint;
	BufferType = string[MAX_BUFFER];
	CookieDataType = string[MAX_COOKIE_DATA];
	DayOfWeek = 1..7;
	Days = 1..31;
	Months = 1..12;
	date = record
		day : Days;
		month : Months;
		year : integer;
		dow : DayOfWeek
	end;
	Cookie = record
		name : string;
		value : CookieDataType
	end;
	CookieList = list of Cookie;
var
	buffer : BufferType;
	strSetCookie : BufferType;
	DaysInMonth : array[Months] of Days;
	DaysElapsed : array[Months] of integer;
	DayOfWeekShortNames : array[DayOfWeek] of string[3];
	MonthShortNames : array[Months] of string[3];
	Cookies : CookieList;

	procedure DateToInt(dt : date; var iDays : integer); forward;
	procedure IntToDate(iDays : integer; var dt : date); forward;
	procedure CalculateDayOfWeek(var dt : date); forward;
	function urlencode(strIn : string) : string; forward;
	function EscapeCharacters(s : string) : string; forward;

	//PURPOSE: Performs program initialization
	procedure Initialize;
	var
		m : months;
	begin (* Initialize *)
		DaysInMonth[1] := 31;
		DaysInMonth[2] := 28;
		DaysInMonth[3] := 31;
		DaysInMonth[4] := 30;
		DaysInMonth[5] := 31;
		DaysInMonth[6] := 30;
		DaysInMonth[7] := 31;
		DaysInMonth[8] := 31;
		DaysInMonth[9] := 30;
		DaysInMonth[10] := 31;
		DaysInMonth[11] := 30;
		DaysInMonth[12] := 31;

		DaysElapsed[1] := 0;
		for m := 2 to 12 do
			DaysElapsed[m] := DaysElapsed[m-1]+DaysInMonth[m-1];

		DayOfWeekShortNames[1] := 'Sun';
		DayOfWeekShortNames[2] := 'Mon';
		DayOfWeekShortNames[3] := 'Tue';
		DayOfWeekShortNames[4] := 'Wed';
		DayOfWeekShortNames[5] := 'Thu';
		DayOfWeekShortNames[6] := 'Fri';
		DayOfWeekShortNames[7] := 'Sat';

		MonthShortNames[1] := 'Jan';
		MonthShortNames[2] := 'Feb';
		MonthShortNames[3] := 'Mar';
		MonthShortNames[4] := 'Apr';
		MonthShortNames[5] := 'May';
		MonthShortNames[6] := 'Jun';
		MonthShortNames[7] := 'Jul';
		MonthShortNames[8] := 'Aug';
		MonthShortNames[9] := 'Sep';
		MonthShortNames[10] := 'Oct';
		MonthShortNames[11] := 'Nov';
		MonthShortNames[12] := 'Dec';

		new(Cookies);	//initialize the list of cookies
	end; (* Initialize *)

	//PURPOSE: Retrieves the information passed to the CGI applications.
	//PARAMETER(s): cl - Used to store all the cookies that were passed to the CGI program
	//GLOBAL(s) - buffer - Used to store the GET or POST information passed to the CGI program
	procedure GetCGIData(var cl : CookieList);
	var
		RequestMethod : string;

		//PURPOSE: Retrieves the cookies passed to the CGI program and puts then in the list.
		//PARAMETER(s): cl - Used to store all the cookies that were passed to the CGI program
		procedure GetCookieInfo(var cl : CookieList);
		var
			data : BufferType;
			iNumCookies, iCurrCookie : positive;
			iPos, iLen : positive;
			strCurrCookie : CookieDataType;
			c : Cookie;
		begin (* GetCookieInfo *)
			//The cookies are stored in the environment variable HTTP_COOKIE as space
			// deliminated words with trailing semi-colons after each word (except the last word).
			//Each word being a single cookie.
			//Retrieve the cookies
			data := getenv('HTTP_COOKIE');

			//Retrieve the number words/cookies.
			iNumCookies := countwords(data);

			//Retrieve each cookie and use the "=" to seperate the name from the value
			//then insert the cookie into the list.
			for iCurrCookie := 1 to iNumCookies do
				begin
					strCurrCookie := copyword(data, iCurrCookie);
					iPos := pos('=', strCurrCookie);
					if iPos <> 0 then
						begin
							c.name := trim(copy(strCurrCookie, 1, iPos-1));
							c.value := trim(copy(strCurrCookie, iPos+1));
							iLen := length(c.value);
							if (iLen<>0) and (c.value[iLen]=';') then
								c.value := copy(c.value, 1, iLen-1);
							insert(c, cl);
						end;
				end;
		end; (* GetCookieInfo *)

		//PURPOSE: Retrieves information sent to by a GET request (i.e. in the QUERY_STRING
		//         environment variable).
		procedure GetRequest;
		begin (* GetRequest *)
			buffer := getenv('QUERY_STRING')
		end; (* GetRequest *)

		//PURPOSE: Retrieves information sent to by a POST request (i.e. through the standard
		//         input stream, with the length of the data in the environment variable
		//         CONTENT_LENGTH).
		procedure PostRequest;
		var
			len, i : positive;
			err : integer;
			ContentLength : string;
			c : char;
		begin (* PostRequest *)
			buffer := '';
			ContentLength := getenv('CONTENT_LENGTH');
			if ContentLength <> '' then
				val(ContentLength, len, err)
			else
				len := 0;
			if len <= MAX_BUFFER then
				for i := 1 to len do
					begin
						read(c);
						buffer := buffer + c
					end
		end; (* PostRequest *)

	begin (* GetCGIData *)
		GetCookieInfo(cl);
		RequestMethod := getenv('REQUEST_METHOD');
		if RequestMethod = 'GET' then
			GetRequest
		else
			PostRequest
	end; (* GetCGIData *)

	//PURPOSE: Process the data passed to the program.
	//NOTES: This is the main part of the program. After retreiving
	//       the information passed to the program this procedure is
	//       called to perform the required processing. This program receives to kinds
	//       of information:
	//       1. Cookies sent back by the brower. These have already been retrieved by the
	//            the procedure "GetCGIData" and do not require further processing here.
	//            The procedure 'GenerateResponse' will display the retrieved cookies.
	//       2. Information entered by the user about a cookie to add or delete. This
	//           information needs to formated and put in the global 'strSetCookie', so that
	//           it can be included in the response by the procedure 'GenerateResponse'.
	procedure ProcessCGIData;
	var
		i, num, p : integer;
		EncodedVariable, DecodedVariable, name, value : string;
		strName : string;
		strValue : string;
		strExpiry : string;
		blnDelete : boolean;

		//PURPOSE: Converts a expiry date into a string in the format required
		//         by the Set-Cookie header (i.e. Day, dd-Mon-yyyy hh:mm:ss GMT
		//ARGUMENT(s): dt - Expire date to convert.
		//RETURNS: The expiry date/time in Set-Cookie format
		//NOTES:
		//    This function just hard-codes the last second in the day "23:59:59"
		// instead of using the current time or allowing the expiry time to be specified.
		//This is done because
		// 1. It is easier and this is just a sample program
		// 2. Usually cookie will be set to expire either when the browser closes
		//    or after many days have passed, so the exact time is not important.
		function FormatCookieExpiryString(dt : date) : string;
		var
			s : string;

			//PURPOSE: Converts an integer into a zero-padded string
			//ARGUMENT(s):
			//   1. i - The integer to convert
			//   2. len - The length of the string to return
			//RETURNS:
			//   The zero padded string
			function Int2String(i : integer; len : integer) : string;
			var
				S : string;
			begin (* Int2String *)
				str(i:1, s);
				while length(s) < len do
					s := '0' + s;
				Int2String := s
			end; (* Int2String *)

		begin (* FormatCookieExpiryString *)
			CalculateDayOfWeek(dt);
			s := DayOfWeekShortNames[dt.dow]+', ';
			s := s + Int2String(dt.day, 2)+'-'+MonthShortNames[dt.month]+'-'+Int2String(dt.year, 4)+' ';
			s := s + '23:59:59 GMT';
			FormatCookieExpiryString := s;
		end; (* FormatCookieExpiryString *)

		//PURPOSE: Processes the named value pairs sent with the GET or POST request.
		//         Which in this case is the information entered by the user about the
		//         cookie to add or delete.
		//ARGUMENT(s): name - name part of the name/value pair
		//             value - value part of name/value pair
		//NOTES:
		//    The information entered by the user is sent as name/value pairs (i.e. name-value)
		//with the name part being the name of the form element holding the information and
		//the value part being the actual information held by the form element.
		procedure ProcessNameValuePair(var name, value : string);
		var
			dt : date;
			dow : integer;
			iDays, iErr : integer;
			iDate : integer;
		begin
			//If the name is 'txtname' then this is the name of the cookie.
			//The name is urlencoded in case it contains special characters
			if name='txtname' then
				strName := urlencode(value)
			//If the name is 'txtvalue' then this is the value of the cookie.
			//The value is urlencoded in case it contains special characters
			else if name='txtvalue' then
				strValue := urlencode(value)
			//If the name is 'txtdays' then this is the number of days before the cookie
			//expires. The value is converted to an integer and if the conversion is
			//successful the value is added to the current date to get the expiry date
			//and this date is formatted for use in a Set-Cookie header.
			else if name='txtdays' then
				begin
					val(value, iDays, iErr);
					if iErr=0 then
						begin
							getdate(dt.year, dt.month, dt.day, dow);//Get current date
							DateToInt(dt, iDate);	//Convert current date to number of days since Jan 1, 0001 AD
							iDate := iDate + iDays; //Add in the number of days to expire
							IntToDate(iDate, dt);	//Convert the result back into a date
							strExpiry := FormatCookieExpiryString(dt); //Format date
						end;
				end
			//If the name is 'cmddelete' then the cookie should be deleted not added.
			//The cookie is deleted by setting an expiry date that has already passed.
			else if name='cmddelete' then
				begin
					getdate(dt.year, dt.month, dt.day, dow);
					DateToInt(dt, iDate);
					iDate := iDate - 2;
					IntToDate(iDate, dt);
					strExpiry := FormatCookieExpiryString(dt);
					blnDelete := true
				end
			else
				;
		end;

	begin (* ProcessCGIData *)
		strSetCookie := '';
		strName := '';
		strValue := '';
		strExpiry := '';
		blnDelete := false;

		//Retrieve each name/value pair from the form and processes them.
		num := CountWords(buffer, '&');
		for i := 1 to num do
			begin
				EncodedVariable := CopyWord(buffer, i, '&');
				DecodedVariable := URLDecode(EncodedVariable);
				p := pos('=', DecodedVariable);
				if p > 0 then
					begin
						name := lowercase(trim(copy(DecodedVariable, 1, p-1)));
						value := trim(copy(DecodedVariable, p+1));
						ProcessNameValuePair(name, value);
					end
			end;

		//If after processing each name/value pair we have enough data for a
		//"Set-Cookie" header then generate a "Set-Cookie" header in the global
		//"strSetCookie".
		//if (strName<>'') and ((strValue<>'') or blnDelete) and (strExpiry<>'') then
		if (strName<>'') and ((strValue='') or (strExpiry<>'')) then
			begin
				//Set-Cookie: name=value; domain=.irietools.com; path=/cgibin;
				//            expires=Tue, 03-Sep-2002 20:42:19 GMT; secure
				strSetCookie := 'Set-Cookie: ' + strName + '=' + strValue + ';';
				if strExpiry<>'' then
					strSetCookie := strSetCookie + ' expires=' + strExpiry;
			end;
	end; (* ProcessCGIData *)

	//PURPOSE: Generates the response to send back to the browser.
	//NOTES:
	//   Most of the HTML generated by this procedure actually comes from a template file.
	//The HTML from the template file is common to almost all the pages on this website.
	//The HTML specific to this program (i.e. the form that accepts the cookie information
	//from the user), is generated by the local procedure 'GenerateBodyData'. So this procedure
	//basically reads the template file and sends most of it to the webserver except for
	//a small section which it repalces with the output of the procedure 'GenerateBodyData'.
	//The template file is divided into sections using HTML comments. The start of a section
	//is marked by the following
	//
	// <!--section SectionName-->
	//
	//Where "SectionName" is the name of the section.
	//
	//The end of a section is marked by
	//
	// <!--/section-->
	//
	//What this procedure actually does is read the template file and write the contents
	//of all the sections except one to the webserver. The section not written to the webserver
	//is called "bodydata". Instead of writing the contents of the section "bodydata" to the
	//webserver the ouput of the procedure "GenerateBodyData" is sent to the webserver.
	//This is done because it is much easier to work with HTML using an HTML editor rather
	//than using embedded writeln's inside a CGI program. So an HTML editor is used to
	//create and update the template file and the portion of the generated output that
	//is specific to this program is generated by "GenerateBodyData". You will notice
	//when you run this program that the page generated looks alot like the other pages
	//on the website because most of it comes from the template file.
	procedure GenerateResponse;
	const
		TEMPLATE_FILE = 'template.html';
		START_SECTION = '<!--section';
		END_SECTION = '<!--/section-->';
		SECTION_NAME = 'bodydata';
	var
		f : text;
		line : BufferType;
		blnSkipping : boolean;

		//PURPOSE: Generates the portion of the response page that is specific to this
		//         program (the rest of the response page comes from the template file).
		//NOTES:
		//    Tables are used to organise the layout of the generated pages on this website.
		//The portion of the response page generated by this procedure is a single row of
		//a table.  Unless you are an HTML expert the easiest way to understand the HTML
		//generated by this procedure is probably to run this program and view the
		//response pages generated.
		procedure GenerateBodyData;
		var
			iNumCookies, iCurrCookie : positive;
			c : Cookie;
		begin (* GenerateBodyData *)
			writeln('<tr>');
            writeln('<td width="100%">');

 			//Generate the HTML to display the cookies returned by the browser.
			writeln('<h1>Current Cookies</h1>');
			iNumCookies := length(Cookies);	//Number of cookies returned by the browser
			if iNumCookies=0 then
				writeln('<p>None</p>')
			else
				//For each cookie returned write it's name and value
				//NOTE: The name and value are escaped in case they contain
				//special characters.
				for iCurrCookie := 1 to iNumCookies do
					begin
						c := Cookies[iCurrCookie];
						writeln('<p>', EscapeCharacters(urldecode(c.name)), '=', EscapeCharacters(urldecode(c.value)),'</p>');
					end;
			writeln('<hr>');
			writeln('<p>', getenv('SCRIPT_NAME'), '</p>');
			writeln('<hr>');

			//Generate the HTML for the form
			writeln('<form method="POST" action="', getenv('SCRIPT_NAME'), '">');
			writeln('<p><big><strong>Name:</strong></big> <input type="text" name="txtName" size="25"></p>');
			writeln('<p><big><strong>Value:</strong></big> <input type="text" name="txtValue" size="56"></p>');
			writeln('<p><big><strong>Expiries in:</strong></big> <input type="text" name="txtDays" size="5"> <big><strong>Days</strong></big></p>');
			writeln('<p><input type="submit" value=" Add Cookie " name="cmdAdd">');
			writeln('<input type="submit" value=" Delete Cookie " name="cmdDelete">');
			writeln('<input type="reset" value=" Reset " name="cmdReset"></p>');
			writeln('</form>');

			writeln('</td>');
			writeln('</tr>');
		end; (* GenerateBodyData *)

	begin (* GenerateResponse *)
		//Generate the response headers (including the blank line at the end).
		writeln('content-type: text/html');
		if strSetCookie <> '' then
			writeln(strSetCookie);
		writeln;

		blnSkipping := false;
		reset(f, TEMPLATE_FILE);

		//Read each line in the template file and search for section markers
		//If a target section marker is found then call 'GenerateBodyData' to
		//generate the HTML for that section and set 'blnSkipping' to true so
		//that the contents of that section, from the template file, will be skipped.
		while not eof(f) do
			begin
				readln(f, line);
				line := trim(line);
				if pos(START_SECTION, line) > 0 then
					begin
						if pos(SECTION_NAME, line) > 0 then
						begin
							GenerateBodyData;
							blnSkipping := true;
						end
					end
				else if line=END_SECTION then
					blnSkipping := false
				else if not blnSkipping then
					writeln(line);
			end;
		close(f);
	end; (* GenerateResponse *)

	procedure Shutdown;
	begin (* Shutdown *)
		dispose(Cookies);
	end; (* Shutdown *)

	//PUPOSE: Determines if a year was a leap year.
	function IsLeapYear(year : integer) : boolean;
	begin (* IsLeapYear *)
		IsLeapYear :=
			((year mod 4)=0)
				and
			(
				((year mod 100)<>0)
					or
				((year mod 400)=0)
			);
	end; (* IsLeapYear *)

	//PURPOSE: Converts a date into an integer. The integer represents the
	//         number of days since 'Jan 1, 0001 AD'. Negative values present
	//         the number of days before 'Jan 1, 0001 AD' (i.e. BC dates).
	//ARGUMENT(s): dt - The date to convert to an integer
	//             iDays - The converted date as an integer.
	procedure DateToInt;
	var
		i, year : integer;

		//PURPOSE: Used to help adjust for the leap years when calulating the number
		//         of days that have already elapsed since the year began.
		//RETURNS: +1 if the date is a leap year and the month of february
		//            has already passed.
		// or
		//          0 otherwise
		function LeapYearAdjustment(dt : date) : integer;
		var
			iAdjustment : integer;

		begin (* LeapYearAdjustment *)
			iAdjustment := 0;

			if (IsLeapYear(dt.year)) and (dt.month >= 3) then
				iAdjustment := 1;

			LeapYearAdjustment := iAdjustment;
		end; (* LeapYearAdjustment *)

	begin (* DateToInt *)
		if dt.year < 0 then
			year := dt.year+1	//Adjust for the fact that there was no year 0.
		else
			year := dt.year;
		//Calculate days since Jan 1, 0001 AD ignoring leap years.
		i := (year-1)*365 + DaysElapsed[dt.month] + (dt.day-1);

		//Adjust for leap years except the final year
		i := i + ((year-1) div 4) - ((year-1) div 100) + ((year-1) div 400);

		//Add 1 if the final year is a leap year and february has passed.
		iDays := i + LeapYearAdjustment(dt);
	end; (* DateToInt *)

	//PURPOSE: Coverts an integer, representing the number of days since Jan 1, 0001 AD
	//         to a date.
	//ARGUMENT(s): iDays - The integer to convert to a date.
	//             dt - The converted date.
	//NOTE: There are 365.2425 days in the year adjusting for leap years.
	procedure IntToDate;
	var
		m : Months;
		d : integer;
		iError, iTemp : integer;
	begin
		//First calculate an approximation of the correct answer
		if iDays >= 0 then
			begin
				dt.year := trunc(idays / 365.2425)+1;
				iTemp := iDays - trunc((dt.year-1)*365.2425);
			end
		else
			begin
				dt.year := -(trunc(abs(idays) / 365.2425)+1);
				iTemp := abs(abs(iDays) - trunc((abs(dt.year)-1)*365.2425));
			end;

		dt.month := 12;
		for m := 1 to 12 do
			if iTemp >= DaysElapsed[m] then
				dt.month := m;

		iTemp := iTemp - DaysElapsed[dt.month];

		d := iTemp+1;
		if d > DaysInMonth[dt.month] then
			dt.day := DaysInMonth[dt.month]
		else
			dt.day := d;

		//Now to keep adjusting our approximation until it is exact
		DateToInt(dt, iTemp);
		while iTemp<>iDays do
			begin
				//Number of days that need to be added to the approximation to make it correct.
				iError := iDays-iTemp;

				//If adding the days would make the day of the month negative then
				// then move the approximation to the beginning of the previous month.
				// Adjusting the year if necessary (i.e. it is the first month of the year).
				if (dt.day + iError) < 1 then
					begin
						if dt.month=1 then
							begin
								dt.month := 12;
								dt.year := dt.year - 1;
								if dt.year = 0 then
									dt.year := -1;
							end
						else
							dt.month := dt.month - 1;
						dt.day := 1;
					end
				//If adding the days might take the date into the next month then
				// check if adding the days actually takes the date to Feb 29 on a leap year
				//   if so then adjust the date to Feb 29
				//otherwise move the date to the beginning of the next month.
				else if (dt.day + iError) > DaysInMonth[dt.month] then
					begin
						if (dt.month=2) and (IsLeapYear(dt.year)) and (dt.day+iError=DaysInMonth[dt.month]+1) then
							dt.day := DaysInMonth[dt.month]+1
						else
							begin
								if dt.month=12 then
									begin
										dt.month := 1;
										dt.year := dt.year + 1;
										if dt.year = 0 then
											dt.year := 1;
									end
								else
									dt.month := dt.month + 1;
								dt.day := 1
							end
					end
				//else adding the days results in a valid day of the month then
				// just add the days.
				else
					dt.day := dt.day + iError;

				DateToInt(dt, iTemp);
			end;
	end;

	//PURPOSE: Calculates the day of the week of a given date
	//ARGUMENT(s): dt - The date to calculate the day of the week of.
	//NOTES:
	//   First the date is converted into the number of days since (Jan 1, 0001 AD)
	//Next this value is adjusted with the day of the week of Jan 1, 0001 AD.
	//Next mod 7 is used to give a value in the range 0..6 that repeats every
	//seven days (like the days of the week).
	//Finally the value is incremented by 1 to give a value in the range 1..7
	//
	//The most interesting part of this algorithm is probably the adjustment for the
	//first day of the week. Suppose this adjustment was not made then of the date
	//was Jan 1, 0001 AD then 'DateToInt' would calculate the number of days as 0.
	//Then (0 mod 7) + 1 would give the value 1 or Sunday. But Jan 1, 0001 AD was a
	//Monday, so we need to add a 1 to adjust for this fact. How do I know that
	//Jan 1, 0001 AD was a Monday? I don't, but I do know that day of the week of
	//today. So what I did was calculate the day of the week for today without the
	//adjustment and then added an adjustment that resulted in the correct value.
	procedure CalculateDayOfWeek;
	const
		ADJUST_FOR_FIRST_DAY = 1;	//Apparently Jan 1, 0001 AD was a Monday
	var
		iDays : integer;
	begin (* CalculateDayOfWeek *)
		DateToInt(dt, iDays);
		dt.dow := ((iDays+ADJUST_FOR_FIRST_DAY) mod 7) + 1;
	end; (* CalculateDayOfWeek *)

	//PURPOSE: Returns the URL encoded version of a string.
	//ARGUMENT(s): strIn - The string to be encoded.
	//NOTES:
	//   Spaces are converted to '-' and non-alphanumeric characters are converted
	//to %NN, where NN is the hexidecimal value of the chacters ordinal value. This
	//is the reverse of the built-in function 'urldecode'.
	function urlencode;
	var
		strOut : string;
		i : positive;
		c : char;
		sTemp : string[3];
	begin (* urlencode *)
		strOut := '';
		for i := 1 to length(strIn) do
			begin
				c := strIn[i];
				if c = ' ' then
					strOut := strOut + '+'
				else if isalphanum(c) then
					strOut := strOut + c
				else
					begin
						sTemp := hex(ord(c));
						if length(sTemp)=1 then
							sTemp := '0' + sTemp;
						strOut := strOut + '%' + sTemp
					end;
			end;
		urlencode := strOut;
	end; (* urlencode *)

	//*************************************************************************
	//PURPOSE: This function converts certain characters that have a
	//         special meaning in HTML documents to their HTML representation.
	//ARGUMENT(s): s - The string to be escaped.
	//RETURNS: The string with all special characters escaped.
	//NOTES: The characters converted are < > "
	function EscapeCharacters;
	const
		LessThanChar = '<';
		GreaterThanChar = '>';
		QuoteChar = '"';
		HTMLLessThan = '&lt;';
		HTMLGreaterThan = '&gt;';
		HTMLQuote = '&quot;';
	var
		i : positive;

		procedure ReplaceChar(var strBuffer : string; strReplace : string; i : positive);
		begin
			delete(strBuffer, i, 1);
			insert(strReplace, strBuffer, i)
		end;

	begin (* EscapeCharacters *)
		repeat
			i := pos(LessThanChar, s, 1);
			if i > 0 then
				ReplaceChar(s, HTMLLessThan, i)
		until i = 0;

		repeat
			i := pos(GreaterThanChar, s, 1);
			if i > 0 then
				ReplaceChar(s, HTMLGreaterThan, i)
		until i = 0;

		repeat
			i := pos(QuoteChar, s, 1);
			if i > 0 then
				ReplaceChar(s, HTMLQuote, i)
		until i = 0;

		EscapeCharacters := s;
	end; (* EscapeCharacters *)

begin
	Initialize;

	GetCGIData(Cookies);

	ProcessCGIData;

	GenerateResponse;

	Shutdown;
end.