header fiddling.
[catagits/fcgi2.git] / examples / SampleStore / sample-store.c
1 /*
2  * sample-store.c --
3  *
4  *      FastCGI example program using fcgi_stdio library
5  *
6  *
7  * Copyright (c) 1996 Open Market, Inc.
8  *
9  * See the file "LICENSE.TERMS" for information on usage and redistribution
10  * of this file, and for a DISCLAIMER OF ALL WARRANTIES.
11  *
12  *
13  * sample-store is a program designed to illustrate one technique
14  * for writing a high-performance FastCGI application that maintains
15  * permanent state.  It is real enough to demonstrate a range of issues
16  * that can arise in FastCGI application programming.
17  *
18  * sample-store implements per-user shopping carts.  These carts are kept
19  * in memory for speed but are backed up on disk for reliability; the
20  * program can restart at any time, affecting at most one request.  Unlike
21  * CGI applications, the performance impact of sample-store's disk
22  * use is negligible: no I/O for query requests, no reads and one write
23  * for a typical update request.
24  *
25  * sample-store's on-disk representation is extremely simple.  The
26  * current state of all shopping carts managed by a process is kept
27  * in two files, a snapshot and a log.  Both files have the same format,
28  * a sequence of ASCII records.  On restart the current state is restored
29  * by replaying the snapshot and the log.  When the log grows to a certain
30  * length, sample-store writes a new snapshot and empties the log.
31  * This prevents the time needed for restart from growing without
32  * bound.
33  *
34  * Since users "visit" Web sites, but never "leave", sample-store
35  * deletes a shopping cart after the cart has been inactive
36  * for a certain period of time.  This policy prevents sample-store's
37  * memory requirements from growing without bound.
38  *
39  * sample-store operates both as a FastCGI Responder and as an
40  * Authorizer, showing how one program can play two roles.
41  *
42  * The techniques used in sample-store are not specific to shopping
43  * carts; they apply equally well to maintaining all sorts of
44  * information.
45  *
46  */
47
48 #ifndef lint
49 static const char rcsid[] = "$Id: sample-store.c,v 1.2 2001/06/19 17:49:26 robs Exp $";
50 #endif /* not lint */
51
52 #include <assert.h>      /* assert */
53 #include <dirent.h>      /* readdir, closedir, DIR, dirent */
54 #include <errno.h>       /* errno, ENOENT */
55 #include <stdlib.h>      /* malloc/free, getenv, strtol */
56 #include <string.h>      /* strcmp, strncmp, strlen, strstr, strchr */
57 #include <time.h>        /* time, time_t */
58
59 #include "fcgi_config.h"
60
61 #ifdef HAVE_UNISTD_H
62 #include <unistd.h>      /* fsync */
63 #endif
64
65 #include "tcl.h"         /* Tcl_*Hash* functions */
66
67 #if defined __linux__
68 int fsync(int fd);
69 #endif
70
71 #include "fcgi_stdio.h"  /* FCGI_Accept, FCGI_Finish, stdio */
72
73 /*
74  * sample-store is designed to be configured as follows (for the OM server):
75  *
76  * SI_Department SampleStoreDept -EnableAnonymousTicketing 1
77  * Region /SampleStore/ *  { SI_RequireSI SampleStoreDept 1 }
78  *
79  * Filemap /SampleStore $fcgi-devel-kit/examples/SampleStore
80  * AppClass  SampleStoreAppClass \
81  *     $fcgi-devel-kit/examples/sample-store \
82  *     -initial-env STATE_DIR=$fcgi-devel-kit/examples/SampleStore.state \
83  *     -initial-env CKP_THRESHOLD=100 \
84  *     -initial-env CART_HOLD_MINUTES=240 \
85  *     -processes 2 -affinity
86  * Responder SampleStoreAppClass /SampleStore/App
87  * AuthorizeRegion /SampleStore/Protected/ *  SampleStoreAppClass
88  *
89  * sample-store looks for three initial environment variables:
90  *
91  *  STATE_DIR
92  *    When sample-store is run as a single process without affinity
93  *    this is the directory containing the permanent state of the
94  *    process.  When sample-store is run as multiple processes
95  *    using session affinity, the state directory is
96  *    $STATE_DIR.$FCGI_PROCESS_ID, e.g. SampleStore.state.0
97  *    and SampleStore.state.1 in the config above.  The process
98  *    state directory must exist, but may be empty.
99  *
100  *  CKP_THRESHOLD
101  *    When the log grows to contain this many records the process
102  *    writes a new snapshot and truncates the log.  Defaults
103  *    to CKP_THRESHOLD_DEFAULT.
104  *
105  *  CART_HOLD_MINUTES
106  *    When a cart has not been accessed for this many minutes it
107  *    may be deleted.  Defaults to CART_HOLD_MINUTES_DEFAULT.
108  *
109  * The program is prepared to run as multiple processes using
110  * session affinity (illustrated in config above) or as a single process.
111  *
112  * The program does not depend upon the specific URL prefix /SampleStore.
113  *
114  */
115 \f
116 /*
117  * This code is organized top-down, trying to put the most interesting
118  * parts first.  Unfortunately, organizing the program in this way requires
119  * lots of extra declarations to take care of forward references.
120  *
121  * Utility functions for string/list processing and such
122  * are left to the very end.  The program uses the Tcl hash table
123  * package because it is both adequate and readily available.
124  */
125
126 #ifndef FALSE
127 #define FALSE (0)
128 #endif
129
130 #ifndef TRUE
131 #define TRUE  (1)
132 #endif
133
134 #ifndef max
135 #define max(a,b) ((a) > (b) ? (a) : (b))
136 #endif
137
138 #define Strlen(str) (((str) == NULL) ? 0 : strlen(str))
139
140 static void *Malloc(size_t size);
141 static void Free(void *ptr);
142 static char *StringNCopy(char *str, int strLen);
143 static char *StringCopy(char *str);
144 static char *StringCat(char *str1, char *str2);
145 static char *StringCat4(char *str1, char *str2, char *str3, char *str4);
146 static char *QueryLookup(char *query, char *name);
147 static char *PathTail(char *path);
148
149 typedef struct ListOfString {
150     char *head;
151     struct ListOfString *tail;
152 } ListOfString;
153 static char *ListOfString_Head(ListOfString *list);
154 static ListOfString *ListOfString_Tail(ListOfString *list);
155 static int ListOfString_Length(ListOfString *list);
156 static int ListOfString_IsElement(ListOfString *list, char *element);
157 static ListOfString *ListOfString_AppendElement(
158         ListOfString *list, char *element);
159 static ListOfString *ListOfString_RemoveElement(
160         ListOfString *list, char *element);
161
162 static int IntGetEnv(char *varName, int defaultValue);
163 \f
164 static void Initialize(void);
165 static void PerformRequest(void);
166 static void GarbageCollectStep(void);
167 static void ConditionalCheckpoint(void);
168
169 /*
170  * A typical FastCGI main program: Initialize, then loop
171  * calling FCGI_Accept and performing the accepted request.
172  * Do cleanup operations incrementally between requests.
173  */
174 int main(void)
175 {
176     Initialize();
177
178     while (FCGI_Accept() >= 0) {
179         PerformRequest();
180         FCGI_Finish();
181         GarbageCollectStep();
182         ConditionalCheckpoint();
183     }
184
185     return 0;
186 }
187 \f
188 /*
189  * All the global variables
190  */
191 typedef struct CartObj {
192     int inactive;                   /* This cart not accessed since mark      */
193     ListOfString *items;            /* Items in cart                          */
194 } CartObj;
195 static Tcl_HashTable *cartTablePtr; /* Table of CartObj, indexed by userId    */
196 static Tcl_HashTable cartTable;
197 static char *fcgiProcessId;         /* Id of this process in affinity group   */
198 static char *stateDir;              /* Path to dir with snapshot and log      */
199 char *snapshotPath, *logPath;       /* Paths to current snapshot and log      */
200 static int generation;              /* Number embedded in paths, inc on ckp   */
201 static FILE *logFile = NULL;        /* Open for append to current log file    */
202 static int numLogRecords;           /* Number of records in current log file  */
203 static int checkpointThreshold;     /* Do ckp when numLogRecords exceeds this */
204 static int purge = TRUE;            /* Cart collector is removing inactives   */
205 static time_t timeCartsMarked;      /* Time all carts marked inactive         */
206 static int cartHoldSeconds;         /* Begin purge when this interval elapsed */
207 \f
208 #define STATE_DIR_VAR             "STATE_DIR"
209 #define PID_VAR                   "FCGI_PROCESS_ID"
210 #define CKP_THRESHOLD_VAR         "CKP_THRESHOLD"
211 #define CKP_THRESHOLD_DEFAULT     200
212 #define CART_HOLD_MINUTES_VAR     "CART_HOLD_MINUTES"
213 #define CART_HOLD_MINUTES_DEFAULT 300
214
215 #define SNP_PREFIX    "snapshot"
216 #define LOG_PREFIX    "log"
217 #define TMP_SNP_NAME  "tmp-snapshot"
218
219 #define LR_ADD_ITEM    "Add"
220 #define LR_REMOVE_ITEM "Rem"
221 #define LR_EMPTY_CART  "Emp"
222
223
224 static char *MakePath(char *dir, char *prefix, int gen);
225 static void AnalyzeStateDir(
226     char *dir, char *prefix, int *largestP, ListOfString **fileListP);
227 static int RecoverFile(char *pathname);
228 static void Checkpoint(void);
229
230 /*
231  * Initialize the process by reading environment variables and files
232  */
233 static void Initialize(void)
234 {
235     ListOfString *fileList;
236     int stateDirLen;
237     /*
238      * Process miscellaneous parameters from the initial environment.
239      */
240     checkpointThreshold =
241             IntGetEnv(CKP_THRESHOLD_VAR, CKP_THRESHOLD_DEFAULT);
242     cartHoldSeconds =
243             IntGetEnv(CART_HOLD_MINUTES_VAR, CART_HOLD_MINUTES_DEFAULT)*60;
244     /*
245      * Create an empty in-memory shopping cart data structure.
246      */
247     cartTablePtr = &cartTable;
248     Tcl_InitHashTable(cartTablePtr, TCL_STRING_KEYS);
249     /*
250      * Compute the state directory name from the initial environment
251      * variables.
252      */
253     stateDir = getenv(STATE_DIR_VAR);
254     stateDirLen = Strlen(stateDir);
255     assert(stateDirLen > 0);
256     if(stateDir[stateDirLen - 1] == '/') {
257         stateDir[stateDirLen - 1] = '\000';
258     }
259     fcgiProcessId = getenv(PID_VAR);
260     if(fcgiProcessId != NULL) {
261         stateDir = StringCat4(stateDir, ".", fcgiProcessId, "/");
262     } else {
263         stateDir = StringCat(stateDir, "/");
264     }
265     /*
266      * Read the state directory to determine the current
267      * generation number and a list of files that may
268      * need to be deleted (perhaps left over from an earlier
269      * system crash).  Recover the current generation
270      * snapshot and log (either or both may be missing),
271      * populating the in-memory shopping cart data structure.
272      * Take a checkpoint, making the current log empty.
273      */
274     AnalyzeStateDir(stateDir, SNP_PREFIX, &generation, &fileList);
275     snapshotPath = MakePath(stateDir, SNP_PREFIX, generation);
276     RecoverFile(snapshotPath);
277     logPath = MakePath(stateDir, LOG_PREFIX, generation);
278     numLogRecords = RecoverFile(logPath);
279     Checkpoint();
280     /*
281      * Clean up stateDir without removing the current snapshot and log.
282      */
283     while(fileList != NULL) {
284         char *cur = ListOfString_Head(fileList);
285         if(strcmp(snapshotPath, cur) && strcmp(logPath, cur)) {
286             remove(cur);
287         }
288         fileList = ListOfString_RemoveElement(fileList, cur);
289     }
290 }
291
292 static char *MakePath(char *dir, char *prefix, int gen)
293 {
294     char nameBuffer[24];
295     sprintf(nameBuffer, "%s.%d", prefix, gen);
296     return  StringCat(dir, nameBuffer);
297 }
298 \f
299 static void ConditionalCheckpoint(void)
300 {
301     if(numLogRecords >= checkpointThreshold) {
302         Checkpoint();
303     }
304 }
305 static void WriteSnapshot(char *snpPath);
306
307 static void Checkpoint(void)
308 {
309     char *tempSnapshotPath, *newLogPath, *newSnapshotPath;
310     /*
311      * Close the current log file.
312      */
313     if(logFile != NULL) {
314         fclose(logFile);
315     }
316     /*
317      * Create a new snapshot with a temporary name.
318      */
319     tempSnapshotPath = StringCat(stateDir, TMP_SNP_NAME);
320     WriteSnapshot(tempSnapshotPath);
321     ++generation;
322     /*
323      * Ensure that the new log file doesn't already exist by removing it.
324      */
325     newLogPath = MakePath(stateDir, LOG_PREFIX, generation);
326     remove(newLogPath);
327     /*
328      * Commit by renaming the snapshot.  The rename atomically
329      * makes the old snapshot and log obsolete.
330      */
331     newSnapshotPath = MakePath(stateDir, SNP_PREFIX, generation);
332     rename(tempSnapshotPath, newSnapshotPath);
333     /*
334      * Clean up the old snapshot and log.
335      */
336     Free(tempSnapshotPath);
337     remove(snapshotPath);
338     Free(snapshotPath);
339     snapshotPath = newSnapshotPath;
340     remove(logPath);
341     Free(logPath);
342     logPath = newLogPath;
343     /*
344      * Open the new, empty log.
345      */
346     logFile = fopen(logPath, "a");
347     numLogRecords = 0;
348 }
349 \f
350 /*
351  * Return *largestP     = the largest int N such that the name prefix.N
352  *                        is in the directory dir.  0 if no such name
353  *        *fileListP    = list of all files in the directory dir,
354  *                        excluding '.' and '..'
355  */
356 static void AnalyzeStateDir(
357     char *dir, char *prefix, int *largestP, ListOfString **fileListP)
358 {
359     DIR *dp;
360     struct dirent *dirp;
361     int prefixLen = strlen(prefix);
362     int largest = 0;
363     int cur;
364     char *curName;
365     ListOfString *fileList = NULL;
366     dp = opendir(dir);
367     assert(dp != NULL);
368     while((dirp = readdir(dp)) != NULL) {
369         if(!strcmp(dirp->d_name, ".") || !strcmp(dirp->d_name, "..")) {
370             continue;
371         }
372         curName = StringCat(dir, dirp->d_name);
373         fileList = ListOfString_AppendElement(fileList, curName);
374         if(!strncmp(dirp->d_name, prefix, prefixLen)
375                 && (dirp->d_name)[prefixLen] == '.') {
376             cur = strtol(dirp->d_name + prefixLen + 1, NULL, 10);
377             if(cur > largest) {
378                 largest = cur;
379             }
380         }
381     }
382     assert(closedir(dp) >= 0);
383     *largestP = largest;
384     *fileListP = fileList;
385 }
386 \f
387 static int DoAddItemToCart(char *userId, char *item, int writeLog);
388 static int DoRemoveItemFromCart(char *userId, char *item, int writeLog);
389 static int DoEmptyCart(char *userId, int writeLog);
390
391 /*
392  * Read either a snapshot or a log and perform the specified
393  * actions on the in-memory representation.
394  */
395 static int RecoverFile(char *pathname)
396 {
397     int numRecords;
398     FILE *recoveryFile = fopen(pathname, "r");
399     if(recoveryFile == NULL) {
400         assert(errno == ENOENT);
401         return 0;
402     }
403     for(numRecords = 0; ; numRecords++) {
404         char buff[128];
405         char op[32], userId[32], item[64];
406         int count;
407         char *status = fgets(buff, sizeof(buff), recoveryFile);
408         if(status == NULL) {
409             assert(feof(recoveryFile));
410             fclose(recoveryFile);
411             return numRecords;
412         }
413         count = sscanf(buff, "%31s %31s %63s", op, userId, item);
414         assert(count == 3);
415         if(!strcmp(op, LR_ADD_ITEM)) {
416             assert(DoAddItemToCart(userId, item, FALSE) >= 0);
417         } else if(!strcmp(op, LR_REMOVE_ITEM)) {
418             assert(DoRemoveItemFromCart(userId, item, FALSE) >= 0);
419         } else if(!strcmp(op, LR_EMPTY_CART)) {
420             assert(DoEmptyCart(userId, FALSE) >= 0);
421         } else {
422             assert(FALSE);
423         }
424     }
425 }
426 \f
427 static void WriteLog(char *command, char *userId, char *item, int force);
428
429 /*
430  * Read the in-memory representation and write a snapshot file
431  * that captures it.
432  */
433 static void WriteSnapshot(char *snpPath)
434 {
435     Tcl_HashSearch search;
436     Tcl_HashEntry *cartEntry;
437     ListOfString *items;
438     char *userId;
439     logFile = fopen(snpPath, "w");
440     assert(logFile != NULL);
441     cartEntry = Tcl_FirstHashEntry(cartTablePtr, &search);
442     for(cartEntry = Tcl_FirstHashEntry(cartTablePtr, &search);
443             cartEntry != NULL; cartEntry = Tcl_NextHashEntry(&search)) {
444         userId = Tcl_GetHashKey(cartTablePtr, cartEntry);
445         for(items = ((CartObj *) Tcl_GetHashValue(cartEntry))->items;
446                 items != NULL; items = ListOfString_Tail(items)) {
447             WriteLog(LR_ADD_ITEM, userId, ListOfString_Head(items), FALSE);
448         }
449     }
450     fflush(logFile);
451     fsync(fileno(logFile));
452     fclose(logFile);
453 }
454 \f
455 static void WriteLog(char *command, char *userId, char *item, int force)
456 {
457     fprintf(logFile, "%s %s %s\n", command, userId, item);
458     ++numLogRecords;
459     if(force) {
460         fflush(logFile);
461         fsync(fileno(logFile));
462     }
463 }
464 \f
465 static int RemoveOneInactiveCart(void);
466 static void MarkAllCartsInactive(void);
467
468 /*
469  * Incremental garbage collection of inactive shopping carts:
470  *
471  * Each user access to a shopping cart clears its "inactive" bit via a
472  * call to MarkThisCartActive.  When restart creates a cart it
473  * also marks the cart active.
474  *
475  * If purge == TRUE, each call to GarbageCollectStep scans for and removes
476  * the first inactive cart found.  If there are no inactive carts,
477  * GarbageCollectStep marks *all* carts inactive, records the time in
478  * timeCartsMarked, and sets purge = FALSE.
479  *
480  * If purge == FALSE, each call to GarbageCollectStep checks the
481  * elapsed time since timeCartsMarked.  If the elapsed time
482  * exceeds a threshold, GarbageCollectStep sets purge = TRUE.
483  */
484
485 static void GarbageCollectStep(void)
486 {
487     if(purge) {
488         if(!RemoveOneInactiveCart()) {
489             MarkAllCartsInactive();
490             timeCartsMarked = time(NULL);
491             purge = FALSE;
492         }
493     } else {
494         int diff = time(NULL)-timeCartsMarked;
495         if(diff > cartHoldSeconds) {
496             purge = TRUE;
497         }
498     }
499 }
500 \f
501 static int RemoveOneInactiveCart(void)
502 {
503     Tcl_HashSearch search;
504     Tcl_HashEntry *cartEntry;
505     CartObj *cart;
506     char *userId;
507     cartEntry = Tcl_FirstHashEntry(cartTablePtr, &search);
508     for(cartEntry = Tcl_FirstHashEntry(cartTablePtr, &search);
509             cartEntry != NULL; cartEntry = Tcl_NextHashEntry(&search)) {
510         cart = (CartObj *)Tcl_GetHashValue(cartEntry);
511         if(cart->inactive) {
512             userId = Tcl_GetHashKey(cartTablePtr, cartEntry);
513             DoEmptyCart(userId, TRUE);
514             return TRUE;
515         }
516     }
517     return FALSE;
518 }
519
520 static Tcl_HashEntry *GetCartEntry(char *userId);
521
522 static void MarkAllCartsInactive(void)
523 {
524     Tcl_HashSearch search;
525     Tcl_HashEntry *cartEntry;
526     CartObj *cart;
527     cartEntry = Tcl_FirstHashEntry(cartTablePtr, &search);
528     for(cartEntry = Tcl_FirstHashEntry(cartTablePtr, &search);
529             cartEntry != NULL; cartEntry = Tcl_NextHashEntry(&search)) {
530         cart = (CartObj *)Tcl_GetHashValue(cartEntry);
531         cart->inactive = TRUE;
532     }
533 }
534
535 static void MarkThisCartActive(char *userId)
536 {
537     Tcl_HashEntry *cartEntry = GetCartEntry(userId);
538     CartObj *cart = (CartObj *)Tcl_GetHashValue(cartEntry);
539     cart->inactive = FALSE;
540 }
541 \f
542 #define OP_DISPLAY_STORE "DisplayStore"
543 #define OP_ADD_ITEM      "AddItemToCart"
544 #define OP_DISPLAY_CART  "DisplayCart"
545 #define OP_REMOVE_ITEM   "RemoveItemFromCart"
546 #define OP_PURCHASE      "Purchase"
547
548 static void DisplayStore(
549         char *scriptName, char *parent, char *userId, char *processId);
550 static void AddItemToCart(
551         char *scriptName, char *parent, char *userId, char *processId,
552         char *item);
553 static void DisplayCart(
554         char *scriptName, char *parent, char *userId, char *processId);
555 static void RemoveItemFromCart(
556         char *scriptName, char *parent, char *userId, char *processId,
557         char *item);
558 static void Purchase(
559         char *scriptName, char *parent, char *userId, char *processId);
560 static void InvalidRequest(char *code, char *message);
561 static void Authorize(char *userId);
562
563 /*
564  * As a Responder, this application expects to be called with the
565  * GET method and a URL of the form
566  *
567  *     http://<host-port>/<script-name>?op=<op>&item=<item>
568  *
569  * The application expects the SI_UID variable to provide
570  * a user ID, either authenticated or anonymous.
571  *
572  * The application expects the directory *containing* <script-name>
573  * to contain various static HTML files related to the application.
574  *
575  * As an Authorizer, the application expects to be called with
576  * SID_UID and URL_PATH set.
577  */
578
579 static void PerformRequest(void)
580 {
581     char *method = getenv("REQUEST_METHOD");
582     char *role = getenv("FCGI_ROLE");
583     char *scriptName = PathTail(getenv("SCRIPT_NAME"));
584     char *parent = "";
585     char *op = QueryLookup(getenv("QUERY_STRING"), "op");
586     char *item = QueryLookup(getenv("QUERY_STRING"), "item");
587     char *userId =  getenv("SI_UID");
588     if(userId == NULL) {
589         InvalidRequest("405", "Incorrect configuration, no user id");
590         goto done;
591     } else {
592         MarkThisCartActive(userId);
593     }
594     if(!strcmp(role, "RESPONDER")) {
595         if(strcmp(method, "GET")) {
596             InvalidRequest("405", "Only GET Method Allowed");
597         } else if(op == NULL || !strcmp(op, OP_DISPLAY_STORE)) {
598             DisplayStore(scriptName, parent, userId, fcgiProcessId);
599         } else if(!strcmp(op, OP_ADD_ITEM)) {
600             AddItemToCart(scriptName, parent, userId, fcgiProcessId, item);
601         } else if(!strcmp(op, OP_DISPLAY_CART)) {
602             DisplayCart(scriptName, parent, userId, fcgiProcessId);
603         } else if(!strcmp(op, OP_REMOVE_ITEM)) {
604             RemoveItemFromCart(scriptName, parent, userId, fcgiProcessId, item);
605         } else if(!strcmp(op, OP_PURCHASE)) {
606             Purchase(scriptName, parent, userId, fcgiProcessId);
607         } else {
608             InvalidRequest("404", "Invalid 'op' argument");
609         }
610     } else if(!strcmp(role, "AUTHORIZER")) {
611         Authorize(userId);
612     } else {
613         InvalidRequest("404", "Invalid FastCGI Role");
614     }
615   done:
616     Free(scriptName);
617     Free(op);
618     Free(item);
619 }
620 \f
621 /*
622  * Tiny database of shop inventory.  The first form is the
623  * item identifier used in a request, the second form is used
624  * for HTML display.  REQUIRED_ITEM is the item required
625  * the the Authorizer.  SPECIAL_ITEM is the item on the protected
626  * page (must follow unprotected items in table).
627  */
628
629 char *ItemNames[] = {
630         "BrooklynBridge",
631         "RMSTitanic",
632         "CometKohoutec",
633         "YellowSubmarine",
634         NULL
635         };
636 char *ItemDisplayNames[] = {
637         "<i>Brooklyn Bridge</i>",
638         "<i>RMS Titanic</i>",
639         "<i>Comet Kohoutec</i>",
640         "<i>Yellow Submarine</i>",
641         NULL
642         };
643 #define REQUIRED_ITEM 1
644 #define SPECIAL_ITEM 3
645
646
647 static char *ItemDisplayName(char *item)
648 {
649     int i;
650     if(item == NULL) {
651         return NULL;
652     }
653     for(i = 0; ItemNames[i] != NULL; i++) {
654         if(!strcmp(item, ItemNames[i])) {
655             return ItemDisplayNames[i];
656         }
657     }
658     return NULL;
659 }
660 \f
661 static void DisplayNumberOfItems(int numberOfItems, char *processId);
662
663 static void DisplayHead(char *title, char *parent, char *gif)
664 {
665     printf("Content-type: text/html\r\n"
666            "\r\n"
667            "<html>\n<head>\n<title>%s</title>\n</head>\n\n"
668            "<body bgcolor=\"ffffff\" text=\"000000\" link=\"39848c\"\n"
669            "      vlink=\"808080\" alink=\"000000\">\n", title);
670     if(parent != NULL && gif != NULL) {
671         printf("<center>\n<img src=\"%s%s\" alt=\"[%s]\">\n</center>\n\n",
672                parent, gif, title);
673     } else {
674         printf("<h2>%s</h2>\n<hr>\n\n", title);
675     }
676 }
677
678 static void DisplayFoot(void)
679 {
680     printf("<hr>\n</body>\n</html>\n");
681 }
682
683 static void DisplayStore(
684         char *scriptName, char *parent, char *userId, char *processId)
685 {
686     Tcl_HashEntry *cartEntry = GetCartEntry(userId);
687     ListOfString *items = ((CartObj *) Tcl_GetHashValue(cartEntry))->items;
688     int numberOfItems = ListOfString_Length(items);
689     int i;
690
691     DisplayHead("FastCGI Shop!", parent, "Images/main-hd.gif");
692     DisplayNumberOfItems(numberOfItems, processId);
693     printf("<h3>Goods for sale:</h3>\n<ul>\n");
694     for(i = 0; i < SPECIAL_ITEM; i++) {
695         printf("  <li>Add the <a href=\"%s?op=AddItemToCart&item=%s\">%s</a>\n"
696                "      to your shopping cart.\n",
697                scriptName, ItemNames[i], ItemDisplayNames[i]);
698     }
699     printf("</ul><p>\n\n");
700     printf("If the %s is in your shopping cart,\n"
701            "<a href=\"%sProtected/%s.html\">go see a special offer</a>\n"
702            "available only to %s purchasers.<p>\n\n",
703            ItemDisplayNames[REQUIRED_ITEM], parent,
704            ItemNames[REQUIRED_ITEM], ItemDisplayNames[REQUIRED_ITEM]);
705     printf("<a href=\"%sUnprotected/Purchase.html\">Purchase\n"
706            "the contents of your shopping cart.</a><p>\n\n", parent);
707     printf("<a href=\"%s?op=DisplayCart\">View the contents\n"
708            "of your shopping cart.</a><p>\n\n", scriptName);
709     DisplayFoot();
710 }
711 \f
712 static Tcl_HashEntry *GetCartEntry(char *userId)
713 {
714     Tcl_HashEntry *cartEntry = Tcl_FindHashEntry(cartTablePtr, userId);
715     int newCartEntry;
716     if(cartEntry == NULL) {
717         CartObj *cart = (CartObj *)Malloc(sizeof(CartObj));
718         cart->inactive = FALSE;
719         cart->items = NULL;
720         cartEntry = Tcl_CreateHashEntry(cartTablePtr, userId, &newCartEntry);
721         assert(newCartEntry);
722         Tcl_SetHashValue(cartEntry, cart);
723     }
724     return cartEntry;
725 }
726 \f
727 static void AddItemToCart(
728         char *scriptName, char *parent, char *userId, char *processId,
729         char *item)
730 {
731     if(DoAddItemToCart(userId, item, TRUE) < 0) {
732         InvalidRequest("404", "Invalid 'item' argument");
733     } else {
734         /*
735          * Would call
736          *   DisplayStore(scriptName, parent, userId, processId);
737          * except for browser reload issue.  Redirect instead.
738          */
739         printf("Location: %s?op=%s\r\n"
740                "\r\n", scriptName, OP_DISPLAY_STORE);
741     }
742 }
743
744 static int DoAddItemToCart(char *userId, char *item, int writeLog)
745 {
746     if(ItemDisplayName(item) == NULL) {
747         return -1;
748     } else {
749         Tcl_HashEntry *cartEntry = GetCartEntry(userId);
750         CartObj *cart = (CartObj *)Tcl_GetHashValue(cartEntry);
751         cart->items = ListOfString_AppendElement(
752                               cart->items, StringCopy(item));
753         if(writeLog) {
754             WriteLog(LR_ADD_ITEM, userId, item, TRUE);
755         }
756     }
757     return 0;
758 }
759 \f
760 static void DisplayCart(
761         char *scriptName, char *parent, char *userId, char *processId)
762 {
763     Tcl_HashEntry *cartEntry = GetCartEntry(userId);
764     CartObj *cart = (CartObj *)Tcl_GetHashValue(cartEntry);
765     ListOfString *items = cart->items;
766     int numberOfItems = ListOfString_Length(items);
767
768     DisplayHead("Your shopping cart", parent, "Images/cart-hd.gif");
769     DisplayNumberOfItems(numberOfItems, processId);
770     printf("<ul>\n");
771     for(; items != NULL; items = ListOfString_Tail(items)) {
772         char *item = ListOfString_Head(items);
773         printf("  <li>%s . . . . . \n"
774                "    <a href=\"%s?op=RemoveItemFromCart&item=%s\">Click\n"
775                "    to remove</a> from your shopping cart.\n",
776                ItemDisplayName(item), scriptName, item);
777     }
778     printf("</ul><p>\n\n");
779     printf("<a href=\"%sUnprotected/Purchase.html\">Purchase\n"
780            "the contents of your shopping cart.</a><p>\n\n", parent);
781     printf("<a href=\"%s?op=DisplayStore\">Return to shop.</a><p>\n\n",
782            scriptName);
783     DisplayFoot();
784 }
785 \f
786 static void RemoveItemFromCart(
787         char *scriptName, char *parent, char *userId, char *processId,
788         char *item)
789 {
790     if(DoRemoveItemFromCart(userId, item, TRUE) < 0) {
791         InvalidRequest("404", "Invalid 'item' argument");
792     } else {
793         /*
794          * Would call
795          *   DisplayCart(scriptName, parent, userId, processId);
796          * except for browser reload issue.  Redirect instead.
797          */
798         printf("Location: %s?op=%s\r\n"
799                "\r\n", scriptName, OP_DISPLAY_CART);
800     }
801 }
802
803 static int DoRemoveItemFromCart(char *userId, char *item, int writeLog)
804 {
805     if(ItemDisplayName(item) == NULL) {
806         return -1;
807     } else {
808         Tcl_HashEntry *cartEntry = GetCartEntry(userId);
809         CartObj *cart = (CartObj *)Tcl_GetHashValue(cartEntry);
810         if(ListOfString_IsElement(cart->items, item)) {
811             cart->items = ListOfString_RemoveElement(cart->items, item);
812             if (writeLog) {
813                 WriteLog(LR_REMOVE_ITEM, userId, item, TRUE);
814             }
815         }
816     }
817     return 0;
818 }
819 \f
820 static void Purchase(
821         char *scriptName, char *parent, char *userId, char *processId)
822 {
823     DoEmptyCart(userId, TRUE);
824     printf("Location: %sUnprotected/ThankYou.html\r\n"
825            "\r\n", parent);
826 }
827
828 static int DoEmptyCart(char *userId, int writeLog)
829 {
830     Tcl_HashEntry *cartEntry = GetCartEntry(userId);
831     CartObj *cart = (CartObj *)Tcl_GetHashValue(cartEntry);
832     ListOfString *items = cart->items;
833     /*
834      * Write log *before* tearing down cart structure because userId
835      * is part of the structure.  (Thanks, Purify.)
836      */
837     if (writeLog) {
838         WriteLog(LR_EMPTY_CART, userId, "NullItem", TRUE);
839     }
840     while(items != NULL) {
841         items = ListOfString_RemoveElement(
842                 items, ListOfString_Head(items));
843     }
844     Free(cart);
845     Tcl_DeleteHashEntry(cartEntry);
846     return 0;
847 }
848 \f
849 static void NotAuthorized(void);
850
851 static void Authorize(char *userId)
852 {
853     Tcl_HashEntry *cartEntry = GetCartEntry(userId);
854     ListOfString *items = ((CartObj *) Tcl_GetHashValue(cartEntry))->items;
855     for( ; items != NULL; items = ListOfString_Tail(items)) {
856         if(!strcmp(ListOfString_Head(items), ItemNames[REQUIRED_ITEM])) {
857             printf("Status: 200 OK\r\n"
858                    "Variable-Foo: Bar\r\n"
859                    "\r\n");
860            return;
861         }
862     }
863     NotAuthorized();
864 }
865 \f
866 static void DisplayNumberOfItems(int numberOfItems, char *processId)
867 {
868     if(processId != NULL) {
869         printf("FastCGI process %s is serving you today.<br>\n", processId);
870     }
871     if(numberOfItems == 0) {
872         printf("Your shopping cart is empty.<p>\n\n");
873     } else if(numberOfItems == 1) {
874         printf("Your shopping cart contains 1 item.<p>\n\n");
875     } else {
876         printf("Your shopping cart contains %d items.<p>\n\n", numberOfItems);
877     };
878 }
879 \f
880 static void InvalidRequest(char *code, char *message)
881 {
882     printf("Status: %s %s\r\n", code, message);
883     DisplayHead("Invalid request", NULL, NULL);
884     printf("%s.\n\n", message);
885     DisplayFoot();
886 }
887
888 static void NotAuthorized(void)
889 {
890     printf("Status: 403 Forbidden\r\n");
891     DisplayHead("Access Denied", NULL, NULL);
892     printf("Put the %s in your cart to access this page.\n\n",
893            ItemDisplayNames[REQUIRED_ITEM]);
894     DisplayFoot();
895 }
896 \f
897 /*
898  * Mundane utility functions, not specific to this application:
899  */
900
901
902 /*
903  * Fail-fast version of 'malloc'
904  */
905 static void *Malloc(size_t size)
906 {
907     void *result = malloc(size);
908     assert(size == 0 || result != NULL);
909     return result;
910 }
911
912 /*
913  * Protect against old, broken implementations of 'free'
914  */
915 static void Free(void *ptr)
916 {
917     if(ptr != NULL) {
918         free(ptr);
919       }
920 }
921
922 /*
923  * Return a new string created by calling Malloc, copying strLen
924  * characters from str to the new string, then appending a null.
925  */
926 static char *StringNCopy(char *str, int strLen)
927 {
928     char *newString = (char *)Malloc(strLen + 1);
929     memcpy(newString, str, strLen);
930     newString[strLen] = '\000';
931     return newString;
932 }
933
934 /*
935  * Return a new string that's a copy of str, including the null
936  */
937 static char *StringCopy(char *str)
938 {
939     return StringNCopy(str, strlen(str));
940 }
941
942 /*
943  * Return a new string that's a copy of str1 followed by str2,
944  * including the null
945  */
946 static char *StringCat(char *str1, char *str2)
947 {
948     return StringCat4(str1, str2, NULL, NULL);
949 }
950
951 static char *StringCat4(char *str1, char *str2, char *str3, char *str4)
952 {
953     int str1Len = Strlen(str1);
954     int str2Len = Strlen(str2);
955     int str3Len = Strlen(str3);
956     int str4Len = Strlen(str4);
957     char *newString = (char *)Malloc(str1Len + str2Len + str3Len + str4Len + 1);
958     memcpy(newString, str1, str1Len);
959     memcpy(newString + str1Len, str2, str2Len);
960     memcpy(newString + str1Len + str2Len, str3, str3Len);
961     memcpy(newString + str1Len + str2Len + str3Len, str4, str4Len);
962     newString[str1Len + str2Len + str3Len + str4Len] = '\000';
963     return newString;
964 }
965
966 /*
967  * Return a copy of the value associated with 'name' in 'query'.
968  * XXX: does not perform URL-decoding of query.
969  */
970 static char *QueryLookup(char *query, char *name)
971 {
972     int nameLen = strlen(name);
973     char *queryTail, *nameFirst, *valueFirst, *valueLast;
974
975     if(query == NULL) {
976         return NULL;
977     }
978     queryTail = query;
979     for(;;) {
980         nameFirst = strstr(queryTail, name);
981         if(nameFirst == NULL) {
982             return NULL;
983         }
984         if(((nameFirst == query) || (nameFirst[-1] == '&')) &&
985                 (nameFirst[nameLen] == '=')) {
986             valueFirst = nameFirst + nameLen + 1;
987             valueLast = strchr(valueFirst, '&');
988             if(valueLast == NULL) {
989                 valueLast = strchr(valueFirst, '\000');
990             };
991             return StringNCopy(valueFirst, valueLast - valueFirst);
992         }
993         queryTail = nameFirst + 1;
994     }
995 }
996
997 /*
998  * Return a copy of the characters following the final '/' character
999  * of path.
1000  */
1001 static char *PathTail(char *path)
1002 {
1003     char *afterSlash, *slash;
1004     if(path == NULL) {
1005         return NULL;
1006     }
1007     afterSlash = path;
1008     while((slash = strchr(afterSlash, '/')) != NULL) {
1009         afterSlash = slash + 1;
1010     }
1011     return StringCopy(afterSlash);
1012 }
1013
1014 /*
1015  * Return the integer value of the specified environment variable,
1016  * or a specified default value if the variable is unbound.
1017  */
1018 static int IntGetEnv(char *varName, int defaultValue)
1019 {
1020     char *strValue = getenv(varName);
1021     int value = 0;
1022     if(strValue != NULL) {
1023         value = strtol(strValue, NULL, 10);
1024     }
1025     if(value <= 0) {
1026         value = defaultValue;
1027     }
1028     return value;
1029 }
1030
1031 /*
1032  * ListOfString abstraction
1033  */
1034
1035 static char *ListOfString_Head(ListOfString *list)
1036 {
1037     return list->head;
1038 }
1039
1040 static ListOfString *ListOfString_Tail(ListOfString *list)
1041 {
1042     return list->tail;
1043 }
1044
1045 static int ListOfString_Length(ListOfString *list)
1046 {
1047     int length = 0;
1048     for(; list != NULL; list = list->tail) {
1049         length++;
1050     }
1051     return length;
1052 }
1053
1054 static int ListOfString_IsElement(ListOfString *list, char *element)
1055 {
1056     for(; list != NULL; list = list->tail) {
1057         if(!strcmp(list->head, element)) {
1058             return TRUE;
1059         }
1060     }
1061     return FALSE;
1062 }
1063
1064 static ListOfString *ListOfString_AppendElement(
1065         ListOfString *list, char *element)
1066 {
1067     ListOfString *cur;
1068     ListOfString *newCell = (ListOfString *)Malloc(sizeof(ListOfString));
1069     newCell->head = element;
1070     newCell->tail = NULL;
1071     if(list == NULL) {
1072         return newCell;
1073     } else {
1074         for(cur = list; cur->tail != NULL; cur = cur->tail) {
1075         }
1076         cur->tail = newCell;
1077         return list;
1078     }
1079 }
1080
1081 static ListOfString *ListOfString_RemoveElement(
1082         ListOfString *list, char *element)
1083 {
1084     ListOfString *cur;
1085     ListOfString *prevCell = NULL;
1086     for(cur = list; cur != NULL; cur = cur->tail) {
1087         if(!strcmp(cur->head, element)) {
1088             if(prevCell == NULL) {
1089                 list = cur->tail;
1090             } else {
1091                 prevCell->tail = cur->tail;
1092             }
1093             free(cur->head);
1094             free(cur);
1095             return list;
1096         }
1097         prevCell = cur;
1098     }
1099     return list;
1100 }
1101
1102
1103 /*
1104  * End
1105  */