/* Maintain server configuration files though HTML forms.
 * The script coordinates the configuration (.conf) file with an edit form via
 * use of a form template (.template) file along with special directives
 * in the configuration file (.DEFINE, .EXPAND, etc).
 *
 * Author:	David Jones
 * Date:	2-JUL-1996
 * Revised:	7-AUG-1996	fix bugs in collect_symbols() and .iterate
 *				expansion for .authorize list.
 * Revised:	14-AUG-1996	More bugs in .iterate expansion
 * Revised:	16-AUG-1996	Inhibit ":80" port, some browsers treat
 *				host and host:80 and independant realms.
 * Revised:	17-AUG-1996	Allow .authorize rule to specify remote
 *				user of '*'.
 * Revised:	13-JAN-1997	Attempt to fix problem with extra blank lines
 *				inserted when no .next's accompany .iterate.
 *				Support continuation lines in rule file
 *				(last character on line is backslash).
 * Revised:	19-JUL-1997	Allow double-quotes in form values, replace
 *				with "''" in displayed forms to get around
 *				bugs in browser support for &quote;
 * Revised:	22-FEB-1998	Add reflex hack support.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <stat.h>
#include <time.h>
#include "cgilib.h"

struct field_def { struct field_def *next; char *key, *value; int klen; };
typedef struct field_def *field_def_p;
static char *entify_string ( char *source );

static int output_started;
static void error_abort ( char *stsline, char *reason, void *arg )
{
    char fmt[256];
    if ( !output_started ) output_started = cgi_begin_output(1); 
    printf("error abort: '%s' '%s'\n", stsline, reason );
    sprintf ( fmt, "content-type: text/plain\nstatus: %s\n\nFailure: %s", 
	stsline, reason );
    cgi_printf ( fmt, arg, arg );
    exit ( 1 );
}

static char *url_host ( )
{
    /*
     * Return port number string and delimiter for building redirects.
     */
    static char buffer[300];
    char *port, *host;
    int length, total;
    /*
     * If port number is 80, just return host name.
     */
    port = cgi_info ( "SERVER_PORT" );
    host = cgi_info ( "SERVER_NAME" );
    if ( !port ) port = "80";
    if ( strcmp ( port, "80" ) == 0 ) return host;
    /*
     * construct host:port in static storage.
     */
    length = strlen ( host );
    total = length + 2 + strlen(port);
    if ( total > sizeof(buffer) ) return host;
    strcpy ( buffer, host );
    buffer[length++] = ':';
    strcpy ( &buffer[length], port );
    return buffer;
}

struct file_rec {
    struct file_rec *next;
    struct file_rec *related;
    int length, type;
    char s[1];		/* string */
};
typedef struct file_rec *file_recp;

struct symdef {
    file_recp source;
    char *symbol;
    char *value;
};

struct rec_group {
    char *tag;
    file_recp first;
    file_recp last;
    int count;
};

static struct file_rec rule_file, form_file;
static time_t rule_file_mdate;
static struct rec_group file_dir[] = {
	{ "ALL", &rule_file, &rule_file, 0 },			/* 0 */
	{ ".DEFINE", (file_recp) 0, (file_recp) 0, 0 },		/* 1 */
	{ ".EXPAND", (file_recp) 0, (file_recp) 0, 0 },		/* 2 */
	{ ".IGNORE", (file_recp) 0, (file_recp) 0, 0 },		/* 3 */
	{ ".ITERATE", (file_recp) 0, (file_recp) 0, 0 },	/* 4 */
	{ ".NEXT", (file_recp) 0, (file_recp) 0, 0 },		/* 5 */
	{ ".AUTHORIZE", (file_recp) 0, (file_recp) 0, 0 },	/* 6 */
	{ ".FORM",   (file_recp) 0, (file_recp) 0, 0 },		/* 7 */
	{ (char *) 0, (file_recp) 0, (file_recp) 0, 0 } };

static void authenticate_user ( struct file_rec *auth_recs, char *fname );
static char *load_configuration ( char *method, char *path_info, char **mancmd );
static char *load_template ( char *fname );
static void generate_form(char *, char*);
static void update_configuration(char *);
extern void server_query ( char * );
static int expand_form_tag ( char *tag, char *, int tag_length, struct symdef *  );
/**************************************************************************/
int main ( int argc, char **argv )
{
    int i, status, manage_flag, SYS$SETEF();
    char *path_info, *fname, *method, *query, *manage_cmd;
    /*
     * Use low-level cgi_init to allow additional scriptserver transactions.
     * (will call cgi_begin_output(1) when ready to generate CGI output.
     */
    
    status = cgi_init_env ( argc, argv );
    if ( (status&1) == 0 ) return status;
    output_started = 0;
    /*
     * Break out configfile name and mtime from path_info.
     */
    method = cgi_info ( "REQUEST_METHOD" );
    path_info = cgi_info ( "PATH_INFO" );
    if ( !path_info ) error_abort ( "500 no path", "No path", 0 );
    fname = load_configuration ( method, path_info, &manage_cmd );
    /*
     * authorize access.
     */
    if ( file_dir[4].count > 0 ) {
	/*
	 * Test .iterate records for .authorize expansion
	 */
	char iter_line[32];
	file_recp rec, tmp;
	for ( rec = file_dir[4].first; rec; rec = rec->related ) {
	    if ( rec->length < 20 ) continue;
	    for ( i = 0; i < 20; i++ ) 	iter_line[i] = toupper(rec->s[i]);
	    if ( strncmp ( iter_line, ".ITERATE .AUTHORIZE ", 20 ) == 0 ) {
		/* Append .next records for iterate to .authorize list */
		for ( rec = rec->next; rec; rec = rec->next ) {
		    if ( rec->type == 4 ) break;	/* next .iterate */
		    if ( rec->type == 5 ) {
			/*
			 * Record is of type .next, reformat as new .authorize
			 * record.
			 */
			tmp = malloc ( sizeof(struct file_rec)+rec->length+6 );
			tmp->next = tmp->related = (file_recp) 0;
			tmp->type = 4;
			tmp->length = rec->length + 5;
			strcpy ( tmp->s, ".AUTHORIZE" );
			strncpy ( &tmp->s[10], &rec->s[5], rec->length-5 );
			tmp->s[tmp->length] = '\0';
			
			if ( file_dir[6].count == 0 ) {
			     file_dir[6].first = file_dir[6].last = tmp;
			} else {
			    file_dir[6].last->related = tmp;
			    file_dir[6].last = tmp;
			}
			file_dir[6].count++;
		    }
		}
		break;
	    }
	}
    }

    if ( file_dir[6].count == 0 ) error_abort ( "403 noauth", 
	"Rule file %s contains no .AUTHORIZE directives so cannot be accessed",
	fname );
    authenticate_user ( file_dir[6].first, fname );
    /*
     * read template file.
     */
    if ( file_dir[7].count == 0 ) error_abort ( "500 noform",
	"No .FORM directive in %s", fname );
    else if ( file_dir[7].count > 1 ) error_abort ( "500 duplicate form", 
	"More that one .FORM directive in %s", fname );
    load_template ( &file_dir[7].first->s[6] );
    /*
     * Action depends upon method.
     */
    if ( strncmp ( method, "POST", 5 ) == 0 ) {
	output_started = cgi_begin_output(1);
	update_configuration(fname);
    } else if ( strncmp ( method, "GET", 4 ) == 0 ) {
	/*
	 * Generate form or perform action.
	 */
	if ( manage_cmd ) {
	    server_query ( manage_cmd );
	} else {
	    int cgi_begin_preprocessed(), cgi_end_preprocessed();
	    char full_path[256];

	    sprintf ( full_path, "/www_system/%s.conf", fname );
	    output_started = /* cgi_begin_output(1); */
		cgi_begin_preprocessed ( argv[1], full_path );
	    generate_form(path_info, fname);
	    cgi_end_preprocessed();
	}
    } else if ( strncmp ( method, "HEAD", 5 ) == 0 ) {
	output_started = cgi_begin_output(1);
	cgi_printf ( "Content-type: text/plain\n\nHTML form goes here\n" );
    } else {
	error_abort ( "401 unsupported", "Method %s unsupported", method );
    }
    return 1;
}
/************************************************************************/
/* Read specified file into global structures.
 */
