4 * FastCGI example program using fcgi_stdio library
7 * Copyright (c) 1996 Open Market, Inc.
9 * See the file "LICENSE.TERMS" for information on usage and redistribution
10 * of this file, and for a DISCLAIMER OF ALL WARRANTIES.
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.
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.
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
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.
39 * sample-store operates both as a FastCGI Responder and as an
40 * Authorizer, showing how one program can play two roles.
42 * The techniques used in sample-store are not specific to shopping
43 * carts; they apply equally well to maintaining all sorts of
49 static const char rcsid[] = "$Id: sample-store.c,v 1.1 1997/09/16 15:36:28 stanleyg Exp $";
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 */
63 * sample-store is designed to be configured as follows:
65 * SI_Department SampleStoreDept -EnableAnonymousTicketing 1
66 * Region /SampleStore/* { SI_RequireSI SampleStoreDept 1 }
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
78 * sample-store looks for three initial environment variables:
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.
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.
95 * When a cart has not been accessed for this many minutes it
96 * may be deleted. Defaults to CART_HOLD_MINUTES_DEFAULT.
98 * The program is prepared to run as multiple processes using
99 * session affinity (illustrated in config above) or as a single process.
101 * The program does not depend upon the specific URL prefix /SampleStore.
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.
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.
124 #define max(a,b) ((a) > (b) ? (a) : (b))
127 #define Strlen(str) (((str) == NULL) ? 0 : strlen(str))
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);
138 typedef struct ListOfString {
140 struct ListOfString *tail;
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);
151 static int IntGetEnv(char *varName, int defaultValue);
153 static void Initialize(void);
154 static void PerformRequest(void);
155 static void GarbageCollectStep(void);
156 static void ConditionalCheckpoint(void);
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.
166 while(FCGI_Accept() >= 0) {
169 GarbageCollectStep();
170 ConditionalCheckpoint();
175 * All the global variables
177 typedef struct CartObj {
178 int inactive; /* This cart not accessed since mark */
179 ListOfString *items; /* Items in cart */
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 */
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
201 #define SNP_PREFIX "snapshot"
202 #define LOG_PREFIX "log"
203 #define TMP_SNP_NAME "tmp-snapshot"
205 #define LR_ADD_ITEM "Add"
206 #define LR_REMOVE_ITEM "Rem"
207 #define LR_EMPTY_CART "Emp"
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);
217 * Initialize the process by reading environment variables and files
219 static void Initialize(void)
222 ListOfString *fileList;
225 * Process miscellaneous parameters from the initial environment.
227 checkpointThreshold =
228 IntGetEnv(CKP_THRESHOLD_VAR, CKP_THRESHOLD_DEFAULT);
230 IntGetEnv(CART_HOLD_MINUTES_VAR, CART_HOLD_MINUTES_DEFAULT)*60;
232 * Create an empty in-memory shopping cart data structure.
234 cartTablePtr = &cartTable;
235 Tcl_InitHashTable(cartTablePtr, TCL_STRING_KEYS);
237 * Compute the state directory name from the initial environment
240 stateDir = getenv(STATE_DIR_VAR);
241 stateDirLen = Strlen(stateDir);
242 assert(stateDirLen > 0);
243 if(stateDir[stateDirLen - 1] == '/') {
244 stateDir[stateDirLen - 1] = '\000';
246 fcgiProcessId = getenv(PID_VAR);
247 if(fcgiProcessId != NULL) {
248 stateDir = StringCat4(stateDir, ".", fcgiProcessId, "/");
250 stateDir = StringCat(stateDir, "/");
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.
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);
268 * Clean up stateDir without removing the current snapshot and log.
270 while(fileList != NULL) {
271 char *cur = ListOfString_Head(fileList);
272 if(strcmp(snapshotPath, cur) && strcmp(logPath, cur)) {
275 fileList = ListOfString_RemoveElement(fileList, cur);
279 static char *MakePath(char *dir, char *prefix, int gen)
282 sprintf(nameBuffer, "%s.%d", prefix, gen);
283 return StringCat(dir, nameBuffer);
286 static void ConditionalCheckpoint(void)
288 if(numLogRecords >= checkpointThreshold) {
292 static void WriteSnapshot(char *snpPath);
294 static void Checkpoint(void)
296 char *tempSnapshotPath, *newLogPath, *newSnapshotPath;
298 * Close the current log file.
300 if(logFile != NULL) {
304 * Create a new snapshot with a temporary name.
306 tempSnapshotPath = StringCat(stateDir, TMP_SNP_NAME);
307 WriteSnapshot(tempSnapshotPath);
310 * Ensure that the new log file doesn't already exist by removing it.
312 newLogPath = MakePath(stateDir, LOG_PREFIX, generation);
315 * Commit by renaming the snapshot. The rename atomically
316 * makes the old snapshot and log obsolete.
318 newSnapshotPath = MakePath(stateDir, SNP_PREFIX, generation);
319 rename(tempSnapshotPath, newSnapshotPath);
321 * Clean up the old snapshot and log.
323 Free(tempSnapshotPath);
324 remove(snapshotPath);
326 snapshotPath = newSnapshotPath;
329 logPath = newLogPath;
331 * Open the new, empty log.
333 logFile = fopen(logPath, "a");
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 '..'
343 static void AnalyzeStateDir(
344 char *dir, char *prefix, int *largestP, ListOfString **fileListP)
348 int prefixLen = strlen(prefix);
352 ListOfString *fileList = NULL;
355 while((dirp = readdir(dp)) != NULL) {
356 if(!strcmp(dirp->d_name, ".") || !strcmp(dirp->d_name, "..")) {
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);
369 assert(closedir(dp) >= 0);
371 *fileListP = fileList;
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);
379 * Read either a snapshot or a log and perform the specified
380 * actions on the in-memory representation.
382 static int RecoverFile(char *pathname)
385 FILE *recoveryFile = fopen(pathname, "r");
386 if(recoveryFile == NULL) {
387 assert(errno == ENOENT);
390 for(numRecords = 0; ; numRecords++) {
392 char op[32], userId[32], item[64];
394 char *status = fgets(buff, sizeof(buff), recoveryFile);
396 assert(feof(recoveryFile));
397 fclose(recoveryFile);
400 count = sscanf(buff, "%31s %31s %63s", op, userId, item);
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);
414 static void WriteLog(char *command, char *userId, char *item, int force);
417 * Read the in-memory representation and write a snapshot file
420 static void WriteSnapshot(char *snpPath)
422 Tcl_HashSearch search;
423 Tcl_HashEntry *cartEntry;
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);
438 fsync(fileno(logFile));
442 static void WriteLog(char *command, char *userId, char *item, int force)
444 fprintf(logFile, "%s %s %s\n", command, userId, item);
448 fsync(fileno(logFile));
452 static int RemoveOneInactiveCart(void);
453 static void MarkAllCartsInactive(void);
456 * Incremental garbage collection of inactive shopping carts:
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.
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.
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.
472 static void GarbageCollectStep(void)
475 if(!RemoveOneInactiveCart()) {
476 MarkAllCartsInactive();
477 timeCartsMarked = time(NULL);
481 int diff = time(NULL)-timeCartsMarked;
482 if(diff > cartHoldSeconds) {
488 static int RemoveOneInactiveCart(void)
490 Tcl_HashSearch search;
491 Tcl_HashEntry *cartEntry;
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);
499 userId = Tcl_GetHashKey(cartTablePtr, cartEntry);
500 DoEmptyCart(userId, TRUE);
507 static Tcl_HashEntry *GetCartEntry(char *userId);
509 static void MarkAllCartsInactive(void)
511 Tcl_HashSearch search;
512 Tcl_HashEntry *cartEntry;
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;
522 static void MarkThisCartActive(char *userId)
524 Tcl_HashEntry *cartEntry = GetCartEntry(userId);
525 CartObj *cart = Tcl_GetHashValue(cartEntry);
526 cart->inactive = FALSE;
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"
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,
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,
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);
551 * As a Responder, this application expects to be called with the
552 * GET method and a URL of the form
554 * http://<host-port>/<script-name>?op=<op>&item=<item>
556 * The application expects the SI_UID variable to provide
557 * a user ID, either authenticated or anonymous.
559 * The application expects the directory *containing* <script-name>
560 * to contain various static HTML files related to the application.
562 * As an Authorizer, the application expects to be called with
563 * SID_UID and URL_PATH set.
566 static void PerformRequest(void)
568 char *method = getenv("REQUEST_METHOD");
569 char *role = getenv("FCGI_ROLE");
570 char *scriptName = PathTail(getenv("SCRIPT_NAME"));
572 char *op = QueryLookup(getenv("QUERY_STRING"), "op");
573 char *item = QueryLookup(getenv("QUERY_STRING"), "item");
574 char *userId = getenv("SI_UID");
576 InvalidRequest("405", "Incorrect configuration, no user id");
579 MarkThisCartActive(userId);
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);
595 InvalidRequest("404", "Invalid 'op' argument");
597 } else if(!strcmp(role, "AUTHORIZER")) {
600 InvalidRequest("404", "Invalid FastCGI Role");
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).
616 char *ItemNames[] = {
623 char *ItemDisplayNames[] = {
624 "<i>Brooklyn Bridge</i>",
625 "<i>RMS Titanic</i>",
626 "<i>Comet Kohoutec</i>",
627 "<i>Yellow Submarine</i>",
630 #define REQUIRED_ITEM 1
631 #define SPECIAL_ITEM 3
634 static char *ItemDisplayName(char *item)
640 for(i = 0; ItemNames[i] != NULL; i++) {
641 if(!strcmp(item, ItemNames[i])) {
642 return ItemDisplayNames[i];
648 static void DisplayNumberOfItems(int numberOfItems, char *processId);
650 static void DisplayHead(char *title, char *parent, char *gif)
652 printf("Content-type: text/html\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",
661 printf("<h2>%s</h2>\n<hr>\n\n", title);
665 static void DisplayFoot(void)
667 printf("<hr>\n</body>\n</html>\n");
670 static void DisplayStore(
671 char *scriptName, char *parent, char *userId, char *processId)
673 Tcl_HashEntry *cartEntry = GetCartEntry(userId);
674 ListOfString *items = ((CartObj *) Tcl_GetHashValue(cartEntry))->items;
675 int numberOfItems = ListOfString_Length(items);
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]);
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);
699 static Tcl_HashEntry *GetCartEntry(char *userId)
701 Tcl_HashEntry *cartEntry = Tcl_FindHashEntry(cartTablePtr, userId);
703 if(cartEntry == NULL) {
704 CartObj *cart = Malloc(sizeof(CartObj));
705 cart->inactive = FALSE;
707 cartEntry = Tcl_CreateHashEntry(cartTablePtr, userId, &new);
709 Tcl_SetHashValue(cartEntry, cart);
714 static void AddItemToCart(
715 char *scriptName, char *parent, char *userId, char *processId,
718 if(DoAddItemToCart(userId, item, TRUE) < 0) {
719 InvalidRequest("404", "Invalid 'item' argument");
723 * DisplayStore(scriptName, parent, userId, processId);
724 * except for browser reload issue. Redirect instead.
726 printf("Location: %s?op=%s\r\n"
727 "\r\n", scriptName, OP_DISPLAY_STORE);
731 static int DoAddItemToCart(char *userId, char *item, int writeLog)
733 if(ItemDisplayName(item) == NULL) {
736 Tcl_HashEntry *cartEntry = GetCartEntry(userId);
737 CartObj *cart = Tcl_GetHashValue(cartEntry);
738 cart->items = ListOfString_AppendElement(
739 cart->items, StringCopy(item));
741 WriteLog(LR_ADD_ITEM, userId, item, TRUE);
746 static void DisplayCart(
747 char *scriptName, char *parent, char *userId, char *processId)
749 Tcl_HashEntry *cartEntry = GetCartEntry(userId);
750 CartObj *cart = Tcl_GetHashValue(cartEntry);
751 ListOfString *items = cart->items;
752 int numberOfItems = ListOfString_Length(items);
755 DisplayHead("Your shopping cart", parent, "Images/cart-hd.gif");
756 DisplayNumberOfItems(numberOfItems, processId);
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);
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",
773 static void RemoveItemFromCart(
774 char *scriptName, char *parent, char *userId, char *processId,
777 if(DoRemoveItemFromCart(userId, item, TRUE) < 0) {
778 InvalidRequest("404", "Invalid 'item' argument");
782 * DisplayCart(scriptName, parent, userId, processId);
783 * except for browser reload issue. Redirect instead.
785 printf("Location: %s?op=%s\r\n"
786 "\r\n", scriptName, OP_DISPLAY_CART);
790 static int DoRemoveItemFromCart(char *userId, char *item, int writeLog)
792 if(ItemDisplayName(item) == NULL) {
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);
800 WriteLog(LR_REMOVE_ITEM, userId, item, TRUE);
806 static void Purchase(
807 char *scriptName, char *parent, char *userId, char *processId)
809 DoEmptyCart(userId, TRUE);
810 printf("Location: %sUnprotected/ThankYou.html\r\n"
814 static int DoEmptyCart(char *userId, int writeLog)
816 Tcl_HashEntry *cartEntry = GetCartEntry(userId);
817 CartObj *cart = Tcl_GetHashValue(cartEntry);
818 ListOfString *items = cart->items;
820 * Write log *before* tearing down cart structure because userId
821 * is part of the structure. (Thanks, Purify.)
824 WriteLog(LR_EMPTY_CART, userId, "NullItem", TRUE);
826 while(items != NULL) {
827 items = ListOfString_RemoveElement(
828 items, ListOfString_Head(items));
831 Tcl_DeleteHashEntry(cartEntry);
835 static void NotAuthorized(void);
837 static void Authorize(char *userId)
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"
852 static void DisplayNumberOfItems(int numberOfItems, char *processId)
854 if(processId != NULL) {
855 printf("FastCGI process %s is serving you today.<br>\n", processId);
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");
862 printf("Your shopping cart contains %d items.<p>\n\n", numberOfItems);
866 static void InvalidRequest(char *code, char *message)
868 printf("Status: %s %s\r\n", code, message);
869 DisplayHead("Invalid request", NULL, NULL);
870 printf("%s.\n\n", message);
874 static void NotAuthorized(void)
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]);
884 * Mundane utility functions, not specific to this application:
889 * Fail-fast version of 'malloc'
891 static void *Malloc(size_t size)
893 void *result = malloc(size);
894 assert(size == 0 || result != NULL);
899 * Protect against old, broken implementations of 'free'
901 static void Free(void *ptr)
909 * Return a new string created by calling Malloc, copying strLen
910 * characters from str to the new string, then appending a null.
912 static char *StringNCopy(char *str, int strLen)
914 char *newString = Malloc(strLen + 1);
915 memcpy(newString, str, strLen);
916 newString[strLen] = '\000';
921 * Return a new string that's a copy of str, including the null
923 static char *StringCopy(char *str)
925 return StringNCopy(str, strlen(str));
929 * Return a new string that's a copy of str1 followed by str2,
932 static char *StringCat(char *str1, char *str2)
934 return StringCat4(str1, str2, NULL, NULL);
937 static char *StringCat4(char *str1, char *str2, char *str3, char *str4)
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';
953 * Return a copy of the value associated with 'name' in 'query'.
954 * XXX: does not perform URL-decoding of query.
956 static char *QueryLookup(char *query, char *name)
958 int nameLen = strlen(name);
959 char *queryTail, *nameFirst, *valueFirst, *valueLast, *value;
965 nameFirst = strstr(queryTail, name);
966 if(nameFirst == NULL) {
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');
976 return StringNCopy(valueFirst, valueLast - valueFirst);
978 queryTail = nameFirst + 1;
983 * Return a copy of the characters following the final '/' character
986 static char *PathTail(char *path)
988 char *afterSlash, *slash;
993 while((slash = strchr(afterSlash, '/')) != NULL) {
994 afterSlash = slash + 1;
996 return StringCopy(afterSlash);
1000 * Return the integer value of the specified environment variable,
1001 * or a specified default value if the variable is unbound.
1003 static int IntGetEnv(char *varName, int defaultValue)
1005 char *strValue = getenv(varName);
1007 if(strValue != NULL) {
1008 value = strtol(strValue, NULL, 10);
1011 value = defaultValue;
1017 * Should the Tcl hash package detect an unrecoverable error(!), halt.
1019 void panic(char *format,
1020 char *arg1, char *arg2, char *arg3, char *arg4,
1021 char *arg5, char *arg6, char *arg7, char *arg8)
1028 * ListOfString abstraction
1031 static char *ListOfString_Head(ListOfString *list)
1036 static ListOfString *ListOfString_Tail(ListOfString *list)
1041 static int ListOfString_Length(ListOfString *list)
1044 for(; list != NULL; list = list->tail) {
1050 static int ListOfString_IsElement(ListOfString *list, char *element)
1052 for(; list != NULL; list = list->tail) {
1053 if(!strcmp(list->head, element)) {
1060 static ListOfString *ListOfString_AppendElement(
1061 ListOfString *list, char *element)
1064 ListOfString *newCell = Malloc(sizeof(ListOfString));
1065 newCell->head = element;
1066 newCell->tail = NULL;
1070 for(cur = list; cur->tail != NULL; cur = cur->tail) {
1072 cur->tail = newCell;
1077 static ListOfString *ListOfString_RemoveElement(
1078 ListOfString *list, char *element)
1081 ListOfString *prevCell = NULL;
1082 for(cur = list; cur != NULL; cur = cur->tail) {
1083 if(!strcmp(cur->head, element)) {
1084 if(prevCell == NULL) {
1087 prevCell->tail = cur->tail;