
\subsubsection{Idea}

We use the recipes of the preceding sections and the MySQL C API
available through {\tt libmysqlclient} to build a web chat with
MySQL. We suggest you acquaint yourself with {\tt phpMyAdmin} and the
{\tt mysql} command line interface in order to facilitate your work
with MySQL.

We will work with a database that stores two types of information:
session data such as the user's name and how often her chat screen
should be refreshed and the chat data, which consists of open and
private channels that are protected by a password and the messages
themselves.

The structure of the chat can be described by the three different
screens it uses:
\begin{itemize}
\item the login screen: this is where the user enters her name, how
often the chat display should be refreshed, and whether she wants to
use an open or a private channel
\item the channel selector screen: this screen lets the user start a
new channel or join an existing one. This is also where the password
for private channels is entered. There is a menu of current channels
when the user selected open channels in the login screen; these show
how many messages are available and when the channel was last active
and are sorted with the most recently active channels first.
\item the main chat screen: this is a frame set with the upper frame
containing the latest messages from the channel and the bottom frame
being a form where the user may enter her message. 
\end{itemize}
There are a variety of error message screens.

The login sequence is as follows: the user identifies herself in the
login screen, which causes a new cookie, i.e. session identifier with
a certain expiry time to be sent, selects or starts a new channel and
starts using the chat through the main screen. The program must handle
all of these steps. Furthermore, it must bracket its actions by a read
and write of the session data, which it reads into a dictionary
whose keys are the individual items.

\subsubsection{MySQL tables used}

There are three tables: {\tt session, channels} and {\tt messages.}
The first holds session data. There is one entry for each session
initiated by a client. The second, i.e. the table {\tt channels},
records channel properties and the the third, {\tt messages}, records
the actual messages. We now discuss the fields of each channel in
detail. Note that the string '\%d' in the create statements is a {\tt
printf}-like placeholder that will be replaced by a constant.

Fields of the table {\tt session}:

\begin{center}
\begin{boxedminipage}{0.33\textwidth}
\begin{verbatim}
CREATE TABLE session (
  sessid char(%d) NOT NULL,
  time BIGINT,
  login char(%d),
  chanid BIGINT,
  refresh INT,
  open ENUM('Y', 'N'),
  UNIQUE KEY (sessid),
  PRIMARY KEY (sessid)
)
\end{verbatim}
\end{boxedminipage}
\end{center}

\begin{itemize}
\item {\tt sessid}: this is the session id, which is a string of
  alphanumeric characters that are not easy to guess
\item {\tt time}: the time when the session was last active, e.g. when
the user sent a message, given in seconds since the epoch (Jan 1,
1970), as are all time stamps in this application
\item {\tt login}: the name that the user chose for herself
\item {\tt chanid}: the identifier of the channel that the user is
currently logged in on
\item {\tt refresh}: this is the number of seconds after which the
display frame of the main screen will refresh
\item {\tt open}: indicates whether the user is logged in to an open
or a private channel
\end{itemize}

Fields of the table {\tt channels}:

\begin{center}
\begin{boxedminipage}{0.43\textwidth}
\begin{verbatim}
CREATE TABLE channels (
  id BIGINT NOT NULL AUTO_INCREMENT,
  time BIGINT NOT NULL,
  name char(%d),
  password char(%d) BINARY,
  open ENUM('Y', 'N'),
  PRIMARY KEY (id)
)
\end{verbatim}
\end{boxedminipage}
\end{center}

\begin{itemize}
\item {\tt id}: is a unique identifier of the channel and assigned by
  MySQL
\item {\tt time}: the time of the most recent activity on the channel
\item {\tt name}: a string that contains the name of the channel,
which we choose to restrict to alphanumeric characters
\item {\tt password}: the encrypted password for the channel if it is
a private channel, the empty string otherwise
\item {\tt open}: a flag that indicates whether the channel is open or
not
\end{itemize}

Fields of the table {\tt messages}:

\begin{center}
\begin{boxedminipage}{0.28\textwidth}
\begin{verbatim}
CREATE TABLE messages (
  time BIGINT NOT NULL,
  login char(%d),
  chanid BIGINT,
  msg BLOB,
  PRIMARY KEY (time)
)
\end{verbatim}
\end{boxedminipage}
\end{center}

\begin{itemize}
\item {\tt time}: time this message was entered
\item {\tt login}: name of the user who entered the message
\item {\tt chanid}: identifier of the channel where the message was
  entered
\item {\tt msg}: the encoded text of the message, which the type {\tt
BLOB} restricts to $2^{16}$ characters, of which we only use a few,
however, but more than fit into the {\tt CHAR} type.
\end{itemize}

\subsubsection{Preliminaries}

There is a wrapper script that works like the wrappers in the recipes
for working with cookies and form variables.
\begin{verbatim}
#! /bin/sh
#

export GNUSTEP_SYSTEM_ROOT=/usr/GNUstep
export TZ=Europe/Berlin
. $GNUSTEP_SYSTEM_ROOT/System/Makefiles/GNUstep.sh

/home/gnustep/webchat/shared_obj/ix86/linux-gnu/gnu-gnu-gnu/webchat
\end{verbatim} %%$

There is an important note concerning the makefile. We must make sure
that the {\tt libmysqlclient} library is linked after {\tt
gnustep-base}. The following makefile accomplishes this. (We have
moved the code from the cookie and form recipes into the file {\tt
CGIaux.m}.)
\begin{verbatim}
include $(GNUSTEP_MAKEFILES)/common.make

TOOL_NAME=webchat
webchat_OBJC_FILES = webchat.m CGIaux.m

TARGET_SYSTEM_LIBS += -lmysqlclient
SHARED_CFLAGS     += -g

include $(GNUSTEP_MAKEFILES)/tool.make
\end{verbatim}

\subsubsection{Implementation I: auxiliary functions and definitions}

Start by defining some constants to tune the behavior of the
program. You are encouraged to experiment with these. The meaning of
each constant follows.

\begin{itemize}
\item {\tt CHATCGI}: this is the path to the CGI script and it tells
the client that it should only send the session id cookie to this path
\item {\tt MYSQL\_USER} and {\tt MYSQL\_PASSWORD}: these two define the
MySQL user that carries out all database operations and it must of
course be defined and have the right set of privileges; we assume
furthermore that the database runs on the same host as the web server
\item {\tt CHATDB}: the name to use for the chat database
\item {\tt LEN\_LOGIN, LEN\_SESSID, LEN\_CHANNEL, LEN\_MSG} and {\tt
LEN\_PASSWORD}: the maximum number of characters for the login name,
the session id, the channel name, the message and the password,
respectively
\item {\tt DISPLAY\_MAX}: the maximum number of messages to display in
the message display frame of the main screen
\item {\tt SESSPERSIST, CHANPERSIST, MSGPERSIST}: these are in seconds
and determine the time that has to elapse for a session, channel or
message to be deleted from the database. 
\end{itemize}

\begin{lstlisting}{}
#include <Foundation/Foundation.h>
#include <mysql/mysql.h>

#include "CGIaux.h"

#define CHATCGI     "/cgi-bin/webchat.sh"

