(******************************************************************************************** ** 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 // // // //Where "SectionName" is the name of the section. // //The end of a section is marked by // // // //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_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(''); writeln(''); //Generate the HTML to display the cookies returned by the browser. writeln('

Current Cookies

'); iNumCookies := length(Cookies); //Number of cookies returned by the browser if iNumCookies=0 then writeln('

None

') 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('

', EscapeCharacters(urldecode(c.name)), '=', EscapeCharacters(urldecode(c.value)),'

'); end; writeln('
'); writeln('

', getenv('SCRIPT_NAME'), '

'); writeln('
'); //Generate the HTML for the form writeln('
'); writeln('

Name:

'); writeln('

Value:

'); writeln('

Expiries in: Days

'); writeln('

'); writeln(''); writeln('

'); writeln('
'); writeln(''); writeln(''); 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 = '<'; HTMLGreaterThan = '>'; HTMLQuote = '"'; 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.