Anatomy of a real CGI program using libht

The following s a real program which manages a registration database for a convention. It uses MSQL to manage the Shriner Wannabes, and uses the login database manager.

Note that this program uses the same template form for both a debugging version and the administrative version of the user interface. This is achieved by using $set's in the template.

Here is the admin template file

#include <sys/file.h>
#include <time.h>
#include <msql.h>
#include <html.h>


#define CONDBFIND  "/user/sfx/public_html/condbfind.html"
#define CONDBADMIN "/user/sfx/public_html/admin.html"

#define DBHOST "fasolt"
#define DBFILE "sfx"
#define LOCKFILE "/user/sfx/db/sfx.lock"
#define ADMINFILE "/user/sfx/db/sfx.admin"
#define BASEURL "http://www.mtcc.com/~sfx/"
#define PASSSEED "sf"
#define ADMIN  "admin"

#define GENERIC "/user/sfx/public_html/generic.html"


int dbsock;

hrec * hrecs;
int nrecs;

char * nochoice = "No Choice/Other";

/*
 * Called con <db> <cmd>
 */

main (int ac, char ** av) 
{
    FILE * lock, * ap;
    hrec * in;
    char * cmd;

    preHTML ();
preHTML() inserts the MIME headers. The sooner it's done, the better as the HTTPD blows chunks if it doesn't see them.
   
    dbsock = msqlConnect (DBHOST);
    if (dbsock < 0)
        error ("can't open database");
    if (msqlSelectDB (dbsock, "sfx") < 0)
        error ("can't select database");
Hinitlogin sets registers us with the login session manager
    if (hinitlogin ("fasolt", "condb", "sf", 60*60) < 0)
        error ("can't start up login database.");
We send the parameters to hrgetparams(). It automatically figures out whether this is a GET or POST and creates an input hrec.

    in = hrgetparams (0, ac, av);

Set various variables in the template environment. Admin will be set later based on login credentials.
    hputenv ("admin", "no");
#ifdef DEBUG
    hputenv ("debug", "yes");
#else
    hputenv ("debug", "no");
#endif
Check what the command is, and dispatch based on it.
    if (* (cmd = hrget (in, "cmd")) == 0) {
        if (ac < 2) {
            if (htreadfile (CONDBADMIN) == 0)
                error ("can't read template.");
            htdumpfile ();
            exit (0);
        }            
        cmd = av [1];
    }    
    lock = fopen (LOCKFILE, "r");
    /* be paranoid: grab an exclusive lock for all reasons. */
    flock (fileno (lock), LOCK_EX);
    switch (* cmd) {
        case 'u':
            login (in);
            break;
        case 'f':
            find (in, 0);
            break;
        case 'l':              /* list */
            list (in);
            break;
        case 'a':              /* add */
            add (in);            
            break;
        case 'r':              /* remove */
            rem (in);
            break;
        default:
            error ("unknown command");
            break;
    }
    htdumpfile ();
    exit (0);
}

The default record is more important than it seems. it contains all of the fields that we will fetch from the database as well as their default values
   
hrec * getdefrec () 
{
    hrec * r = newhrec ();    

    hradd (r, "fullname", "");
    hradd (r, "pwd", "");
    hradd (r, "email", "");
    hradd (r, "phone", "");
    hradd (r, "snail", "");
    hradd (r, "credate", "");
    hradd (r, "number", "1");
    hradd (r, "web", "http://");
    hradd (r, "dates", "Aug 7-11");
    hradd (r, "scholarship", "$0");
    hradd (r, "thur", nochoice);
    hradd (r, "fri", nochoice);
    hradd (r, "satday", nochoice);
    hradd (r, "sateve", nochoice);
    hradd (r, "sunmorn", nochoice);
    hradd (r, "sunday", nochoice);
    hradd (r, "suneve", nochoice);
    hradd (r, "mon", nochoice);
    hradd (r, "lodging", nochoice);
    hradd (r, "muni", "no");
    hradd (r, "private", "no");
    hradd (r, "paid", "");
    return r;
    
}

Login does the login validation. It is somewhat convoluted because it will automagically create new users if their password is typed in twice. Additionally, a privileged mode is created if a special user Admin is used.