#define MYSQL_USER             "chatuser"
#define MYSQL_PASSWORD          "webchat"
#define CHATDB                   "chatdb"

#define LEN_LOGIN                     16
#define LEN_SESSID                    16
#define LEN_CHANNEL                   32
#define LEN_MSG                      256
#define LEN_PASSWORD                  32

#define DISPLAY_MAX                   10

#define SESSPERSIST                10*60
#define CHANPERSIST   (SESSPERSIST+5*60)
#define MSGPERSIST    (CHANPERSIST+5*60)
\end{lstlisting}

A series of useful macros follows. The macro {\tt MYSQL\_ERR} turns
the most recent error message from MySQL into an {\tt NSString}
object. The macros FMT1 to FMT6 help simplify the use of strings
obtained from a format string, which occurs a lot in this application.

\begin{lstlisting}{}
#define MYSQL_ERR      [NSString stringWithCString: \
                                    mysql_error(&mysql)]

#define FMT1(_f, _a1) \
       [NSString stringWithFormat:_f, _a1]
#define FMT2(_f, _a1, _a2) \
       [NSString stringWithFormat:_f, _a1, _a2]
#define FMT3(_f, _a1, _a2, _a3) \
       [NSString stringWithFormat:_f, _a1, _a2, _a3]
#define FMT4(_f, _a1, _a2, _a3, _a4) \
       [NSString stringWithFormat:_f, _a1, _a2, _a3, _a4]
#define FMT5(_f, _a1, _a2, _a3, _a4, _a5) \
       [NSString stringWithFormat:_f, _a1, _a2, _a3, _a4, _a5]
#define FMT6(_f, _a1, _a2, _a3, _a4, _a5, _a6) \
       [NSString stringWithFormat:_f, _a1, _a2, _a3, _a4, _a5, _a6]
\end{lstlisting}

We now define a set of auxiliary functions that we will use in the
{\tt main} procedure of the script. The function {\tt Empty} tests
whether a string object is empty, i.e. whether it is {\tt nil} or has
length zero.

\begin{lstlisting}{}
BOOL inline Empty(NSString *str)
{
    return (str==nil || ![str length]);
}
\end{lstlisting}

There are peculiarities pertaining to the way messages are being
stored and retrieved that have to be taken into account. A message may
contain characters that we would have to escape before the message can
be given to MySQL. Furthermore, what if a message contains HTML tags?
These could hamper the functionality of the message display screen. We
avoid all these problems by encoding each byte of the message in two
hexadecimal digits and store this safe representation. We reconstruct
the original byte 'B' when we decode the message and output it as the
HTML entity '\&\#B;', which assures that even HTML tags will be
displayed literally (as opposed to being interpreted as markup).

The function {\tt EncodeMsg} iterates over each byte of the string and
uses the upper and lower four bits as an index into a string of
hexadecimal digits. It appends the two digits to the result. We are
done when we have reached the end of the string. Of course this
doubles the length of the string that we will store.

\begin{lstlisting}{}
NSString *EncodeMsg(NSString *msg)
{
    NSString *result = @"";
    const char *ptr = [msg cString],
        *xdigits = "0123456789ABCDEF";
    
    while(*ptr){
        unsigned char item = *ptr++;
        result = [result stringByAppendingFormat:@"%c%c",
                         xdigits[item/16], xdigits[item%16]];
    }

    return result;
}
\end{lstlisting}

The function {\tt DecodeMsgIntoEntities} also iterates over the
individual bytes of its argument. It processes two bytes, i.e. digits,
at a time and depends on digits and letters being in sequence in the
ASCII string encoding. If it finds a decimal digit, then its value is
given by the difference between the digit and the character '0'; the
value of a hexadecimal digit is ten plus the difference between the
digit and the character 'A'. It remains to compute the reconstructed
byte and append the appropriate HTML entity to the result.

\begin{lstlisting}{}
NSString *DecodeMsgIntoEntities(NSString *msg)
{
    NSString *result = @"";
    const char *ptr = [msg cString];

    while(ptr[0] && ptr[1]){
        int upper = (isdigit(ptr[0]) ? 
                     ptr[0]-'0' : ptr[0]-'A'+10);
        int lower = (isdigit(ptr[1]) ? 
                     ptr[1]-'0' : ptr[1]-'A'+10);
        result = [result stringByAppendingFormat:@"&#%d;",
                         upper*16+lower];
        ptr += 2;
    }
    
    return result;
}
\end{lstlisting}

The next two functions encapsulate two chunks of HTML that is output
at several places in the program. The function {\tt Footer} outputs a
separator (line), followed by a link to the author's home page,
followed by the name of the program, the host name and the current
date. All of these should make it easy for the user to locate the
server that is currently being used.

\begin{lstlisting}{}
NSString *Footer()
{
    NSProcessInfo *pinfo = [NSProcessInfo processInfo];

    NSString *fstr =
        FMT3(@"<HR>\n"
             @"<ADDRESS>%@ by marko riedel, "
             @"<A HREF="
             @"http://www.geocities.com/markoriedelde>\n"
             @"http://www.geocities.com/markoriedelde</A>\n"
             @"at %@, %@</ADDRESS>\n",
             [pinfo processName], [pinfo hostName],
             [[NSDate date] description]);

    return fstr;
}
\end{lstlisting}

There are several screens, including those for error messages, where
the user should be given the chance to start over. This is what the
output of the function {\tt BackToLogin} does. It outputs a form
containing a single button and an important hidden field: {\tt cmd},
i.e. ``command.'' This is set to ``restart'' and lets the user restart
the session. We shall see later how commands are processed.  In fact
``restart'' is not the only command; there is also ``display'' and
``enter,'' which display the two frames of the main screen,
respectively.

\begin{lstlisting}{}
NSString *BackToLogin()
{
    NSString *btologin =
        @"<BR><FORM TARGET=_top METHOD=POST "
        @"ACTION=webchat.sh>\n"
        @"<INPUT TYPE=HIDDEN NAME=cmd VALUE=restart>\n"
        @"<INPUT TYPE=SUBMIT "
        @"VALUE=\"Back to login\">\n"
        @"</FORM>\n";

    return btologin;
}
\end{lstlisting}

The function {\tt ErrmsgAndExit} plays an important role in the
program. We do extensive error checking (quite possibly a bit more
than necessary) and we need a function that outputs an error message
and lets the user start over. This is the purpose of {\tt
ErrmsgAndExit}. It is invoked with a short description of the error
and a string that gives the details of the error that occurred. It
also outputs the restart button.

\begin{lstlisting}{}
void ErrmsgAndExit(NSString *desc, NSString *detail)
{
    NSString *body = 
        FMT5(@"<HTML><HEAD>\n"
             @"<TITLE>%@</TITLE>\n"
             @"</HEAD><BODY BGCOLOR=white>\n"
             @"<H1>%@</H1>\n"
             @"%@\n%@\n%@\n"
             @"</BODY></HTML>\n",
             desc, desc, detail, 
             BackToLogin(), Footer());

    printf("Content-type: text/html\r\n\r\n");
    printf("%s", [body cString]);

    exit(1);
}    
\end{lstlisting}