static char *load_configuration ( char *method, char *path_info, char **mancmd )
{
    int i;
    unsigned long mtime;
    char *cp, *fname, *mod_date, line[4096];
    FILE *rfile;
    stat_t rstat;
    struct file_rec *rec;

    fname = (char *) malloc ( strlen(path_info)+1 );
    strcpy ( fname, (path_info[0] == '/') ? &path_info[1] : path_info );
    mod_date = strchr ( fname, '/' );
    if ( mod_date ) *mod_date++ = '\0'; else mod_date = "0";
    *mancmd = (char *) 0;
    /*
     * open file and validate mtime in path, redirecting if needed.
     */
    if ( strlen(fname) > 250 ) error_abort("500 invfile", "fname invalid", 0 );
    sprintf ( line, "/www_system/%s.conf", fname );
    rfile = fopen ( line, "r" );
    if ( !rfile ) error_abort ( "500 open", "open failure on %s\n", line );
    if ( fstat ( fileno(rfile), &rstat ) < 0 ) error_abort (
	"500 stat", "stat() failure on %s\n", fname );
    if ( 0 == strncmp ( mod_date, "manage/", 7 ) ) {
	*mancmd = &mod_date[7];
	mtime = rstat.st_mtime;
    } else {
        mtime = atoi ( mod_date );
    }
    if ( mtime != rstat.st_mtime ) {
	if ( strcmp(method,"POST") == 0 ) error_abort ( "500 invfile",
	    "Mismatch on file times, can't update %s", fname );
	/* Redirect request */
	output_started = cgi_begin_output(1);
#ifdef NOREFLEX_HACK
	cgi_printf ( "location: http://%s%s/%s/%d\n\n", url_host(),
		cgi_info("SCRIPT_NAME"), fname,	rstat.st_mtime );
#else
	/* Server will fill in correct information for '*://*:*' */
	cgi_printf ( "location: *://*:*%s/%s/%d\n\n",
		cgi_info("SCRIPT_NAME"), fname, rstat.st_mtime );
#endif
	exit(1);
    }
    rule_file_mdate = rstat.st_mtime;
    /*
     * Load file into memory.
     */
    rule_file.length = rule_file.type = 0;
    rule_file.next = rule_file.related = (struct file_rec *) 0;
    
    while ( fgets ( line, sizeof(line), rfile ) ) {
	char *nl; int length;
	nl = strchr ( line, '\n' ); if ( nl ) *nl = '\0';
	length = strlen(line);
	if ( length > 0 ) while ( line[length-1] == '\\' ) {
	    /*
	     * Continuation line, concatentate additional lines but
	     *  preserve linefeeds.
	     */
	    line[length++] = '\n'; 
	    line[length] = '\0';
	    if (!fgets(&line[length], sizeof(line)-length, rfile)) break;
	    nl = strchr ( &line[length], '\n' ); if (nl) *nl = '\0';
	    length += strlen(&line[length]);
	}
	rec = (struct file_rec *) malloc 
		( sizeof(struct file_rec) + length );
	if ( !rec ) error_abort ( "500 mem", "malloc failed", 0 );

	file_dir[0].last->next = rec;
	file_dir[0].last = rec;
	rec->next = (struct file_rec *) 0;
	rec->related = (struct file_rec *) 0;
	rec->length = length;
	strcpy ( rec->s, line );
	/*
	 * If line begins with period, group into related list.
	 */
	rec->type = 0;
	if ( rec->length < 1 ) continue;
	if ( rec->s[0] != '.' ) continue;

	for (cp = &rec->s[1]; *cp && !isspace(*cp); cp++) *cp = toupper(*cp);
	*cp = '\0';
 	for ( i = 1; file_dir[i].tag; i++ ) {
	    if ( 0 == strcmp ( rec->s, file_dir[i].tag ) ) {
		if ( file_dir[i].last ) file_dir[i].last->related = rec;
		else file_dir[i].first = rec;
		file_dir[i].last = rec;
		break;
	    }
	}
	file_dir[i].count++;
	rec->type = i;
	*cp = ' ';
    }
    fclose ( rfile );
    return fname;
}
/*************************************************************************/
/* Read template file used to generate HTML form.  Template is parsed into
 * a linked list of tagged segments for subsequent processing.  The head
 * of this list is the global variable form_file.
 *
 * Segment types:
 *     0	Text.
 *     1	Text with newline at end.
 *     2	Input marker, delimited in template by square bracket.
 *     3	Control marker, special input marker of form [$command].
 */
