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