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