One error condition that occurs quite frequently is when we expected
an alphanumeric string and received something else, or that the length
of the string is not what we require. The function {\tt CheckForAlNum}
tests for these conditions. First it verifies that the string has the
right length and aborts with an error otherwise. Next it iterates over
the characters that make up the string, testing each character with
{\tt isalnum} in turn. If it does not get to the terminating null byte
then there was an illegal character. In this case it outputs a
descriptive error message.

\begin{lstlisting}{}
void CheckForAlNum(NSString *toCheck, NSString *desc,
                   int min, int max)
{
    int len = (toCheck==nil ? 0 : [toCheck length]);
    NSString *err, *aux;

    if(len<min || len>max){
        err = FMT3(@"%@ empty or too long; "
                   @"min %d, max %d characters.", 
                   desc, min, max);
        aux = FMT1(@"Got '%@'.", toCheck);
        ErrmsgAndExit(err, aux);
    }

    const char *ptr = [toCheck cString], *base = ptr;
    while(isalnum(*ptr++));
    if(*(ptr-1)){
        err = FMT1(@"Illegal character "
                   @"in %@; must be alphanumeric.", desc);
        aux = FMT2(@"Got '&#%d' "
                   @"at position %d.", 
                   (int)*(ptr-1), ptr-base);
        ErrmsgAndExit(err, aux);        
    }
}
\end{lstlisting}

We now meet one of the most important functions of the program,
i.e. {\tt OutputPageAndExit.} Recall that we store the session data in
the MySQL database. Whatever we do, the first step must be to read
those session data into the session dictionary if present, and the
last, to record the current contents of the dictionary. The read only
occurs once, i.e. at the beginning of the program. The update may be
invoked at different exit points of the program, which is why we put
it into the function {\tt OutputPageAndExit}.

There are two steps to this function: first, write the session data,
and second, output the page and exit. The first step starts by
building the MySQL query. It says to update the table {\tt session}
with the values from the session dictionary, which contains the
session data that we retrieved from MySQL. Only the time is not set
from a dictionary value. It is set to the current time, because that
is the time of the most recent activity in the session.

\begin{lstlisting}{}
void OutputPageAndExit(MYSQL mysql, NSMutableDictionary *session,
                       NSString *title, NSString *frames,
                       NSString *body, NSString *bodyargs)
{
    NSString *query = 
        FMT6(@"UPDATE session SET "
             @"time = '%lu', login='%@', refresh='%@', "
             @"chanid = '%@', open='%@' "
             @"WHERE sessid='%@'",
             (long)[[NSDate date] timeIntervalSince1970],
             [session objectForKey:@"login"],
             [session objectForKey:@"refresh"],
             [session objectForKey:@"chanid"],
             [session objectForKey:@"open"],
             [session objectForKey:@"sessid"]);
\end{lstlisting}

The end of the first step is to actually do the query and generate an
error message if it fails.

\begin{lstlisting}{}
    if(mysql_query(&mysql, [query cString])){
        NSString *err = 
            FMT1(@"Couldn't write session data: %@", query);
        ErrmsgAndExit(err, MYSQL_ERR);
    }
\end{lstlisting}

Step two is easy: output the page as described by the arguments. We
include the footer. The argument {\tt bodyargs} can be used to set
BODY properties, such as an ``onLoad'' that starts a timer or sets the
focus.

\begin{lstlisting}{}
    printf("Content-type: text/html\r\n");
    printf("Pragma: no-cache\r\n\r\n");

    printf("<HTML><HEAD><TITLE>%s</TITLE>\n%s</HEAD>\n",
           [title cString], [frames cString]);
    printf("<BODY%s>\n", [bodyargs cString]); 
    printf("%s", [body cString]);
    printf("%s\n", [Footer() cString]);
    printf("</BODY></HTML>\n");

    exit(0);
}
\end{lstlisting}

The function {\tt SetCookie} is as important as {\tt
OutputPageAndExit}. There are three cases where it must be used.
We must generate and output a new cookie if the script did not
receive one, which signals the start of a new session. We should
reset the expiry time of the cookie when the session is
restarted. Finally, the expiry time must also be updated when the user
enters a message, which indicates activity on the session.

The expiry time of the cookie should be {\tt SESSPERSIST} seconds from
now. We build the cookie dictionary with the expiry time, the path and
most importantly, the session id, convert it to a string, then to a C
string, and output the result.

\begin{lstlisting}{}
void SetCookie(NSString *sessid)
{
    NSDate *expDate =
        [NSDate dateWithTimeIntervalSinceNow:SESSPERSIST];
    
    NSMutableDictionary *cdict =
        [NSMutableDictionary
            dictionaryWithObjectsAndKeys:sessid, @"SESSID",
            expDate, @"expires", @CHATCGI, @"path", nil];
    
    printf([[cdict cookieToString] cString]);
}
\end{lstlisting}

It remains to discuss two auxiliary functions that encapsulate MySQL
queries. The first of these, {\tt FirstFieldsFromResult}, takes a
MySQL result consisting of some number of rows, of which we are only
interested in the first field. It produces an array of those
fields. It fetches the rows in turn, converts the C string in the
first field to an {\tt NSString} and stores the latter in an array,
which it returns when it has processed all the rows.

\begin{lstlisting}{}
NSMutableArray *FirstFieldsFromResult(MYSQL_RES *result)
{
    MYSQL_ROW row;
    NSMutableArray *data = 
        [NSMutableArray arrayWithCapacity:1];

    while((row=mysql_fetch_row(result))!=NULL){
        NSString *fstr = 
            [NSString stringWithCString:row[0]];
        [data addObject:fstr];
    }
    
    return data;
}
\end{lstlisting}

The second auxiliary function for use with MySQL is {\tt
RowsFromQuery}, which also processes rows resulting from a query. It
returns an array of dictionaries, one dictionary per row. The keys of
the dictionary are the names of the fields, and the values are the
actual data. First we make the query and signal any errors that may
have occurred.