login (hrec * in) 
{
    char epwd [50];
    char * u = hrget (in, "email");
    hrec * r, * login;
    hrec * found;
    
    /* either if login fails or we're not using the
       admin password and the usernames have changed,
       go through login again. */

    if ((login = hvrfysession (in, 0)) == 0 ||
         strcmp (hrget (login, "user"), hrget (in, "email"))) {
        char * pw1, * pw2;
        
        r = getdefrec ();
        /* it's not an error if the record isn't found. the ugly
           if below will figure that out later... */
        found = hmfind (dbsock, r, DBFILE, "email", hrget (in, "email"));
        strcpy (epwd, hrget (r, "pwd"));

        pw1 = hrget (in, "pwd");
        pw2 = hrget (in, "pwd2");

        if (* pw1 && * pw2) {
            if (found) {
                if (* epwd == 0)
                    login = hnewlogin (u, pw1, pw2, epwd);
                else
                    error ("can't change password.");
            } else            
                login = hnewlogin (u, pw1, pw2, epwd);
        } else
            login = hlogin (u, pw1, epwd);
        if (login == 0)  
            error ("Login failed.");
        hradd (in, "sessid", hrget (login, "sessid"));
The session id will uniquely bind stateless HTTPD into a stateful session. The session id is bound to each form below.

    }
    if (strcmp (hrget (login, "user"), ADMIN))
        find (in, login);
    else {
        htelm * h;
        hputenv ("admin", "yes");
        h = htreadfile (CONDBADMIN);
        hloginbindall (in);
    }
}
Find a user given the login information found above and the incoming record.
find (hrec * in, hrec * login)
{
    hrec * r;
    htelm * h;
    char * pass;
    int isadmin;


    if (login == 0 && (login = hvrfysession (in, 0)) == 0)
        error ("invalid login information.");    

    isadmin = ! strcmp (hrget (login, "user"), ADMIN);    

    if (isadmin)
        hputenv ("admin", "yes");
Note this hputenv controls whether the template shows privileged fields.
    h = htreadfile (CONDBFIND);
    if (h == 0)
        error ("can't read template file");

    if (* hrget (in, "email") == 0)
        error ("You must supply an email address\n");
    
    r = getdefrec ();

    if (hmfind (dbsock, r, DBFILE, "email", hrget (in, "email")) == 0) 
        hradd (r, "email", hrget (in, "email"));
This part fetches the user out of the database. The input field email's value is used as the key to find them. Note that the fields that are fetched from the database use the default record created above.
      
    hrmerge (r, in);
    htgotomark ("add");
    htformnext ();
    htbindform (r);
Bind form takes the var/vals and attaches them to the appropriate fields in the current form.
   

    htgotomark ("rem");
    htformnext ();
    htbindform (r);
All done!
   

}

add (hrec * in)
{
    long tm;
    char curtime [50];
    htelm * h;
    hrec * old, * login;
    int prv = 1;
    char * u;
    int isadmin;
        

    if (* (u = hrget (in, "email")) == 0)
        error ("You must supply an email address.");
    if ((login = hvrfysession (in, 0)) == 0)
        error ("login information invalid.");

    isadmin = ! strcmp (hrget (login, "user"), ADMIN);

    if (! strcmp (hrget (in, "button"), "Find")) {
        old = newhrec ();
        hradd (old, "email", hrget (in, "email"));
        find (old, login);
        return;
    }
    h = htreadfile (GENERIC);
    if (h == 0)
        error ("couldn't read template.");

    old = getdefrec ();
    if (isadmin == 0 && strcmp (hrget (login, "user"), u))
        error ("can't change the email address.");

    time (&tm);
    strcpy (curtime, ctime (&tm));
    chop (curtime);        
    if (hmfind (dbsock, old, DBFILE, "email", u) == 0) {
        /* use the incoming record as a base */
        old = in;
        hradd (old, "pwd", hrget (login, "pwd"));
        hradd (old, "credate", curtime);
    } else {
        /* merge everything into old */
        hrsetflg (old, "credate", HR_NOMERGE);
        hrsetflg (old, "pwd", HR_NOMERGE);
        hrsetflg (old, "pwd2", HR_NOMERGE);
        hrmerge (old, in);
    }
Much of the voodoo above is making certain that we don't overwrite creation dates, and cleartext passwords over encrypted passwords.
   
    /* if there wasn't a password, inherit the login password */
    if (* hrget (old, "pwd") == 0)
        hradd (old, "pwd", hrget (login, "pwd"));
    /* if they pressed the clear password button, clear it */
    if (! strcmp (hrget (in, "button"), "clear password")) {
        if (isadmin)
            hradd (old, "pwd", "");
        else
            error ("not logged in.");
    }    
    hrsetflg (old, "sessid", HR_HIDDEN);
    hrsetflg (old, "pwd2", HR_HIDDEN);
    hrsetflg (old, "button", HR_HIDDEN);
Since the database manager tages everything in the hrec, unmeaningful fields cause MSQL to whine. Hide them.
   
    nrecs++;
    hradd (old, "moddate", curtime);
    if (hmupdate (dbsock, old, DBFILE, "email", hrget (old, "email")) < 0) {
        error ("sql error: %s", msqlErrMsg);
    }
Klunk! Into the database.
   
    htgotomark ("pageheader");
    if (isadmin)
        htprintf ("%s's record for con attendance has been updated.\n",
                  hrget (old, "email"));
    else {
        htprintf ("%s, your record for con attendance has been added.\n",
                  hrget (old, "fullname"));
        htgotomark ("pagebody");
        bq (2);
        htprintf ("Thanks for taking the time to fill it out<br>\n");
        htprintf ("If you need to change anything, just re-enter<br>\n");
        htprintf ("your email address and modify it to your heart's content.\n");
        bq (-2);
    }
}