static char *load_template ( char *fname ) {
    int j, i;
    char line[4096];
    FILE *tfile;
    struct file_rec *rec;
    /*
     * open file
     */
    while ( *fname && isspace(*fname) ) fname++;
    if ( strlen(fname) > 250 ) error_abort("500 invfile", "fname invalid", 0 );
    tfile = fopen ( fname, "r" );
    if ( !tfile ) error_abort ( "500 open", "Open failure on '%s'", fname );
    /*
     * Load file into memory.
     */
    form_file.length = form_file.type = 0;
    form_file.next = form_file.related = (struct file_rec *) 0;

    for ( rec=(&form_file); fgets ( line, sizeof(line), tfile ); ) {
	int length, level, seg_type;
	/*
	 * Parse file into text segments(types 0,1)  separated by marker/control 
	 * segments, type 2/3.
	 */
        level = 0;
	for ( j = i = 0; i < sizeof(line); i++ ) {
	    if ( line[i] == '[' ) {
		level++;
		if ( level > 1 ) continue;
		seg_type = 0;
	    } else if ( line[i] == ']' ) {
		--level;
		if ( level > 0 ) continue;
		seg_type = 2;
		if ( line[j] == '$' ) seg_type = 3;
	    } else if ( (line[i] == '\n') || (line[i] == '\0') ) {
		seg_type = 1;
	    } else continue;
	    /*
	     * Copy portion of read record to new segment if j has been
	     * updated.
	     */
	    if ( j < i ) {
		length = i - j;
	        rec->next = (struct file_rec *) malloc (
			sizeof(struct file_rec) + length );
	        if ( !rec ) error_abort ( "500 mem", "malloc failed", 0 );
	        rec = rec->next;
	        rec->next = rec->related = (struct file_rec *) 0;
	        strncpy ( rec->s, &line[j], length );
	        rec->s[length] = '\0';
	        rec->length = length;
	        rec->type = seg_type;
	    }
	    j = i + 1;
	    if ( seg_type == 1 ) break;
	}
    }
    fclose ( tfile );
    return fname;
}
/****************************************************************************/
/* Convert pluses and escaped characters in string, also convert
 * '' to " (special hack since browser support for &quote; sucks) */
static int htmlStrcpy (
    char *out,
    char *in)
{
    int	i, j;
    /* printf("htmlstr raw string: %s\n", in ); */
    for ( j=0;  *in;  in++)
    {
    	if (*in == '%') {
	    int	xxx ;
	    if ( (in[1]=='\0') || (in[2]=='\0') ) break;  /* too short */
	    sscanf (&in[1], "%02x", &xxx) ;
	    if ( xxx == '\'' ) {
		if ( strncmp ( &in[3], "%27", 3 ) == 0 ) {
		    xxx = '"'; in += 3;		/* convert '' to " */
		}
	    }
	    out[j++] = (char) xxx ;
	    in += 2 ;
	} else if ( *in == '+' ) {
	    out[j++] = ' ';
	} else if ( (*in == '\'') && (in[1] == '\'') ) {
	    in++;		/* skip */
	    out[j++] = '"';
	} else {
	    out[j++] = *in;
	}
    }
    out[j] = '\0';
    return j;
}
/****************************************************************************/
/* Read form data and parse into list of key/value pairs.  Plus-to-space
 * conversion is performed and escaped chars are converted.
 * Full CGI responses are generated on error.
 */
static field_def_p form_data()
{
    char *method, *content_data, *cp;
    field_def_p field_list, fld;
    int i, content_length, status;
    /*
     * Determine what form data is being delivered by client.
     */
    method = cgi_info ( "REQUEST_METHOD" );
    if ( 0 == strcmp ( method, "POST" ) ) {
	/*
	 * Data was posted.
	 */
        content_length = atoi(cgi_info ("CONTENT_LENGTH"));
        if (content_length == 0) error_abort ( "500 missing content", 
		"No data in CONTENT_LENGTH (%s)", cgi_info("CONTENT_LENGTH") );

        content_data = (char *) malloc(sizeof(char)*(content_length+1));
        status = cgi_read(content_data, content_length);
        if (status == 0) error_abort ( "500 missing content", 
		"No data from cgi_read", 0 );

        content_data[content_length] = '\0';
    } else {
	error_abort("501 unsupported method","Method %s is unsupported",
	    method );
    }
    /*
     * Parse the content into key/value pairs.
     */
    field_list = (field_def_p) 0;
    if ( content_data[0] != '&' ) {
	fld = (field_def_p) malloc ( sizeof(struct field_def) );
	fld->next = field_list;
	fld->key = content_data;
	fld->klen = 0;
	field_list = fld;
    }
    for ( i = 0; i < content_length; i++ ) if ( content_data[i] == '&' ) {
	fld = (field_def_p) malloc ( sizeof(struct field_def) );
	fld->next = field_list;
	fld->key = &content_data[i+1];
	fld->klen = 0;
	content_data[i] = '\0';
	field_list = fld;
    }
    for ( fld = field_list; fld; fld = fld->next ) {
	fld->value = "";
	for ( cp = fld->key, i = 0; cp[i]; i++ ) {
	    if ( cp[i] == '=' ) {
		cp[i] = '\0';
		fld->klen = i;
		fld->value = &cp[i+1];
	    }
	}
	htmlStrcpy ( fld->value, fld->value );
	/* printf("Parsed key '%s' = '%s'\n", fld->key, fld->value ); */
    }

    return field_list;
}
/*****************************************************************************/
/* Return table of symbol definitions of a given line type (specified by
 * index).  Each line is parsed into a symbol name and value.
 *
 * If copy is true, new memory is allocated to hold the string data, otherwise
 * the exsiing strings are modified.
 */