\begin{lstlisting}{}
NSMutableArray *RowsFromQuery(MYSQL mysql, NSString *query)
{
    if(mysql_query(&mysql, [query cString])){
        NSString *err = FMT1(@"Query failed: %@", query);
        ErrmsgAndExit(err, MYSQL_ERR);
    }
\end{lstlisting}

Next we obtain the query result so that we may fetch the rows, again
checking for possible errors.

\begin{lstlisting}{}
    MYSQL_RES *result;
    if((result=mysql_use_result(&mysql))==NULL){
        NSString *err = FMT1(@"No results for query: %@", query);
        ErrmsgAndExit(err, MYSQL_ERR);
    }
\end{lstlisting}

We need the field names, which are the keys to the row dictionaries
and need only be fetched once. We obtain the number of fields and a
pointer to the fields themselves. We convert each field name to an
{\tt NSString} and add it to the array of field names. The name of an
element at position $k$ of a row is given by the name at position $k$
in the array of field names.

\begin{lstlisting}{}
    unsigned int ftotal = mysql_field_count(&mysql), fcur;
    MYSQL_FIELD *fields = mysql_fetch_fields(result);
    NSString *fstr;

    NSMutableArray
        *fnames = [NSMutableArray arrayWithCapacity:ftotal];
    for(fcur=0; fcur<ftotal; fcur++){
        fstr = [NSString stringWithCString:fields[fcur].name];
        [fnames addObject:fstr];
    }
\end{lstlisting}

We can process the rows now that we have the field names. We fetch the
total number of rows and initialize a mutable array having this
capacity. It will hold dictionaries.

\begin{lstlisting}{}
    MYSQL_ROW row;
    my_ulonglong rtotal = mysql_num_rows(result);

    NSMutableArray 
        *data = [NSMutableArray arrayWithCapacity:rtotal];
\end{lstlisting}

Now iterate over the rows. Create a dictionary with the right capacity
for each row. Then iterate over the fields, storing them as {\tt
NSString} objects. The key is obtained from the corresponding entry in
the array of field names. Add the row dictionary to the data array
once all the fields have been processed.

\begin{lstlisting}{}
    while((row=mysql_fetch_row(result))!=NULL){
        NSMutableDictionary *rowData =
            [NSMutableDictionary dictionaryWithCapacity:ftotal];
        for(fcur=0; fcur<ftotal; fcur++){
            fstr = [NSString stringWithCString:row[fcur]];
            [rowData setObject:fstr 
                     forKey:[fnames objectAtIndex:fcur]];
        }
        [data addObject:rowData];
    }
\end{lstlisting}

It remains to free the MySQL result and return the array of
dictionaries.

\begin{lstlisting}{}
    mysql_free_result(result);

    return data;

}
\end{lstlisting}

This is the end of the first section.

\subsubsection{Implementation II: {\tt main}}

We are now ready to discuss the function {\tt main}, which does the
actual work of generating the different screens and interacting with
the database. We declare commonly used variables that hold a MySQL
query, an error message and an error message description,
repectively. The first thing to do is to initialize the MySQL client
library and connect to the local host as user {\tt MYSQL\_USER}, with
the password {\tt MYSQL\_PASSWORD}. We send any errors to the client,
which is how all errors will be handled.