rem (hrec * in) 
{
    hrec * old, * login;
    htelm * h = htreadfile (GENERIC);
    char * u;
    int isadmin;
    

    if (h == 0)
        error ("can't read template file.");    
    if (* (u = hrget (in, "email")) == 0)
        error ("You must supply an email address.");    
    if ((login = hvrfysession (in, 0)) == 0)
        error ("login information invalid.");
    isadmin = ! strcmp (hrget (login, "user"), ADMIN);
    
    if (isadmin == 0 && strcmp (hrget (login, "user"), u))
        error ("remove: can't change the email address.");
    
    old = getdefrec ();
    if (hmfind (dbsock, old, DBFILE, "email", u)) {
        if (hmdelete (dbsock, DBFILE, "email", hrget (in, "email")) < 0)
            error (msqlErrMsg);
        htgotomark ("pageheader");
        htprintf ("your record for con attendance has been deleted.\n");
        return;            
    }
    htgotomark ("pageheader");    
    htprintf ("your record was never added.\n");
}


This could conceivably use the database to sort stuff, but I wanted to prettify the output to key off of last name.
   


int ucmp (const void * k1, const void * k2) 
{
    char * p1, * p2, * p;
    int rv;

    p1 = hrget (* (hrec **) k1, "fullname");    
    if (p = strrchr (p1, ' '))
        p1 = p+1;
    p2 = hrget (* (hrec **) k2, "fullname");    
    if (p = strrchr (p2, ' '))
        p2 = p+1;
    return strcasecmp (p1, p2);
}


Most of the contortion here is trying to educe which list I wanted to output. If I were smarter in the first place, I would have coded events as event numbers and joined it to another table thus greatly simplifying this mess, but I was lazy and thus this mess.
   