struct symdef *collect_symbols ( int ndx, int copy )
{
    struct symdef *table;
    file_recp rec;
    int i, start_offset;
    char *line, *tok;
    /*
     * Allocate table, the load procedure tracked how many lines of each
     * type there were.
     */
    start_offset = strlen ( file_dir[ndx].tag );
    table = (struct symdef *) malloc (
		(file_dir[ndx].count+1)*sizeof(struct symdef));
    if ( !table ) error_abort ( "500 malloc", "malloc failed", 0 );
    /*
     * Scan all records of type ndx.
     */
    for ( i = 0, rec = file_dir[ndx].first; rec; rec = rec->related ) {
	table[i].source = rec;
	if ( copy ) {
	    /*
	     * Make copy of line so the parsing doesnt destroy the original.
	     */
	    line = malloc ( rec->length+1 );
	    if ( !line ) printf("error in malloc, length: %d\n", rec->length);
	    strncpy ( line, rec->s, rec->length );
	    line[rec->length] = '\0';
	}  else line = rec->s;
        /*
	 * Parse of first token after .type, which is symbol name.
         */
	tok = &line[start_offset];
	while ( *tok && isspace(*tok) ) tok++;
	table[i].symbol = tok;
	if ( *tok ) {
	     /*
	      * Find symbol name's end and terminate it.
	      */
	     while ( *tok && !isspace(*tok) ) tok++;
	     if ( *tok ) {
	         *tok++ = '\0';
		/* Skip to start of value */
	         while ( *tok && isspace(*tok) ) tok++;
	     }
	}
	table[i].value = tok;
	i++;
    }
    table[i].symbol = table[i].value = (char *) 0;
    return table;
}
/***************************************************************************/
/*
 * Decode block of 4 characters to 1,2 or 3 characters.  Return 0 if
 * block is invalid.
 */
static int decode_char ( char **inp, int *context, unsigned char *out )
{
    char *enc_table = 
	"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
   int context_bits, context_count, i, j;
   char *p, c;
   context_bits = *context;
   context_count = (context_bits>>12);
   context_bits = context_bits&4095;
   
   p = *inp;
   while ( context_count < 8 ) {
       c = *p++;
	if ( c == '=' ) return 0;
	if ( c == '\0' ) return 0;
       for ( j = 0; (enc_table[j] != c); j++ ) if ( !enc_table[j] ) return 0;
	context_bits = (context_bits&63)<<6 | j;
	context_count += 6;
   }
    if ( context_count == 8 ) *out = (context_bits&255);
    else *out = (context_bits>>(context_count-8))&255;
   *context = (context_bits&4095) | ((context_count-8)<<12);
   *inp = p;
   return 1;
}
/**************************************************************************/
/* Validate the user has access rights to file.  Program aborts on failure.
 *
 * .authorize formats:
 *     user[@host-addr]		User must match REMOTE_USER variable
 *					(Script must be protected)
 *     *[@host-addr]		User must have been authenticated via protect
 *				rule on script.
 *     user[@host-addr] pwd 	User and password must match that decoded
 *				from .authorize statement AND server port must
 *				be non-privileged (>1024).
 */
static void authenticate_user ( struct file_rec *auth_recs, char *fname )
{
    struct symdef *user_list;
    char *auth_header, *decode_buf, *user, *password, *remote_user;
    char *host_addr, *remote_addr, *remote_host;
    char *authenticate = "WWW-authenticate: Basic realm=\"ServerMaint\"";
    int i, j, ctx;
    unsigned char c;
    /*
     * Get authorization header and decode user/password;
     */
    remote_user = cgi_info ( "REMOTE_USER" );
    remote_addr = cgi_info ( "REMOTE_ADDR" );
    remote_host = cgi_info ( "REMOTE_HOST" );
    auth_header = cgi_info ( "HTTP_AUTHORIZATION" );
    if ( !auth_header ) error_abort ( 
	"401 authorization failed, none provided\n%s", "no auth header",
	authenticate );
    decode_buf = malloc ( strlen ( auth_header ) );
    for ( i = 0; auth_header[i] && !isspace(auth_header[i]); i++ ) {
	decode_buf[i] = tolower(auth_header[i]);
	if ( i > 5 ) break;
    }
    decode_buf[i] = '\0';
    if ( strcmp ( decode_buf, "basic" ) ) error_abort ( 
	"401 authorization failed, wrong type\n%s", "Wrong auth. type",
	authenticate );
    while ( auth_header[i] && isspace(auth_header[i]) ) i++;
    user = &auth_header[i];
    password = (char *) 0;
    for ( i = ctx = 0; decode_char(&user, &ctx, &c); i++ ) {
	decode_buf[i] = toupper(c);
	if ( !password ) if ( c == ':' ) {
	    decode_buf[i] = '\0';
	    password = &decode_buf[i+1];
	}
    }
    decode_buf[i] = '\0';

    if ( !password ) error_abort ( 
	"401 authorization failed, syntax\n%s", "Syntax error",
	authenticate );
    user = decode_buf;
    /*
     * Scan file for .authorize directive that matches.
     */
    user_list = collect_symbols ( 6, 1 );	/* parse .authorize list */
    /* printf("%s records: %d, cand: '%s' pwd: '%s'\n", file_dir[6].tag, 
	file_dir[6].count, user, password ); */
    for ( i = 0; user_list[i].symbol; i++ ) {
	/*
	 * Upcase the username and trim off hostname
	 */
	host_addr = (char *) 0;
	for ( j=0; user_list[i].symbol[j]; j++ ) {
	    user_list[i].symbol[j] = toupper(user_list[i].symbol[j]);
	    if ( user_list[i].symbol[j] == '@' ) {
		host_addr = &user_list[i].symbol[j+1];
		user_list[i].symbol[j] = '\0';
		break;
	    }
	}
	if ( host_addr ) {
	    /* Skip this record if host doesn't match */
	    if ( strcmp ( host_addr, remote_host ) &&
		strcmp ( host_addr, remote_addr ) ) continue;
	}
	if ( user_list[i].value[0] ) {
	    /*  Verify password and succeed if matches */
	    if ( atoi ( cgi_info("SERVER_PORT") ) < 1024 ) continue;
	    for ( j=0; user_list[i].value[j]; j++ )
		user_list[i].value[j] = toupper(user_list[i].value[j]);
	    if ( strcmp ( user_list[i].value, password ) == 0 ) return;
	} else if ( remote_user ) {
	    /* No password, user must match remote user  or '*' */
	    if ( *remote_user && 
		( 0 == strcmp ("*", user_list[i].symbol ) ) ) return;
	    if ( 0 == strcmp ( remote_user, user_list[i].symbol ) ) {
	        /* User verified */
	        return;
	    }
	}
    }
    error_abort ( "401 authorization failed\n%s", "authorization failed",
	authenticate );
}
/**************************************************************************/
static int update_definition ( char *tag, int tag_length, 
	field_def_p field_list, struct symdef *symbols, char *suffix )
{
    int i, j, k, state, nlen;
    char  *tmp, *t_value, *value, name[128];
    field_def_p fld;
    /*
     * tag fomats:
     *    [name;size;max]		<input type="text" size=size>
     *	  [name|opt1|opt2|...]		<select><option>opt1<option>opt2<option>...
     *    [name?on:off]			<input type="checkbox" name=name>
     */
    tag[tag_length] = '\0';
    value = "";
    for ( i = 0; i < tag_length; i++ ) {
	if ( (tag[i] == ';') || (tag[i] == '?') || (tag[i] == '|') ) {
	    /*
	     * Lookup symbol in .define list.
	     */
	    for ( j = 0; symbols[j].symbol; j++ ) {
		if ( 0 == strncmp ( tag, symbols[j].symbol, i ) ) {
		    if ( symbols[j].symbol[i] == '\0' ) break;
		}
	    }
	    if ( !symbols[j].symbol ) return 1;	/* no .define */
	    /*
	     * Lookup suffixed symbol in form data.
	     */
	    nlen = strlen(suffix);
	    if ( i > (sizeof(name)- 1 - nlen) ) break;
	    strncpy ( name, tag, i );
	    strcpy ( &name[i], suffix );
	    nlen += i;
	    for (fld=field_list; fld; fld=fld->next) if (fld->klen == nlen) {
		if ( strncmp(name, fld->key, nlen) == 0 ) {
		    value = fld->value;
		    break;
		}
	    }
	    break;
	}
    }
    switch ( tag[i] ) {
	case ';':		/* text field or select field */
	case '|':
	    if ( fld ) {
		symbols[j].value = fld->value;
	    } else symbols[j].value = "";
	    break;
	case '?':		/* check box */
	    /* Get value tag following ? or : */
	    t_value = &tag[i+1];
	    tag_length = tag_length - i - 1;
	    for ( k=0; (k < tag_length) && (t_value[k] != ':'); k++);
	    if ( strncmp ( value, "on", 3 ) != 0 ) {
		if ( t_value[k] ) {
		    t_value = &t_value[k+1];
		    k = tag_length - k - 1;
		} else {
		    t_value = &t_value[k];
		    k = 0;
		}
	    }
	    symbols[j].value = malloc ( k+1 );
	    strncpy ( symbols[j].value, t_value, k );
	    symbols[j].value[k] = '\0';
	    break;
	default:
	    break;
    }
    return 1;
}
/**************************************************************************/
/*  Setup symbol table for iteration arguments.  Return value is pointer
 *  to .iterate record that matched.  The edit and end variable may
 *   be specified as NULL.
 *
 *  The symbol table is re-ordered to match the order of the .iterate command.
 */
