/* pb.c */
/* Andrew Davison, July 1996 (ad@cs.mu.oz.au)  */
/* A Web page containing a GIF is returned. The GIF contains 
   the user's three biorhythm curves for a specified month/year.
   The user supplies their birthday and month/year of interest
   via a form.
   The number of days since the user's birthday is
   calculated by converting the birthday and month/year of interest
   into their number of Julian days. The difference is the
   start day for the biorhythm equation.
   The gnuplot commands are in PLOT_CMDS, and this is modified
   and copied into TEMP_CMDS before being displayed. The two
   changes insert the start day and the month/year details.
   gnuplot saves its drawing into <PIC>.ppm, and this is
   converted into GIF format with ppmtogif.
   URL_PIC is the URL of PIC. 
   Tools used: gnuplot, ppmtogif
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>     /* for read(), lseek(), write(), close(), lockf() */
#include <fcntl.h>      /* for open() */

#define BKGROUND "http://www.cs.mu.oz.au/~ad/chalk.jpg"
#define MAX_ENTRIES 2           /* number of input fields */
#define MAXLEN 500              /* max length of a string */
#define NOVAL "$$no-value$$"    /* when no value is found */
#define COUNT_NM  "/home/staff/ad/www_public/code/biorhythm/count"
#define PLOT_CMDS "/home/staff/ad/www_public/code/biorhythm/bio.plt"
#define TEMP_CMDS "/home/staff/ad/www_public/code/biorhythm/tmp.plt"
#define PIC       "/home/staff/ad/www_public/code/biorhythm/pbio"
#define URL_PIC   "http://www.cs.mu.oz.au/~ad/code/biorhythm/pbio"
#define DELNO 4
#define LOCKFNM   "/home/staff/ad/www_public/code/biorhythm/lock##"

typedef struct {  /* structure for HTML name/value pair */
  char *name;
  char *val;
} entry; 
typedef struct date {
  int day;
  int month;    /* 1 to 12 */
  int year;     /* e.g. 1996 */
} Date;

Date start_date(entry entries[], int etnum, long int *start);
char *get_val(entry entries[], int etnum, char *field);
Date check_date(Date d);
long int toJulian(Date d);
int get_count();
void make_bpic(long int start, Date sd, int count);
void modify_plot(char *plotnm, char *tempnm, long int start, Date d);
/* Locking Utilities */
int my_lock(void);
void my_unlock(int fd);
/* Standard Form Functions */
void start_reply(char *title);
void cgi_errs();
int build_entries(entry entries[]);
char *find_value(entry entries[], int etnum, char *name);
/* Debugging Utilities */
int input_entries(entry entries[]);
void show_entries(entry entries[], int etnum);
/* CGI utilities written by Rob McCool */
char *makeword(char *line, char stop);
char *fmakeword(FILE *f, char stop, int *len);
void unescape_url(char *url);
char x2c(char *what);
void plustospace(char *str);

int main()
{
  entry entries[MAX_ENTRIES];
  int etnum = 0, count;
  Date sd;
  long int start;
  start_reply("Biorhythm Output");
  cgi_errs();
  etnum = build_entries(entries);
/*
  etnum = input_entries(entries);
  show_entries(entries, etnum);
*/
  sd = start_date(entries, etnum, &start);
  printf("<P>Number of days since your birth: %ld</P>\n", start);
  count = get_count();
  make_bpic(start, sd, count);
  printf("<P><img src=\"%s%d.gif\" alt=\"[No Graph]\"></P>\n", 
                                                 URL_PIC, count); 
  printf("</body></html>\n");
  return 1;
}