list (hrec * in) 
{
    hrec * p;
    int cnt = 0, samt = 0, j;
    hrec ** vec, * login;
    int priv = 0, fmt;
    htelm * h = htreadfile (GENERIC);
    
    struct lcmd {
        char * list;
        char * fmt;
        char * priv;
        char * targ;
    } * cmd;
    

    if (h == 0)
        error ("can't open template.");

    if (login = hvrfysession (in, 0))
        priv  = ! strcmp (hrget (login, "user"), ADMIN);

    cmd = (struct lcmd *) split (hrget (in, "list"), ',');
    /* if not a privileged list, elevate their privilege */
    if (cmd->priv [0] == 'y' && ! priv)
        error ("admin password required.");

    fmt = atol (cmd->fmt);
This lazily reads the database into memory. Obviously this could be done using hmfetchrows directly, but this program was converted several times along the way and I wasn't in the mood.
    nrecs = readdb ();
    vec = alloca (nrecs * sizeof (vec));
    for (p = hrecs; p; p = p->flink) {
        if (fmt > 0 || ! strcmp (hrget (p, cmd->list), cmd->targ)) {
            vec [cnt] = p;
            cnt++;
        }
    }    

    htgotomark ("pageheader");
    htprintf ("List of %s", cmd->targ);
    htgotomark ("pagebody");

    bq (2);    
    
    qsort (vec, cnt, sizeof (vec), ucmp);
    if (fmt == 4 || fmt == 3)
        dl (1);
Note that this doesn't use any templates for the most part. This is not entirely unsavory, but using a table may have been a better overall scheme.
   
    for (j = 0; j < cnt; j++) {
        int t;
        char url [512], * s;

        p = vec [j];
        /* construct a url if privileged. */
        if (priv) {
            strcpy (url, "http://");
            if (s = getenv ("SERVER_NAME")) 
                strcat (url, s);
            if (s = getenv ("SCRIPT_NAME"))
                strcat (url, s);
            strcat (url, "?cmd=find&email=");
            strcat (url, hrget (p, "email"));
            strcat (url, "&sessid=");
            strcat (url, hrget (login, "sessid"));
        } else
            url [0] = 0;        
        sscanf (hrget (p, "scholarship"), "$%d", &t);
        samt += t;
        if (! strcmp (hrget (p, "private"), "yes") && ! priv)
            continue;
        /* don't bother with the admin record. */
        if (! strcmp (hrget (p, "email"), "admin") && fmt != 4)
            continue;
        
        switch (fmt) {
            case 1:
                if (priv)
                    aHTML (url, "");
                htprintf ("%s (%s, %s)\n", hrget (p, "fullname"),
                          hrget (p, "email"), hrget (p, "lodging"));
                if (priv)
                    endaHTML ();
                br (1);
                break;
            case 2:
                aHTML (url, "");
                htprintf ("%s (%s, %s)\n", hrget (p, "fullname"),
                          hrget (p, "email"), hrget (p, "scholarship"));
                endaHTML ();
                br (1);
                break;
            case 3:
                hl (4);
                dt ();
                aHTML (url, "");                
                htprintf ("%s (%s) has paid for:\n",
                          hrget (p, "fullname"), hrget (p, "email"));
                endaHTML ();
                hl (-4);
                dd ();
                pre (1);
                htprintf ("%s\n", hrget (p, "paid"));
                pre (0);
                break;
            case 4: 
            {

                
                hl (4);
                dt ();
                aHTML (url, "");                
                htprintf ("%s (%s)\n", hrget (p, "fullname"), hrget (p, "email"));
                endaHTML ();
                
                hl (-4);
                dd ();
                htprintf ("snail: %s<br>\n", hrget (p, "snail"));
                htprintf ("phone: %s<br>\n", hrget (p, "phone"));
                htprintf ("web: %s<br>\n", hrget (p, "web"));
                htprintf ("number in party: %s<br>\n", hrget (p, "number"));
                htprintf ("thurs: %s<br>\n", hrget (p, "thur"));
                htprintf ("fri: %s<br>\n", hrget (p, "fri"));
                htprintf ("satday: %s<br>\n", hrget (p, "satday"));
                htprintf ("sateve: %s<br>\n", hrget (p, "sateve"));
                htprintf ("sunmorn: %s<br>\n", hrget (p, "sunmorn"));
                htprintf ("suneve: %s<br>\n", hrget (p, "suneve"));
                htprintf ("mon: %s<br>\n", hrget (p, "mon"));
                htprintf ("lodging: %s<br>\n", hrget (p, "lodging"));
                htprintf ("muni: %s<br>\n", hrget (p, "muni"));
                htprintf ("scholarship: %s<br>\n", hrget (p, "scholarship"));
                htprintf ("private: %s<br>\n", hrget (p, "private"));
                htprintf ("paid:<br> <pre>%s</pre><br>\n", hrget (p, "paid"));
                htprintf ("credate: %s<br>\n", hrget (p, "credate"));
                htprintf ("moddate: %s<br>\n", hrget (p, "moddate"));
                break;
            }
            default:
                if (priv)
                    aHTML (url, "");
                htprintf ("%s (%s)\n", hrget (p, "fullname"), hrget (p, "email"));
                if (priv)
                    endaHTML ();
                br (1);
                break;
        }
    }
    if (fmt == 4 || fmt == 3)
        dl (0);
    br (1);
    if (fmt == 2)
        htprintf ("%d total for $%d", cnt-1, samt);
    else
        htprintf ("%d total.", cnt-1);
    bq (-2);
}



readdb () 
{
    hrec * p;
    m_result * ctx = 0;
    int nrecs = 0;
    
    while (1) {
        p = getdefrec ();
        if (hmfetchrows (dbsock, p, DBFILE, &ctx) == 0)
            break;
        hrlink (&hrecs, p);
        nrecs++;
    }
    return nrecs;
}

Note that this makes certain that something will always be output if something goes wrong.
   


error (char * str) 
{
    htelm * h;
    
    hterase ();
    preHTML ();
    if ((h = htreadfile (GENERIC)) == 0)
        startHTML ("condb error", BASEURL);
    else {
        htgotomark ("title");
        htprintf ("condb error");
    }
    htgotomark ("pageheader");
    par ();
    htprintf ("an error has occurred: %s\n", str);
    if (h == 0)
        endHTML ();
    else
        htdumpfile ();    
    exit (0);
}

That's all folks!



© (copyright) 1997 MTCC
Last modified: Tue Apr 22 20:24:26 PDT 1997