file_recp setup_iteration ( file_recp begin, struct symdef *symbols,
	file_recp *edit, file_recp *end )
{
    int i, j, count, state, start, k;
    file_recp rec, config_rec;
    int map[16];

    if ( edit ) *edit = (file_recp) 0;		/* initialize */
    if ( end ) *end = (file_recp) 0;
    /*
     * Scan ahead in template for segment type 2 and 3, looking for
     * symbols referenced and $END.
     */
    i = 0;
    for ( rec = begin->next; rec; rec = rec->next ) /*if ( rec->type > 1 )*/ {
	/* printf("setup iteration, rec type: %d, string: '%s'\n", rec->type, rec->s );*/
	if ( rec->type == 2 ) {
	    /*
	     * Parse symbol name from tag and add to local symbol table.
	     * symbol names will have an .nn suffix attached so include
	     * romm for it.
	     */
	    for ( j = 0; j < rec->length; j++ ) 
		if ( strchr(";|:?",rec->s[j]) ) break;
	    symbols[i].symbol = malloc ( j+10 );
	    if ( !symbols[i].symbol ) break;
	    strncpy ( symbols[i].symbol, rec->s, j );
	    symbols[i].symbol[j] = '\0';
	    symbols[i].value = "";
	    i++;
	    if ( i > 14 ) break;
	} else if ( rec->type == 3 ) {
	    if ( strcmp ( rec->s, "$END" ) == 0 ) {
		if ( end ) *end = rec;
		break;
	    } else if ( strncmp ( rec->s, "$EDIT", 5 ) == 0 ) {
		if ( edit ) *edit = rec;
	    }
	}
    }
    count = i;
    symbols[i].symbol = (char *) 0;	/* terminate symbol list */
    /* printf("setup iteration, local symbols: %d\n", count ); */
    /*
     * Scan .iterate records in configuration file for one with dummy 
     * argument names that match our symbol list.  Symbols on .iterate
     * line amy not match order they appear in template, re-order to match.
     *  ( NOT DONE YET)
     */
    config_rec = (file_recp) 0;
    for ( rec = file_dir[4].first; rec; rec = rec->related ) {
	char *line, symname;
	state = i = 0;
	/*
	 * Parse the line's tokens for referenced variables.
	 */
	config_rec = rec;
	line = &rec->s[1];
	for ( j = 8; j < rec->length; j++ ) switch ( state ) {
	    case 0:
		if ( !isspace(line[j]) ) {
		    if ( line[j] == '$' ) {
			start = j+1;
			state = 2;
		    }
		    else state = 1;   /* skip */
		}
		break;
	    case 1:
		if ( isspace(line[j]) ) state = 0;
		break;
	    case 2:
		/*
		 * Find end of token, count a backslash as whitespace.
		 */
		if ( !isspace(line[j]) && line[j] && (line[j] != '\\') ) break;
		/*
		 * search symbol table for symname.
		 */
		map[i] = -1;
		for ( k = 0; symbols[k].symbol; k++ ) {
		    if ( 0 == strncmp(&line[start], symbols[k].symbol, 
			    (j-start)) && !symbols[k].symbol[j-start] ) {
			map[i] = k;
			break;
		    }
		}
		if ( !symbols[k].symbol ) {	/* no match */
		    config_rec = (file_recp) 0;
		    break;
		}
		i++;
		state = 0;
	    default:
		break;
	}
	if ( config_rec ) {
	    /* use map to re-order the symbol table */
	    for ( k = 0; k < i; k++ ) {
		while ( k != map[k] ) {
		    symbols[i] = symbols[map[k]];
		    symbols[map[k]] = symbols[k];
		    symbols[k] = symbols[i];
		    map[i] = map[map[k]];
		    map[map[k]] = map[k];
		    map[k] = map[i];
		}
	    }
	    break;
	}
    }
    return config_rec;
}
/**************************************************************************/
/* Handle the form files [$command] markers in POST operation.
 */
