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