\begin{lstlisting}{}
int main(int argc, char** argv, char **env)
{
    NSAutoreleasePool *pool = [NSAutoreleasePool new];

    NSString *query, *err, *aux;
    MYSQL mysql;
    MYSQL_RES *result;

    mysql_init(&mysql);
    
    if(mysql_real_connect(&mysql,"localhost",
                          MYSQL_USER, MYSQL_PASSWORD,
                          NULL, 0, NULL, 0)==NULL){
        ErrmsgAndExit(@"MySQL connect failed", MYSQL_ERR);
    }
\end{lstlisting}

We ask for the list of databases that match the regular expression
{\tt CHATDB} and process any errors. We convert the result into an
array of database names and free the MySQL result.

\begin{lstlisting}{}
    if((result=mysql_list_dbs(&mysql, CHATDB))==NULL){
        ErrmsgAndExit(@"Couldn't list databases", MYSQL_ERR);
    }
    NSMutableArray *databases = FirstFieldsFromResult(result);
    mysql_free_result(result);
\end{lstlisting}

We have a problem if there was no database that matched the
pattern. We try to create it if this is the case and abort with an
error message if the create query failed.

\begin{lstlisting}{}
    if([databases containsObject:@CHATDB]==NO){
        query = [NSString stringWithFormat:@"CREATE DATABASE %s",
                          CHATDB];
        if(mysql_query(&mysql, [query cString])){
            err = FMT1(@"Couldn't create database: %s", CHATDB);
            ErrmsgAndExit(err, MYSQL_ERR);
        }
    }
\end{lstlisting}

We can select the chat database once we know that it exists.

\begin{lstlisting}{}
    if(mysql_select_db(&mysql, CHATDB)){
        err = FMT1(@"Couldn't select database: %s", CHATDB);
        ErrmsgAndExit(err, MYSQL_ERR);
    }
\end{lstlisting}

We need to verify that the three tables {\tt session}, {\tt channel}
and {\tt messages} are present. Hence we ask MySQL for a list of
tables. We may use {\tt FirstFieldsFromResult} because every row of
the result contains exactly one field.

\begin{lstlisting}{}
    if((result=mysql_list_tables(&mysql, NULL))==NULL){
        ErrmsgAndExit(@"Couldn't list tables", MYSQL_ERR);
    }
    NSMutableArray *tables = FirstFieldsFromResult(result);
    mysql_free_result(result);
\end{lstlisting}

What follows is a very important declaration. We declare an array that
holds the names of the tables that must be present, and a second array
containing {\tt CREATE} statements for each table for use if the table
is not present. The third array holds the maximum age of an entry for
each table. These three arrays use the same order.

\begin{lstlisting}{}
    NSString *tnames[] = { @"session", @"channels", @"messages", nil };
    NSString *tcreate[] = {
        FMT2(@"CREATE TABLE session ("
             @"sessid char(%d) NOT NULL,"
             @"time BIGINT,"
             @"login char(%d),"
             @"chanid BIGINT,"
             @"refresh INT,"
             @"open ENUM('Y', 'N'),"
             @"UNIQUE KEY (sessid),"
             @"PRIMARY KEY (sessid)"
             @")", LEN_SESSID, LEN_LOGIN),
        FMT2(@"CREATE TABLE channels ("
             @"id BIGINT NOT NULL AUTO_INCREMENT,"
             @"time BIGINT NOT NULL,"
             @"name char(%d),"
             @"password char(%d) BINARY,"
             @"open ENUM('Y', 'N'),"
             @"PRIMARY KEY (id)"
             @")", LEN_CHANNEL, LEN_PASSWORD),
        FMT1(@"CREATE TABLE messages ("
             @"time BIGINT NOT NULL,"
             @"login char(%d),"
             @"chanid BIGINT,"
             @"msg BLOB,"
             @"PRIMARY KEY (time)"
             @")", LEN_LOGIN)
    };
    long persists[] = {
        SESSPERSIST,
        CHANPERSIST,
        MSGPERSIST
    };
\end{lstlisting}

We iterate over these three arrays in parallel. If a table is not
present in the list of tables that we obtained from MySQL, then it
must be created, which we immediately try to do, checking for errors
as we go.

\begin{lstlisting}{}
    int tindex = 0;

    while(tnames[tindex]!=nil){
        if([tables containsObject:tnames[tindex]]==NO){
            if(mysql_query(&mysql, [tcreate[tindex] cString])){
                err = FMT1(@"Query failed: %@", tcreate[tindex]);
                ErrmsgAndExit(err, MYSQL_ERR);
            }
        }
\end{lstlisting}

The second half of the loop does what we may call house-keeping. It
erases records that are too old. Errors are sent to the client. (We
probably should have put the {\tt DELETE} in an {\em else} clause,
because there is nothing to delete from a new table, although it can
do no harm.)

\begin{lstlisting}{}
        long now =
            [[NSDate date] timeIntervalSince1970];
        query = FMT2(@"DELETE FROM %@ WHERE time<%ld", 
                     tnames[tindex], now-persists[tindex]);
        if(mysql_query(&mysql, [query cString])){
            err = FMT1(@"Delete failed: %@", query);
            ErrmsgAndExit(err, MYSQL_ERR);
        }
        tindex++;
    }
\end{lstlisting}

We are now ready to do some actual processing. We parse any GET or
POST variables that were passed to the script. We also parse the
cookie, if there was one, and look up the session id in the cookie
dictionary.

\begin{lstlisting}{}
    NSMutableDictionary *params =
        [[NSMutableDictionary alloc] initWithCGI];    
    NSString *cmd = [params objectForKey:@"cmd"];

    NSMutableDictionary *cvals = 
        [[NSMutableDictionary alloc] initWithCookie];
    NSString *sessid = [cvals objectForKey:@"SESSID"];
\end{lstlisting}

If there was no session id in the cookie then we are either starting a
new session or we are being called to display a portion of the main
screen, but the cookie has expired. We send an explanatory error
message in the latter case.

\begin{lstlisting}{}
    if(sessid==nil){
        if(cmd!=nil &&
           [cmd isEqualToString:@"display"]==YES ||
           [cmd isEqualToString:@"enter"]==YES){
            err = FMT1(@"Session timed out, date is %@.",
                       [[NSDate date] description]);
            aux = FMT1(@"Command was: %@", cmd);
            ErrmsgAndExit(err, aux);
        }
\end{lstlisting}

If there was no timeout but there is no session id, then we must
generate and initialize a new one that should preferably not be easily
guessable, so that no user can join someone else's session by guessing
an actual value of a session id that is in use and sending it as a
cookie. We seed the random number generator and generate each
character of the key in turn, asking for a random number from the
interval $[0, 26+26+10-1]$. Values from $[0, 26-1]$ yield lower case
letters, values from $[26, 26+26-1]$, upper case letters and values
from $[26+26, 26+26+10-1]$, digits. We append the character obtained
in this manner to the session id.

\begin{lstlisting}{}
        int pos;

        srand48(time(NULL));
        sessid = @"";

        for(pos=0; pos<LEN_SESSID; pos++){
            char item;
            int choice = lrand48() % (26+26+10);
            item = (choice < 26 ? 'a'+choice :
                    (choice< 52 ? 'A'+choice-26 :
                     '0'+choice-26-26));
            sessid = 
                [sessid stringByAppendingFormat:@"%c", item];
        }
\end{lstlisting}

But it is not enough merely to generate the key, we must also store it
in the sessions table and initialize the remaining fields of the
session. We use an {\tt INSERT} query for this purpose. It stores the
session id and the current time, leaving the other fields
empty. Errors are sent to the client; e.g. the query will fail in the
admittedly unlikely case that we generated a session id that is
already in the table, because we declared the corresponding field to
be unique. We are done initializing once we output the cookie for
storage in the client with the session id using {\tt Set-Cookie}.

\begin{lstlisting}{}
        query = FMT2(@"INSERT INTO session (sessid, time) "
                     @"VALUES ('%@', '%ld')",
                     sessid, 
                     (long)[[NSDate date] timeIntervalSince1970]);
        if(mysql_query(&mysql, [query cString])){
            err = FMT1(@"Couldn't initialize session: %@"
                       @"<BR>Try again.", query);
            ErrmsgAndExit(err, MYSQL_ERR);
        }

        SetCookie(sessid);
    }
\end{lstlisting}

We may now be certain that we have a valid session id, even if it has
only just been generated. Hence we may read the session data into a
dictionary. This is done with a {\tt SELECT} query that selects all
fields from those records that match the session id. There should be
exactly one such record; everything else is an error. We extract the
session dictionary.

\begin{lstlisting}{}
    NSString *title, *frames, *body, *bodyargs;

    query = FMT1(@"SELECT * FROM session WHERE sessid='%@'", 
                 sessid);
    NSMutableArray *sessdata = RowsFromQuery(mysql, query);
    if([sessdata count]!=1){
        err = FMT2(@"Found %d entries for %@.",
                   [sessdata count], sessid);
        ErrmsgAndExit(err, query);
    }
    NSMutableDictionary *session = [sessdata objectAtIndex:0];
\end{lstlisting}

We must respond to the command ``restart'' before we do any additional
processing. We clear all fields in the session dictionary and output
the cookie, which will now last an additional {\tt SESSPERSIST}
seconds. Remember that the session time stamp will be set by {\tt
OutputPageAndExit}.

\begin{lstlisting}{}
    if([cmd isEqualToString:@"restart"]){
        [session setObject:@"" forKey:@"login"];
        [session setObject:@"" forKey:@"refresh"];
        [session setObject:@"" forKey:@"open"];
        [session setObject:@"" forKey:@"chanid"];
        SetCookie(sessid);
    }
\end{lstlisting}

We have now come to the point where we output the first of our several
screens, which is the login screen. We output the login form if there
is no user name in the session dictionary and we did not receive one
via a GET or POST. The form contains a table with three rows,
i.e. name, refresh and session type. There is a submit button.

\begin{lstlisting}{}
    if(Empty([session objectForKey:@"login"]) &&
       Empty([params objectForKey:@"login"])){
        title = @"Chat login";
        frames = @"";
        body = @"<FONT SIZE=24><B>Login</B></FONT>\n"
            @"<FORM METHOD=POST ACTION=webchat.sh>\n"
            @"<TABLE>\n"
            @"<TR><TD>Name:</TD>\n"
            @"<TD><INPUT TYPE=TEXT NAME=login>\n"
            @"</TD></TR>\n"
            @"<TR><TD>Refresh (in s)</TD>\n"
            @"<TD>\n"
            @"<INPUT TYPE=TEXT NAME=refresh>\n"
            @"</TD></TR>\n"
            @"<TR><TD>Session type:</TD>\n"
            @"<TD>\n"
            @"<SELECT NAME=open>\n"
            @"<OPTION SELECTED VALUE=Y>  open </OPTION>\n"
            @"<OPTION VALUE=N> private </OPTION>\n"
            @"</SELECT>\n"
            @"</TD></TR>\n"
            @"</TABLE>\n"
            @"<INPUT TYPE=SUBMIT VALUE=Continue>\n"
            @"</FORM>\n";
        bodyargs = @"";
        OutputPageAndExit(mysql, session, 
                          title, frames, body, bodyargs);
    }
\end{lstlisting}

The remaining screens require a time stamp, so we set it here for use
in the rest of the program.

\begin{lstlisting}{}
    long now =
        [[NSDate date] timeIntervalSince1970];
\end{lstlisting}

The first case was an empty session and no CGI variables, which lead
to the login screen. The second case is an empty session and the
appropriate CGI variables. We set the flag {\tt readParams} in this
case and copy the variables from the CGI dictionary to the session
dictionary.

\begin{lstlisting}{}
    BOOL readParams = NO;
    if(Empty([session objectForKey:@"login"])){
        readParams = YES;
        [session setObject:[params objectForKey:@"login"]
                 forKey:@"login"];
        [session setObject:[params objectForKey:@"refresh"]
                 forKey:@"refresh"];
        [session setObject:[params objectForKey:@"open"]
                 forKey:@"open"];
    }
\end{lstlisting}

We may now read the settings that correspond to the data obtained from
the login screen.

\begin{lstlisting}{}
    NSString *login = [session objectForKey:@"login"];
    int refresh = [[session objectForKey:@"refresh"] intValue];
    char open = ([[session objectForKey:@"open"] 
                     isEqualToString:@"Y"] ? 'Y' : 'N');
\end{lstlisting}

There is error checking to do if we obtained those settings from the
CGI dictionary. We verify that the login name is not empty and
consists of alphanumeric characters only. The refresh should be set to
a positive interval.

\begin{lstlisting}{}
    if(readParams==YES){
        CheckForAlNum(login, @"Login", 1, LEN_LOGIN);

        if(refresh<1){
            err = @"Refresh must be positive.";
            aux = FMT1(@"Got '%d'.", refresh);
            ErrmsgAndExit(err, aux);        
        }
    }
\end{lstlisting}

We now have the data that correspond to the login screen. The channel
screen is next. We generate this screen if the channel id has not been
set and is not among the CGI parameters.

\begin{lstlisting}{}
    NSString *chanid = [session objectForKey:@"chanid"];
    int numericCID = [chanid intValue];

    if(!numericCID && Empty([params objectForKey:@"channel"])){
\end{lstlisting}

We will generate the output line by line or item per item in all cases
that follow and only join those lines when everything has been
generated. We do not send HTML as it is created. This is so that there
is no mixing between regular output and error messages.

There are two cases that correspond to open and private
channels. Both contain forms. They have in common that the first form
contains a text input where the name of the channel may be
entered. This can be used to select an existing channel or to create a
new one.

\begin{lstlisting}{}
        NSString *line;
        NSMutableArray *lines =
            [NSMutableArray arrayWithCapacity:8];

        title = @"Chat login: select or start channel";
        body = @"<FONT SIZE=24>\n"
            @"<B>Login: select or start channel</B>\n"
            @"</FONT>\n"
            @"<FORM METHOD=POST ACTION=webchat.sh>\n"
            @"Channel: <INPUT TYPE=TEXT NAME=channel>\n";
\end{lstlisting}

Now is where the two cases (open vs. private) diverge. We output a
password field when the user has selected private channels. The submit
button is again common to both cases.

\begin{lstlisting}{}
        if(open=='N'){
            line = @"Password: <INPUT TYPE=PASSWORD NAME=password>";
            [lines addObject:line];
        }
        line = @"<INPUT TYPE=SUBMIT VALUE=Enter>\n</FORM>";
        [lines addObject:line];
\end{lstlisting}

We wish to present a menu of currently open channels when the user has
selected open channels. We find those channels with a {\tt SELECT}
query. We group the messages by channel id and select the channel
name, the number of messages and the newest time stamp from the table
{\tt channels}, which is updated every time a message is recorded. The
where clause binds the channel name from the table {\tt channels} to
the channel id from the table {\tt messages}. The result is a set of
rows with three fields, that hold the channel name, the number of
messages for the channel, and the newest message time stamp for the
channel. We must check the {\tt open} property so that our menu will
not display private channels. We let MySQL do the sorting. The result
will list channels according to their time stamps, with the channel
that most recently had any activity first.

\begin{lstlisting}{}
        if(open=='Y'){
            query = @"SELECT "
                @"name, "
                @"COUNT(chanid), "
                @"channels.time "
                @"FROM channels, messages "
                @"WHERE channels.id=messages.chanid "
                @"AND open='Y' "
                @"GROUP BY messages.chanid "
                @"ORDER BY channels.time DESC";
            NSMutableArray *chanData = 
                RowsFromQuery(mysql, query);
            NSEnumerator *chanEnum =
                [chanData objectEnumerator];
            NSMutableDictionary *channel;
\end{lstlisting}

We output the channel menu as a table, iterating over the channels
with the enumerator.

\begin{lstlisting}{}
            line = @"<TABLE BORDER BGCOLOR=white>\n";
            [lines addObject:line];

            while((channel = [chanEnum nextObject])!=nil){
\end{lstlisting}

Every entry in the menu is a form that contains a single button, which
selects the respective channel. We must convert the time stamp into a
readable date format, which we do with {\tt NSDate}'s {\tt
description.} The number of messages is given by the appropriate field
from the query.

\begin{lstlisting}{}
                [lines addObject:
                           @"<FORM METHOD=POST ACTION=webchat.sh"];

                long tval = 
                    atol([[channel objectForKey:@"time"]
                             cString]);
                NSString *dateStr =
                    [[NSDate dateWithTimeIntervalSince1970:tval]
                        description];
                NSString *count =
                    [channel 
                        objectForKey:@"COUNT(chanid)"];
\end{lstlisting}

An entry in the menu has three fields: the submit button, the time
stamp, and the number of messages. The submit button is labeled with
the name of the channel. This completes the form for the current
entry, which we also could have put inside a single ``TD'' item rather
than wrapping the entire row in the form.

\begin{lstlisting}{}
                line = FMT4(@"<TR><TD>\n"
                            @"<INPUT TYPE=SUBMIT "
                            @"NAME=channel "
                            @"VALUE=%@></TD>\n<TD>"
                            @"%@</TD>\n<TD> %@ message%@ "
                            @"</TD></TR>",
                            [channel objectForKey:@"name"],
                            dateStr, count,
                            ([count isEqualToString:@"1"]==NO ?
                             @"s" : @""));
                [lines addObject:line];

                [lines addObject:@"</FORM>"];
            }
\end{lstlisting}

The special case of the open chanel menu ends with a closing tag for
the table. The last step is to output everything and exit the
program. This step is again common to open and private channels.

\begin{lstlisting}{}
            line = @"</TABLE>\n";
            [lines addObject:line];
        }

        frames = @"";
        body = 
            [body stringByAppendingString:
                      [lines componentsJoinedByString:@"\n"]];
        bodyargs = @"";
        OutputPageAndExit(mysql, session,
                          title, frames, body, bodyargs);
    }