static file_recp control_update_function ( file_recp rec, 
	field_def_p field_list )
{
    int ndx, j, length, rec_type;
    char *edit_control, field_name[128], suffix[12];
    static file_recp config_rec, edit, end;
    file_recp c_rec, t_rec;
    field_def_p fld;
    static struct symdef local_symbols[16];

    /* printf("control function: '%s'\n", rec->s ); */
    if ( strcmp ( rec->s, "$BEGIN" ) == 0 ) {
	/*
	 *  Build symbol table from referenced varaible names and locate
	 * the matching .iterate record.
	 */
	config_rec = setup_iteration ( rec, local_symbols, &edit, &end );
	if ( !config_rec ) return config_rec;
	/*
	 * Scan forward  of config_rec for all related .next records,
	 * and edit them.
	 */
	ndx = 0;		/* current index number for form fields */
	for (c_rec = config_rec->next; ndx==0 || c_rec; c_rec = c_rec->next ) {
	    rec_type = c_rec ? c_rec->type : 0;
	    if ( (ndx > 0) && rec_type == 4 ) break;	/* new .iterate record */
	    if ( rec_type == 5 ) {
		ndx++;
		sprintf(suffix, ".%d", ndx );
		/*
		 * Look for edit control.
		 */
		if ( edit ) {
		    sprintf ( field_name, "edtctl%s%s", &edit->s[5], suffix );
		    j = strlen(field_name);
		    for ( fld = field_list; fld; fld = fld->next ) {
			if ( j != fld->klen ) continue;
			if ( strcmp ( fld->key, field_name) == 0 ) {
			    edit_control = fld->value;
			    break;
			}
		    }
		    if ( !fld ) continue;	/* no changes */
		    if ( strcmp ( edit_control, "..." ) == 0 ) continue;
		    if ( strcmp ( edit_control, "Delete" ) == 0 ) {
			/* Change type of record to make output pass ignore it */
		        c_rec->type = 1;
			continue;
		    }
		
		} else {
		    /* default to'modified' */
		    edit_control = "Modify";
		}
		/*
		 * Replay records to update symbol table values.
		 */
		for ( t_rec = rec; t_rec; t_rec = t_rec->next ) {
		    if ( t_rec->type == 2 ) {
			update_definition ( t_rec->s, t_rec->length,
				field_list, local_symbols, suffix );
		    } else if ( t_rec->type == 3 ) {
			if ( strncmp ( t_rec->s, "$END", 4 ) == 0 ) break;
		    }
		}
		/*
		 * Copy the new symbols to a new string;
		 */
		for ( length = j = 0; local_symbols[j].symbol; j++ ) {
		    length = length + strlen ( local_symbols[j].value ) + 1;
		}
		/*
		 * Update the .modified. or insert new.
		 */
		if ( (strcmp ( edit_control, "Modify" ) == 0) ||
		      (strcmp ( edit_control, "Create" ) == 0) ) {
		    c_rec->type = 1;	/* hide this record */
		} else if ( strcmp ( edit_control, "Insert" ) == 0 ) {
		}
		t_rec = (file_recp) malloc (sizeof(struct file_rec) +length+6);
		t_rec->next = c_rec->next;
		t_rec->related = c_rec->related;
		t_rec->type = 5;
		c_rec->next = t_rec;
		c_rec->related = t_rec;
		c_rec = t_rec;

		length = 6;
		strcpy ( c_rec->s, ".next " );
		for ( j = 0; local_symbols[j].symbol; j++ ) {
		    if ( j > 0 ) c_rec->s[length++] = ' ';
		    strcpy ( &c_rec->s[length], local_symbols[j].value );
		    length += strlen ( local_symbols[j].value );
		}
		c_rec->length = length;
	    }
	    /*
	     * Check for case of no records.
	     */
	    if ( ndx == 0 ) {
	        j = c_rec ? (c_rec->next ? c_rec->next->type : 4) : 4;
		if ( j == 4 ) {
		    /*  Append a dummy .next after the .iterate */
		    t_rec = (file_recp) malloc ( sizeof(struct file_rec) );
		    t_rec->next = config_rec->next;
		    t_rec->related = (file_recp) 0;
		    t_rec->type = 5;
		    t_rec->length = 0;
		    t_rec->s[0] = '\0';
		    config_rec->next = t_rec;
		    c_rec = config_rec;
		}
	    }
	}
	/*
	 * Return the end marker  so we don't re-hash this ground.
	 */
	rec = end;
     }
    return rec;
}
/**************************************************************************/
void update_configuration ( char *fname )
{
    struct symdef *symbols;
    FILE *cfile;
    char full_name[256];
    file_recp rec;
    stat_t rstat;
    int i, j, k, status;
    field_def_p field_list, fld;
    /*
     * Read form data submitted by client and symbol table of current 
     * rule file values.
     */
    field_list = form_data();
    /* printf("Form data:\n");	for ( fld = field_list; fld; fld = fld->next ) {
	printf("   '%s' = '%s'\n", fld->key, fld->value );
    } */

    symbols = collect_symbols ( 1, 1 );
    /*
     * Scan template file for markers and update corresponding .define 
     * symbols from the form data field_list.
     */
    for (rec = form_file.next; rec; rec = rec->next) if ( rec->type > 1 ) {
	if ( rec->type == 2 ) {
	    if ( !update_definition ( rec->s, rec->length, field_list, 
			symbols, "" ) ) {
	    }
	} else if ( rec->type == 3 ) {
	    rec = control_update_function ( rec, field_list );
	    
	}
    }
    /*
     * Open output file.
     */
    sprintf(full_name,"/www_system/%s.conf", fname );
    cfile = fopen ( full_name, "w" );
    if ( !cfile ) error_abort ( "500 fopen", "error opening output file %s",
	full_name );
    /*
     * Begin file with .ignore directive for servermaint's extensions and
     * folllow with dump of symbol table as .define directives.
     */
    fprintf(cfile,".ignore");
    for (i=6; file_dir[i].tag; i++) fprintf(cfile, " %s", file_dir[i].tag);
    fprintf(cfile,"\n");
    for ( i = 0; symbols[i].symbol; i++ ) {
	/*
	 * Generate .define rule to set value.
	 */
	fprintf(cfile,".define %s %s\n", symbols[i].symbol, symbols[i].value);
    }
    /*
     * Copy records of original to new file, modifying as needed.
     */
    for ( rec = rule_file.next; rec; rec = rec->next ) {
	if ( rec->type == 1 ) {		/* .define record */
	   /* All defines collected and output above. */
	    continue;
	} else if ( rec->type == 3 ) {		/* .ignore record */
	    continue;
	} else if ( rec->type == 4 ) { /* .iterate record */

	} else if ( rec->type == 5 ) { /* .next record */
	    if ( rec->length == 0 ) continue; /* don't write empty records */
	}
	rec->s[rec->length] = '\n';
	status = fwrite ( rec->s, rec->length+1, 1, cfile );
	rec->s[rec->length] =  '\0';
	if ( status != 1 ) error_abort ( "500 fwrite", 
		"file write error '%s'", rec->s );
    }
    fclose ( cfile );
    /*
     * Get new file update time and redirect to regenerate form.
     */
    if ( stat ( full_name, &rstat ) < 0 ) error_abort (
	"500 stat", "stat() failure on %s\n", fname );
#ifdef NOREFLEX_HACK
    cgi_printf("location: http://%s%s/%s/%d\n\n", url_host(),
	cgi_info("SCRIPT_NAME"), fname, rstat.st_mtime );
#else
    /* Server will fill in correct information for '*://*:*' */
    cgi_printf ( "location: *://*:*%s/%s/%d\n\n",
		cgi_info("SCRIPT_NAME"), fname, rstat.st_mtime );
#endif
}
/**************************************************************************/
/* Scan for next .iterate record and load its argument into symbol table.
 */