Date start_date(entry entries[], int etnum, long int *start)
/* Get the user's birthday (bd) and month/year of interest (sd)
   from the form details. Work out the number of days between
   bd and sd, and store it in start. sd is also returned.
*/
{
  char *val;
  Date bd, sd;
  long int bdjul, sdjul;
  val = get_val(entries, etnum, "birth");
  sscanf(val, "%d/%d/%d", &bd.day, &bd.month, &bd.year);
  bd = check_date(bd);
  printf("<P>Birthday: %d/%d/%d<br>\n", bd.day, bd.month, bd.year);
  bdjul = toJulian(bd);
  val =  get_val(entries, etnum, "bio");
  sscanf(val, "%d/%d", &sd.month, &sd.year);
  sd.day = 1;
  sd = check_date(sd);
  printf("Biorhythm month: %d/%d</P>\n", sd.month, sd.year);
  sdjul = toJulian(sd);
  if (sdjul >= bdjul)
    *start = sdjul - bdjul;
  else {
    printf("<P><b>Error</b>: your biorhythm month starts before your birthday;
adjusting it to be the birthday</P>\n");
    *start = 0;
    return bd;
  }
  return sd;
}

char *get_val(entry entries[], int etnum, char *field)
/* Terminate if no value has been entered */
{
  char *val;
  val = find_value(entries, etnum, field);
  if (strcmp(val, NOVAL) == 0) {
    printf("<P>Error: No value found in \"%s\" field</P>\n", field);
    printf("</body></html>\n");
    exit(1);
  }
  return val;
}

Date check_date(Date d)
/* Check the date for obvious mistakes */
{
  if (d.year < 0) {
    printf("<P><b>Error</b>: year cannot be less than 0; adjusting to 0</P>\n");
    d.year = 0;
  }
  if (d.year > 9000) {
    printf("<P><b>Error</b>: year cannot be more than 9000; adjusting to 9000</P>\n");
    d.year = 9000;
  }
  if (d.month < 1) {
    printf("<P><b>Error</b>: month cannot be less than 1; adjusting to 1</P>\n");
    d.month = 1;
  }
  if (d.month > 12) {
    printf("<P><b>Error</b>: month cannot be more than 12; adjusting to 12</P>\n");
    d.month = 12;
  }
  if (d.day < 1) {
    printf("<P><b>Error</b>: day cannot be less than 1; adjusting to 1</P>\n");
    d.day = 1;
  }
  if ((d.month == 1) || (d.month == 3) || (d.month == 5) ||
      (d.month == 7) || (d.month == 8) || (d.month == 10) ||
      (d.month == 12)) {
    if (d.day > 31) {
      printf("<P><b>Error</b>: day cannot be more than 31; adjusting to 31</P>\n");
      d.day = 31;
    }
  }
  else if ((d.month == 4) || (d.month == 6) || (d.month == 9) ||
           (d.month == 11)) {
     if (d.day > 30) {
       printf("<P><b>Error</b>: day cannot be more than 30; adjusting to 30</P>\n");
       d.day = 30;
     }
  }
  else if (d.month == 2) {
     /* february: don't bother with leap year checking */
     if (d.day > 29) {
       printf("<P><b>Error</b>: day cannot be more than 29; adjusting to 29</P>\n");
       d.day = 29;
     }
  }
  return d;
}

long int toJulian(Date d)
/* Calculates the Julian day for the given date.
   This code is taken from the function MdyToJulian() in
   Astrolog (v.5.10), available from ftp.magitech.com
   in the directory /pub/astrology/astrolog.
   It seems to be based on algorithms in Michael Erlewine's
   'Manual of Computer Programming for Astrologers',
   available from Matrix Software.
*/
{
  long im, jul;
  im = 12*((long)d.year+4800) + (long)d.month - 3;
  jul = (2*(im%12) + 7 + 365*im)/12;
  jul += (long)d.day + im/48 - 32083;
  if (jul > 2299171)                   /* Take care of dates in */
    jul += im/4800 - im/1200 + 38;     /* Gregorian calendar.   */
  return jul;
}

int get_count()
/* The count is extracted from COUNT_NM, and the value in
   the file is incremented. The use of lockf() means that
   lower-level UNIX file manipulation (e.g. open(), read())
   must be utilised.
*/
{
  char ln[MAXLEN];
  int fd, cnt, size;
  if ((fd = open(COUNT_NM, O_RDWR)) == -1) {   /* both read and write */
    printf("<P><b>Error</b>: Cannot open %s for changing</P>\n", COUNT_NM);
    printf("</body></html>\n");
    exit(1);
  }
  lockf(fd, F_LOCK, 0L);      /* (wait for) exclusive lock */
  size = read(fd, ln, MAXLEN);
  ln[size] = '\0';
  sscanf(ln, "%d\n", &cnt);
  lseek(fd, 0L, 0);           /* rewind to start of file */
  sprintf(ln, "%d\n", cnt+1);
  size = strlen(ln);
  write(fd, ln, size);
  lockf(fd, F_ULOCK, 0L);     /* unlock */
  close(fd);
  return cnt;
}

void make_bpic(long int start, Date sd, int count)
/* Housekeeping: remove an old biorhythm GIF.
   Creating a new biorhythm GIF involves making a new 
   version of the gnuplot commands in PLOT_CMDS by changing 
   start and the month/year of interest (sd), to create TEMP_CMDS. 
   TEMP_CMDS is evaluated by gnuplot to create PIC.
   PIC is in ".ppm" format, and so is converted into 
   a GIF with a unique name by using ppmtogif and count.
   Since TEMP_CMDS and PIC are not unique, use locking
   to stop any other invocations of pb from working on them
   at the same time: an 'exclusion' zone.
*/
{
  char cmd[MAXLEN];
  int lock_fd;
  /* Remove old biorhythm picture */
  sprintf(cmd, "%s%d.gif", PIC, count-DELNO);
  remove(cmd);
  lock_fd = my_lock();    /* start of 'exclusion' zone */
  modify_plot(PLOT_CMDS, TEMP_CMDS, start, sd);
  /* Plot the biorhythm */
  sprintf(cmd, "/usr/local/bin/gnuplot %s", TEMP_CMDS);
  system(cmd);
  /* Change the biorhythm into GIF form */
  sprintf(cmd, "/home/staff/ad/pbm/pbm-mundook/ppmtogif %s.ppm > %s%d.gif", 
                                    PIC, PIC, count);
  system(cmd);
  my_unlock(lock_fd);     /* end of 'exclusion' zone */
}

void modify_plot(char *plotnm, char *tempnm, long int start, Date d)
/* Open the template gnuplot commands file (plotnm) and modify it,
   saving the new version in tempnm. The changes are to replace '?'
   by start and '@' by d.
*/
{
  FILE *fp, *tfp;
  int c;
  if ((fp = fopen(plotnm, "r")) == NULL) {
    printf("<P><b>Error</b>: Cannot open %s for reading</P>\n", plotnm);
    printf("</body></html>\n");
    exit(1);
  }
  else if ((tfp = fopen(tempnm, "w")) == NULL) {
    printf("<P><b>Error</b>: Cannot open %s for writing</P>\n", tempnm);
    printf("</body></html>\n");
    exit(1);
  }
  else {
    while ((c = fgetc(fp)) != EOF) 
      if (c == '?')         /* marker for start digit */
        fprintf(tfp, "%ld", start);
      else if (c == '@')    /* marker for start date */
        fprintf(tfp, "%d/%d", d.month, d.year);
      else 
        fputc(c, tfp);
    fclose(fp);
    fclose(tfp);
  }
}

/* Locking Utilities */
int my_lock(void)
{
  int fd;
  if ((fd = open(LOCKFNM, O_RDWR)) == -1)
    printf("<P>Error: Cannot use locking</P>\n");
  else
    lockf(fd, F_LOCK, 0L);
  return fd;
}

void my_unlock(int fd)
{
  if (fd != -1) {
    lockf(fd, F_ULOCK, 0L);
    close(fd);
  }
}

/* Standard Form Functions */
void start_reply(char *title)
/* Output the HTML header, including the title and H1 container */
{
  printf("Content-type: text/html\n\n");
  printf("<html><head><title>%s</title></head>\n", title);
  printf("<body background=\"%s\">", BKGROUND);
  printf("<H1 align=center>%s</H1>\n", title);
  fflush(NULL);   /* force output to occur now */
}

void cgi_errs()
/* Check for errors in the REQUEST_METHOD and CONTENT_TYPE
   environment variables.
*/
{
  if(strcmp(getenv("REQUEST_METHOD"),"POST")) {
    printf("<P>This script should be referenced with a METHOD of POST.\n");
    printf("If you don't understand this, read ");
    printf("<A HREF=\"http://www.ncsa.uiuc.edu/SDG/Software/Mosaic/Docs
/fill-out-forms/overview.html\">forms overview</A>.</P>\n");
    printf("</body></html>");
    exit(1);
  }
  if(strcmp(getenv("CONTENT_TYPE"), "application/x-www-form-urlencoded")) {
    printf("<P>This script can only be used to decode form results.</P>\n");
    printf("</body></html>");
    exit(1);
  }
}

int build_entries(entry entries[])
/* Fill in the entries array with nameString=valueString 
   pairs from the form. 
*/
{
  int len, x;
  len = atoi(getenv("CONTENT_LENGTH"));
  for(x=0; len && (!feof(stdin)) && (x < MAX_ENTRIES); x++) {
    entries[x].val = fmakeword(stdin,'&',&len);
    plustospace(entries[x].val);
    unescape_url(entries[x].val);
    entries[x].name = makeword(entries[x].val,'=');
  }
  return x;  
}

char *find_value(entry entries[], int etnum, char *name)
/* Search through the entries array for a name string 
   which matches name and return its associated value 
   string. Otherwise return NOVAL.
*/
{
  int x;
  char *value;
  for(x=0; x < etnum; x++)
    if (strcmp(entries[x].name, name) == 0) {
      value = (char *)malloc(sizeof(char)*(strlen(entries[x].val)+1));
      strcpy(value, entries[x].val);
      break;
    }
  if ((x == etnum) || (value[0] == '\0')) {
    value = (char *)malloc(sizeof(char)*(strlen(NOVAL)+1));
    strcpy(value, NOVAL);
  }
  return value;
}

/* Debugging Utilities */
int input_entries(entry entries[])
/* Fill in the entries array with nameString=valueString pairs 
   entered by the user at the keyboard.
   Each pair should be entered in the format:
      nameString=valueString
   Terminate the input with <ctrl>d
*/
{
  int num = 0, namelen, linelen;
  char line[MAXLEN];
  printf("\nEnter name=value pairs:\n\n");
  while (gets(line) && (num < MAX_ENTRIES)) {
    linelen = strlen(line);
    namelen = strcspn(line, "=");
    entries[num].name =
      (char *)malloc(sizeof(char)*(namelen+1));
    strncpy(entries[num].name, line, namelen);
    entries[num].name[namelen] = '\0';
    entries[num].val =
      (char *)malloc(sizeof(char)*(linelen-namelen));
    strcpy(entries[num].val, &line[namelen+1]);
    num++;
  }
  return num;
}

void show_entries(entry entries[], int etnum)
/* Output the nameString=valueString pairs as an HTML 
   unnumbered list */
{
  int x;
  printf("<P>You submitted these nameString=valueString pairs:</P>\n");
  printf("<ul>\n");
  for(x=0; x < etnum; x++)
    printf("<li><code>%s = %s</code>\n", entries[x].name, entries[x].val);
  printf("</ul>\n");
  printf("</body></html>\n\n");
}

/* CGI Utilities by Rob McCool; 
   comments by Andrew Davison */
char *makeword(char *line, char stop) 
/* Builds a word by extracting characters from line up to the
   stopping character in stop (or until line is exhausted).
*/
{
  int x = 0, y = 0;
  char *word = (char *) malloc(sizeof(char) * (strlen(line) + 1));
  for(x=0; ((line[x]) && (line[x] != stop)); x++)
     word[x] = line[x];
  word[x] = '\0';
  if(line[x]) 
    ++x;
  while((line[y++] = line[x++]));
  return word;
}

char *fmakeword(FILE *f, char stop, int *len) 
/* Similar to makeword(): builds a word by extracting characters 
   from the file f up to the stopping character in stop (or
   until f is exhausted). The function is also supplied with
   the length of the string (len) left unread in f.
*/
{
  int wsize = 102400, ll = 0;
  char *word = (char *) malloc(sizeof(char) * (wsize + 1));
  while(1) {
    word[ll] = (char)fgetc(f);
    if(ll == wsize) {
       word[ll+1] = '\0';
       wsize += 102400;
       word = (char *)realloc(word,sizeof(char)*(wsize+1));
    }
    --(*len);
    if((word[ll] == stop) || (feof(f)) || (!(*len))) {
       if(word[ll] != stop) ll++;
       word[ll] = '\0';
       return word;
    }
    ++ll;
  }
}

void unescape_url(char *url) 
/* Convert the hexadecimal characters in url into ASCII.
   They are detected by starting with a '%'.
*/
{
  register int x, y;
  for(x=0,y=0; url[y]; ++x,++y) {
    if((url[x] = url[y]) == '%') {
      url[x] = x2c(&url[y+1]);
      y+=2;
    }
  }
  url[x] = '\0';
}

char x2c(char *what) 
/* Convert the hexadecimal (consisting of two characters) into
   a single ASCII character.
*/
{
  register char digit;
  digit = (what[0] >= 'A' ? ((what[0] & 0xdf) - 'A')+10 : (what[0] - '0'));
  digit *= 16;
  digit += (what[1] >= 'A' ? ((what[1] & 0xdf) - 'A')+10 : (what[1] - '0'));
  return(digit);
}

void plustospace(char *str) 
/* Replace '+'s by spaces in str */
{
  register int x;
  for(x=0; str[x]; x++) 
    if(str[x] == '+') str[x] = ' ';
}