\end{lstlisting}

If we have got this far, but do not have a channel id, then the user
must have submitted a channel name and possibly a password using
precisely the form whose generation we just described. Hence we
extract the channel name and the password form the dictionary of CGI
parameters. There are two cases: either the channel exists already,
and we select it, otherwise, we create a new channel and select
it. (We pre-declare some variables that will hold the dictionary for
the current channel and its name.)

\begin{lstlisting}{}
    NSMutableArray *chanLookup;
    int chanCount; 
    NSMutableDictionary *chanRecord;
    NSString *channel;

    if(!numericCID){
        NSString *candidate = [params objectForKey:@"channel"];
        NSString *password = [params objectForKey:@"password"];
\end{lstlisting}

We will only accept channel names and passwords that are alphanumeric
and have the right length.

\begin{lstlisting}{}
        CheckForAlNum(candidate, @"Channel", 4, LEN_CHANNEL);
        if(open=='N'){
            CheckForAlNum(password, @"Password", 4, LEN_PASSWORD);
        }
\end{lstlisting}

We need to know whether this is a new channel or not, so we send the
appropriate query to MySQL, using the channel name to select matching
channels. We have a duplicate channel if we get more than one record
with this name. (This should not happen.)

\begin{lstlisting}{}
        query = [NSString stringWithFormat:@"SELECT "
                          @"id, open, name "
                          @"FROM channels WHERE name='%@' ",
                          candidate];
        
        chanLookup = RowsFromQuery(mysql, query);
        chanCount = [chanLookup count];

        if(chanCount>1){
            err = FMT1(@"Duplicate channel: %@.", candidate);
            aux = FMT1(@"Count was %d.", chanCount);
            ErrmsgAndExit(err, aux);
        }
\end{lstlisting}

If we obtained a single record, then the user is trying to join an
existing channel, in which case we must verify that the user's choice
of an open or private channel matches the setting in the record for
the selected channel and signal an error otherwise.

\begin{lstlisting}{}
        else if(chanCount==1){
            chanRecord = [chanLookup objectAtIndex:0];

            if([[chanRecord objectForKey:@"open"]
                   isEqualToString:(open=='Y' ? @"Y" : @"N")]==NO){
                err = FMT1(@"Channel is not %@.",
                           (open=='Y' ? @"open" : @"private"));
                ErrmsgAndExit(err, candidate);
            }
            
            channel = [chanRecord objectForKey:@"name"];
            chanid = [chanRecord objectForKey:@"id"];
        }
\end{lstlisting}

We have a new channel if we did not obtain any records from the {\tt
SELECT} query. We build an {\tt INSERT} query that contains the time
stamp, the name, the open flag and the encrypted password. (The salt
will be stored in the first two characters of the output from {\tt
ENCRYPT}.)

\begin{lstlisting}{}
        else{
            query = FMT4(@"INSERT INTO "
                         @"channels "
                         @"(id, time, name, password, open) "
                         @"VALUES ('', '%ld', '%@', "
                         @"ENCRYPT('%s'), '%c')",
                         now, candidate, 
                         (Empty(password)==YES ? 
                          "" : [password cString]), open);
\end{lstlisting}

The channel was created if the query succeeded. We can set the channel
name to the candidate value. We must ask MySQL for the channel id that
it created. (Recall that {\tt AUTO\_INCREMENT} was set.)

\begin{lstlisting}{}
            if(mysql_query(&mysql, [query cString])){
                err = FMT1(@"Couldn't create channel: %@",
                           candidate);
                ErrmsgAndExit(err, MYSQL_ERR);
            }
            
            channel = candidate;
            chanid = FMT1(@"%ld", (long)mysql_insert_id(&mysql));
        }
\end{lstlisting}

At this point we can be certain of having a valid channel id and a
channel name. It remains to check the password in the case of a logon
to a private channel that wasn't just created ({\tt chanCount==1}). We
ask MySQL for the id of any channel whose encrypted password matches
the result of encrypting the user-supplied password, with the first
two characters of the stored password being the salt.

\begin{lstlisting}{}
        if(open=='N' && chanCount==1){
            query = FMT2(@"SELECT "
                         @"id FROM channels "
                         @"WHERE id = '%@' AND "
                         @"password = "
                         @"ENCRYPT('%@', LEFT(password, 2))",
                         chanid, password);
\end{lstlisting}

The query must have succeeded and there must be a result set for us to
process if we are to proceed. We could have used {\tt RowsFromQuery},
except that we are not interested in the particular record, but rather
in the number of records. There should be a single record.

\begin{lstlisting}{}
            if(mysql_query(&mysql, [query cString])){
                err = FMT1(@"Password query failed: %@", query);
                ErrmsgAndExit(err, MYSQL_ERR);
            }
            if((result=mysql_store_result(&mysql))==NULL){
                err = FMT1(@"No result from password query: %@",
                           query);
                ErrmsgAndExit(err, MYSQL_ERR);
            }
\end{lstlisting}

We obtain the number of rows. There will be no rows if the password
was wrong and one row otherwise. We send an error (``Permission
denied.'') to the client if that was the case. This concludes the
processing associated to channel selection.

\begin{lstlisting}{}
            int rescount = (int)mysql_num_rows(result);
            if(rescount!=1){
                aux = FMT6(@"User was %@, "
                           @"channel %@, id %@,"
                           @"password %@; "
                           @"combo gave %d entries.<P>"
                           @"query was: %@",
                           login, channel, chanid, 
                           password, rescount, query);
                ErrmsgAndExit(@"Permission denied.", aux);
            }
            mysql_free_result(result);
        }
\end{lstlisting}

The following {\tt else} clause deals with the case where we had a
valid channel id in the session dictionary. We need the channel's name
rather than its id for the display frame, which should not only
display messages, but also indicate what user is viewing what
channel. Hence we build a query that will yield the channel's name and
execute it.

\begin{lstlisting}{}
    else{
        query = FMT1(@"SELECT name FROM channels WHERE id='%@'", 
                     chanid);
        chanLookup = RowsFromQuery(mysql, query);
        chanCount = [chanLookup count];
\end{lstlisting}

There is something wrong if we obtained no entry or more than one
entry. We set the channel name if everything was okay.

\begin{lstlisting}{}
        if(chanCount!=1){
            err = FMT1(@"Expected one channel for ID %@.", 
                       chanid);
            aux = FMT1(@"Got %d.", chanCount);
            ErrmsgAndExit(err, aux);
        }

        channel = [[chanLookup objectAtIndex:0] 
                      objectForKey:@"name"];
    }
\end{lstlisting}

If we got this far in the program, then the session must already be
fully initialized: we have a user name, a refresh setting, an ``open''
flag and a channel id and a channel name. We should output either the
main chat screen (frame set), the upper frame (message display) or the
lower frame (message entry form). What exactly we should do is
determined by the parameter ``cmd.'' It is either ``display'' or
``enter'' or nil. Anything else is an error. We start by responding to
the display command. We select the message text and the author's name
from the database. We again let MySQL do the sorting.

\begin{lstlisting}{}
    if(cmd!=nil){
        if([cmd isEqualToString:@"display"]==YES){
            query = FMT2(@"SELECT msg, login "
                         @"FROM messages "
                         @"WHERE chanid='%@' "
                         @"ORDER BY time "
                         @"DESC LIMIT %d",
                         chanid,  DISPLAY_MAX);
            NSMutableArray *msgs = RowsFromQuery(mysql, query);
\end{lstlisting}

We emit the refresh header right away. This is safe to do because we
know that there cannot be any errors after this point. We initialize
the parameters for {\tt OutputPageAndExit}. The first line of the body
displays the current user and the channel name. We set the ``onLoad''
event property to a Javascript timeout, which is in milliseconds. This
is to be on the safe side; theoretically, either the refresh header or
the timeout alone should suffice.

\begin{lstlisting}{}
            printf("Refresh: %d\r\n", refresh);
            title = @"Chat message";
            frames = @"";

            body = FMT2(@"<FONT SIZE=24>\n"
                        @"<B>User: %@ &nbsp; Channel: %@</B>\n"
                        @"</FONT>\n<P>\n", login, channel);
            bodyargs = FMT1(@" onLoad=\"javascript:"
                            @"window.setTimeout("
                            @"'location.reload()', %ld)\"",
                            (long)(1000)*(long)refresh);
\end{lstlisting}

The messages go into a HTML table. We build the table line by line,
storing lines in the array {\tt msgOutput}. We obtain an iterator for
the set of messages and loop over it.

\begin{lstlisting}{}
            NSMutableArray *msgOutput =
                [NSMutableArray 
                    arrayWithObject:
                        @"<TABLE WIDTH=100% BORDER>"];
            NSEnumerator *msgEnum = [msgs objectEnumerator];
            NSMutableDictionary *item;
            while((item = [msgEnum nextObject])!=nil){
\end{lstlisting}

The content of the message as retrieved from MySQL consists of a
string of hexdecimal values, which we decode into HTML entities. There
is one row with two columns for each message. The first column holds
the name of the author and the second the message itself. We store the
string for the current row in the output array.

\begin{lstlisting}{}
                NSString *encoded = [item objectForKey:@"msg"],
                    *entities = DecodeMsgIntoEntities(encoded);
                NSString *row =
                    FMT2(@"<TR><TD WIDTH=20%>"
                         @"%@</TD><TD WIDTH=80%>%@</TR>",
                         [item objectForKey:@"login"],
                         entities);
                [msgOutput addObject:row];
            }
\end{lstlisting}

We close the table once we have iterated over all messages and produce
a single string by joining the output lines, separating them from each
other with a newline character. We are now done processing the
``display'' command and may output the page.

\begin{lstlisting}{}
            [msgOutput addObject:@"</TABLE>"];

            NSString *allmsgs = 
                [msgOutput componentsJoinedByString:@"\n"];
            body = [body stringByAppendingString:allmsgs];

            OutputPageAndExit(mysql, session, 
                              title, frames, 
                              body, bodyargs);
        }
\end{lstlisting}

The second command that we must implement is the command ``enter,''
which records a message and update's the channel's time stamp. There
are two steps. If there is a message among the CGI parameters, then we
must record it. We must also output a message submit form thereafter,
regardless of whether there was a message or not.

In case of a message we encode it as a string of hexadecimal values
first. Then we build the query, which records the time, the author,
the channel id and the text of the message.

\begin{lstlisting}{}
        else if([cmd isEqualToString:@"enter"]==YES){
            NSString *msg = [params objectForKey:@"msg"];
            if(!Empty(msg)){
                NSString *encoded = EncodeMsg(msg);
                query = FMT4(@"INSERT "
                             @"INTO messages "
                             @"(time, login, chanid, msg) "
                             @"VALUES"
                             @"('%ld', '%@', '%@', '%@')",
                             now, login, chanid, encoded);
\end{lstlisting}

The query must succeed or we output an error message.

\begin{lstlisting}{}
                if(mysql_query(&mysql, [query cString])){
                    err = FMT1(@"Couldn't insert message: %@",
                               DecodeMsgIntoEntities(encoded));
                    ErrmsgAndExit(err, MYSQL_ERR);
                }
\end{lstlisting}

The process of recording a message includes an update of the channel's
time stamp to indicate that there has been activity on the
channel. Recall that this time stamps determines how long the channel
will stay in the database and where on the channel menu it will be
displayed if it is an open channel. The query is simple: enter the
time stamp in the record with the appropriate channel id.

\begin{lstlisting}{}
                query = FMT2(@"UPDATE channels "
                             @"SET time='%ld'"
                             @"WHERE id='%@'", now, chanid);
\end{lstlisting}

This query, too, must succeed, or we signal an error.

\begin{lstlisting}{}
                if(mysql_query(&mysql, [query cString])){
                    NSString *err = 
                        FMT2(@"Couldn't update "
                             @"channel: %@, query %@", 
                             channel, query);
                    ErrmsgAndExit(err, MYSQL_ERR);
                }
            }
\end{lstlisting}

Step two of the response to the command ``enter'' is to output the
message entry form. This is mostly static text and includes the button
that takes the user back to the login screen. This button should not
be missing from the main screen. It remains to point out the
Javascript focus call, which makes it possible for a user to send a
sequence of messages without having to click in the message field
every time. There are three fields: the message, a hidden field which
sets the command to ``enter'' and a submit button.

\begin{lstlisting}{}
            title = @"Chat message";
            frames = @"";
            body = FMT1(@"<FONT SIZE=24>"
                        @"<B>Message</B></FONT>\n"
                        @"<FORM NAME=enter METHOD=POST "
                        @"ACTION=webchat.sh>\n"
                        @"<INPUT TYPE=TEXT NAME=msg SIZE=80 "
                        @"MAXLENGTH=%d>\n"
                        @"<INPUT TYPE=HIDDEN NAME=cmd "
                        @"VALUE=enter>\n"
                        @"<INPUT TYPE=SUBMIT VALUE=Submit>\n"
                        @"</FORM>\n", LEN_MSG);
            body = [body stringByAppendingString:BackToLogin()];
            bodyargs = @" onLoad="
                @"\"javascript:document.enter.msg.focus();\"";
\end{lstlisting}

We must reset the cookie's expiry date because an enter signals that
there has been activity for the current session. We invoke {\tt
SetCookie} for this purpose, and conclude by outputting the page.

\begin{lstlisting}{}
            SetCookie(sessid);
            OutputPageAndExit(mysql, session, 
                              title, frames, body, bodyargs);
        }
\end{lstlisting}

We have now responded to all possible commands. Any other command is
an error.

\begin{lstlisting}{}
        else{
            err = @"Unknown command.";
            aux = [NSString stringWithFormat:@"Got %@.",
                            cmd];
            ErrmsgAndExit(err, aux);
        }
    }
\end{lstlisting}

The final section of the program is for the case when all session
variables were set, but no command was received. This is the default
case and it indicates that we should display the main screen with its
two frames. The frame set allocates 65\% of the browser's height to
the upper frame, where messages are displayed, and 35\% to the lower
frame, where messages are entered. The request method by which
commands are transmitted is GET. We can output the page once the
arguments for {\tt OutputPageAndExit} have been initialized. The exit
statement at the end of the program is not reached.

\begin{lstlisting}{}
    title = FMT2(@"Chat user: %@ Channel: %@", 
                 login, channel);
    frames = @"<FRAMESET ROWS=\"65%, 35%\">\n"
        @"<FRAME SRC=webchat.sh?cmd=display "
        @"SCROLLING=no NAME=display>\n"
        @"<FRAME SRC=webchat.sh?cmd=enter "
        @"SCROLLING=no NAME=enter>\n"
        @"</FRAMESET>\n"
        @"<NOFRAMES>\n"
        @"This software needs a browser "
        @"that can display frames.\n"
        @"</NOFRAMES>\n";
    body = @"";
    bodyargs = @"";
    OutputPageAndExit(mysql, session, 
                      title, frames, 
                      body, bodyargs);

    // not reached
    [pool release];
    exit(0);
}
\end{lstlisting}

This concludes the webchat recipe. It is based on a PHP webchat that I
wrote some years ago, which in turn was inspired by a script I
received from Igor Gilitschenski in July 2000.