static file_recp load_iteration_symbols 
	( file_recp cur_rec, struct symdef *symbols )
{
    int i, j, length, pos, state;
    char *period;
    static char line[4096];
    /*
     * Locate the next .next record following the current record.
     */
    if ( !cur_rec ) printf("Load_iteration_symbols called with null cur_rec\n");
    if ( !cur_rec ) return cur_rec;
    for (  cur_rec = cur_rec->next; cur_rec; cur_rec = cur_rec->next ) {
	if ( cur_rec->type == 5 ) break;
	if ( cur_rec->type == 4 ) {
	    /* A new .iterate was encounter, killing this iteration set */
	    return (file_recp) 0;
	}
    }
    if ( !cur_rec ) return cur_rec;
    /*
     * Copy the line to local static storage so we can break it up into
     * nul-terminated strings.
     */
    length = cur_rec->length;
    if ( length >= sizeof(line) ) length = sizeof(line) - 1;
    strncpy ( line, cur_rec->s, length );
    line[length++] = '\0';
    /* cgi_printf("--rec: '%s', l=%d--<BR>\n", line, length ); */
    /*
     * Parse the values on the line and assign each to next symbol in table.
     */
    state = pos = 0;
    for ( i = strlen(file_dir[5].tag); i < length; i++ ) {
	if ( isspace(line[i]) || (line[i] == '\0')  ) {
	    if ( state == 1 ) {
		/* found end, terminate string*/
		line[i] = '\0';
		state = 0;
	    }
	} else {
	    if ( state == 0 ) {
		/* Found start, save its position */
		symbols[pos].value = &line[i];
		pos++;
		if ( !symbols[pos].symbol ) break; /* no more symbols */
		state = 1;
	    }
	}
    }
    /* Blank out remaining values in symbol table. */
    while ( symbols[pos].symbol ) symbols[pos++].value = "";
    return cur_rec;
}
/**************************************************************************/
static file_recp control_function ( file_recp rec, struct symdef **symbols,
	char *suffix )
{
    int i, j;
    static char *edit_options;
    static struct symdef *original_symbols;
    static file_recp begin_rec;
    static int cur_ndx;
    static struct symdef local_symbols[16];
    static file_recp config_rec;

    if ( strcmp ( rec->s, "$BEGIN" ) == 0 ) {
	/*
	 * Begin an iteration loop.
	 */
	original_symbols = *symbols;
	begin_rec = rec;
	cur_ndx = 0;
	edit_options = "<option>Error";
	/*
	 * Scan ahead in template for segment type 2 and 3, looking for
	 * symbols referenced and $END.
	 */
	config_rec = setup_iteration ( rec, local_symbols,
		(file_recp *) 0, (file_recp *) 0 );
	if ( !config_rec ) {
	    /* No match */
	    cgi_printf ("--No .iterate to match form variables--<BR>\n" );
	    return rec;
	}
	/*
	 * Scan records following for first .next and load local_symbols
	 * with its values.
	 */
	config_rec = load_iteration_symbols 
		( config_rec, local_symbols );
	if ( config_rec ) {
	     *symbols = local_symbols;
	     edit_options = 
		"<option>...<option>Modify<option>Insert<option>Delete";
	} else {
	    /* special case, no .next records */
	    edit_options = "<option>...<option>Create";
	}
	cur_ndx++;
	sprintf ( suffix, ".%d", cur_ndx );
    } else if ( strcmp ( rec->s, "$END" ) == 0 ) {
	/*
	 * End current iteration, start next.  Find next .next and load
	 * local_symbols with its values.
	 */
	config_rec = load_iteration_symbols 
		( config_rec, local_symbols );

	if ( !config_rec ) {
	    /*
	     * No next symbols, restore original symbol tables.
	     */
	    *symbols = original_symbols;
	    suffix[0] = '\0';
	} else {
	    /*
	     * Reset rec to the [$begin] to redo.
	     */
	    rec = begin_rec;
	    cur_ndx++;
	    sprintf ( suffix, ".%d", cur_ndx );
	}
    } else if ( strncmp ( rec->s, "$EDIT", 5 ) == 0 ) {
	/*
	 * insert edit control input element.
	 */
	cgi_printf( "<select name=\"edtctl%s%s\">%s</select>", &rec->s[5],
		suffix, edit_options );
    } else {
	/* Unknown command */
	cgi_printf ( "--Unkown control tag in template file (%s)--", rec->s);
    }
    return rec;
}
/**************************************************************************/
static int expand_form_tag ( char *tag, char *suffix, int tag_length, 
	struct symdef *symbols )
{
    int i, j, state;
    char  *tmp, *value, *ent_value;
    /*
     * tag formats:
     *    [name;size;max]		<input type="text" size=size>
     *	  [name|opt1|opt2|...]		<select><option>opt1<option>opt2<option>...
     *    [name?on:off]			<input type="checkbox" name=name>
     *    [$cmd]			{iteration control} (handled elsewhere)
     */
    value = "";
    for ( state = i = 0; i < tag_length; i++ ) {
	if ( tag[i] == ';' ) {		/* text field */
	    if ( state == 0 ) {
	        tag[i] = '\0';
	    	for ( j = 0; symbols[j].symbol; j++ ) {
		    if ( strcmp ( tag, symbols[j].symbol ) == 0 ) 
		    { value = symbols[j].value; break; }
	    	}
		ent_value = entify_string ( value );
		cgi_printf("<input type=\"text\" name=\"%s%s\" value=\"%s\"", 
			tag, suffix, ent_value );
		if ( ent_value != value ) free ( ent_value );
	        tag[i] = ';';
		state = 1;
	    } else if ( state == 1 ) {
	        tag[i] = '\0';
		cgi_printf(" size=\"%s\"", tmp);
	        tag[i] = ';';
		state = 2;
	    } else if ( state == 2 ) {
		cgi_printf(" maxlength=\"%s\"", tmp);
		state = 3;
		break;
	    }
	    tmp = &tag[i+1];
	} else if ( tag[i] == '|' ) {	/* select list */
	    if ( state == 0 ) {
		/*
		 * Generate a select list.
		 */
	        tag[i] = '\0';
	        for ( j = 0; symbols[j].symbol; j++ ) {
		  if ( strcmp ( tag, symbols[j].symbol ) == 0 ) 
		    { value = symbols[j].value; break; }
	        }
		cgi_printf ( "<select name=\"%s%s\">", tag, suffix );
	        tag[i] = '|';
		state = 4;
	    } else if ( state == 4 ) {
		tag[i] = '\0';
		if ( strncmp ( value, tmp, 100 ) == 0 ) {
		     cgi_printf ( "<option selected>%s</option>\n", tmp );
		} else {
		     cgi_printf ( "<option>%s</option>\n", tmp );
		}
		tag[i] = '|';
	    } else continue;
	    tmp = &tag[i+1];
	} else if ( tag[i] == '?' ) {
	    tag[i] = '\0';
	    if ( state == 0 ) {
	        for ( j = 0; symbols[j].symbol; j++ ) {
		   if ( strcmp ( tag, symbols[j].symbol ) == 0 ) 
		    { value = symbols[j].value; break; }
	        }
		cgi_printf("<input type=\"checkbox\" name=\"%s%s\"", tag, suffix );
		state = 5;
	    }
	    tag[i] = '?';
	    tmp = &tag[i+1];
	} else if ( tag[i] == ':' ) {
	    /*  tmp is 'on' value, tag[i+1...] is 'off' value */
	    if ( state != 5 ) continue;
	    tag[i] = '\0';
	    if ( strcmp(value,tmp) == 0 ) cgi_printf ( " checked" );
	    tag[i] = ':';
	}
    }
    /*
     * Take final action on contents of remaining in tmp string.
     */
    switch ( state ) {
	case 1:
	    cgi_printf(" size=\"%s\">", tmp);
	    break;
	case 2:
	    cgi_printf(" maxlength=\"%s\">", tmp);
	    break;
	case 3:
	case 5:
	    cgi_printf(">");
	    break;
	case 4:
	    if ( strncmp ( value, tmp, 100 ) == 0 ) {
		     cgi_printf ( "<option selected>%s</option></select>\n", tmp );
	    } else {
		     cgi_printf ( "<option>%s</option></select>\n", tmp );
	    }
	    break;
	    
	default:
	    cgi_printf ( "<b>-Syntax error in template-</b>" );
    }
    return 1;
}
/**************************************************************************/
/* Output HTML form from template, loading input fields with current
 * values for configuration file.
 */
static void generate_form ( char *path_info, char *fname )
{
    int i, j, k, status;
    file_recp rec;
    char suffix[32];
    struct symdef *symbols;

    symbols = collect_symbols ( 1, 0 );
    suffix[0] = '\0';		/* Appended to all input field names */

    cgi_printf("<HTML><HEAD>\n");
    cgi_printf("<TITLE>Server Configuration File %s</TITLE>\n</HEAD><BODY>\n",
	fname );
    cgi_printf("<FORM method=\"POST\" action=\"http://%s%s%s\">\n", 
	url_host(), cgi_info("SCRIPT_NAME"), path_info );
    /*
     * Process each record segment;
     */
    for ( rec = form_file.next; rec; rec = rec->next ) {
	char *cp;
	cp = rec->s;
	if ( rec->type == 0 ) {
	    /* text segment */
	    status = cgi_printf ( "%s", rec->s );
	} else if ( rec->type == 1 ) {
	    /* Last text segment on a line */
	    status = cgi_printf ( "%s\n", rec->s );
	} else if ( rec->type == 2 ) {
	    /* Input marker */
	    status = expand_form_tag ( rec->s, suffix, rec->length, symbols );
	} else {
	    /* control tag, may reset the position. */
	    rec = control_function ( rec, &symbols, suffix );
	    if ( !rec ) return;
	}
	if ( (status&1) == 0 ) return;
    }
    cgi_printf ( "</FORM>ServerMaint 1.2, %s last modified %s</BODY></HTML>\n",
	fname, ctime ( &rule_file_mdate ) );
    return;
}
/*
 * Convert special characters to 'entity' forms: <>&".  If no expansions,
 * function value returned is pointer to source string, otherwise return value is
 * pointer to malloc'ed string which caller must deallocate.
 * pointer to ori
 */
static char *entify_string ( char *source )
{
   int i, j,brack_count;
   char *dest;
   dest = source;
   brack_count = 0;
   for ( i = 0; source[i]; i++ ) {
	if ( (source[i] == '<') || (source[i] == '>') ||
	    (source[i] == '&') || (source[i] == '"') ) brack_count++;
   }
   if ( brack_count > 0 ) {
	dest = malloc ( i + brack_count*6 + 1 );
	if ( !dest ) return source;

	for ( i = j = 0; source[i]; i++ ) {
	    if ( source[i] == '<' ) {
		dest[j++] = '&';
		dest[j++] = 'l';
		dest[j++] = 't';
		dest[j++] = ';';
	    } else if ( source[i] == '>' ) {
		dest[j++] = '&';
		dest[j++] = 'g';
		dest[j++] = 't';
		dest[j++] = ';';
	    } else if ( source[i] == '&' ) {
		dest[j++] = '&';
		dest[j++] = 'a';
		dest[j++] = 'm';
		dest[j++] = 'p';
		dest[j++] = ';';
	    } else if ( source[i] == '"' ) {
		/*
		 * NOTE: We should really be producing "&quote;" here, but
		 * browser's are broken so use our own convention of "''".
		 */
		dest[j++] = '\'';
		dest[j++] = '\'';
	    } else
		dest[j++] = source[i];
	}
	dest[j] = '\0';
   }
   return dest;
}
