/*  $Id$

    Part of SWI-Prolog

    Author:        Jan Wielemaker
    E-mail:        wielemak@science.uva.nl
    WWW:           http://www.swi-prolog.org
    Copyright (C): 1985-2007, University of Amsterdam

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Lesser General Public
    License as published by the Free Software Foundation; either
    version 2.1 of the License, or (at your option) any later version.

    This library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public
    License along with this library; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <SWI-Prolog.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <ctype.h>
#include <errno.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <string.h>
#include <assert.h>
#include "form.h"
#ifdef __WINDOWS__
#include <io.h>
#endif

#include "error.h"

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Breaks a string holding data from a WWW form into its values.  Outputs a
sequence of NAME=VALUE commands for a shell.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

static int
dehex(int chr)
{ chr &= 0xff;

  if ( chr >= '0' && chr <= '9' )
    return chr - '0';
  if ( chr >= 'A' && chr <= 'F' )
    return chr - 'A' + 10;
  if ( chr >= 'a' && chr <= 'f' )
    return chr - 'a' + 10;

  return -1;
}


static size_t
form_argument_decode(const char *in, size_t inlen, char *out, size_t outlen)
{ const char *ein  = in+inlen;
  size_t written = 0;

  for(; in < ein; in++)
  { switch(*in)
    { case '+':
	if ( ++written < outlen )
	  *out++ = ' ';
        break;
      case '%':
	if ( in+2 < ein )
	{ int h1 = dehex(*(++in));
	  int h2 = dehex(*(++in));

	  if ( h1 < 0 || h2 < 0 )
	    return (size_t)-1;

	  if ( ++written < outlen )
	    *out++ = h1<<4|h2;
	} else
	  return (size_t)-1;		/* syntax error */
	break;
      default:
	if ( ++written < outlen )
	  *out++ = *in;
        break;
    }
  }

  if ( written < outlen )
    *out++ = '\0';

  return written;
}


#define SHORTVALUE 512

int
break_form_argument(const char *formdata,
		    int (*func)(const char* name,
				size_t namelen,
				const char *value,
				size_t valuelen,
				void *closure),
		    void *closure)
{ while ( *formdata )
  { char value[SHORTVALUE];
    char *eq = strchr(formdata, '=');

    if ( eq )
    { size_t nlen = eq-formdata;
      char *end;
      size_t vlen;

      eq++;
      end = strchr(eq, '&');
      if ( !end )
	end = eq+strlen(eq);		/* end of the string */

      if ( (vlen=form_argument_decode(eq, end-eq, value, SHORTVALUE)) >= SHORTVALUE )
      { char *buf;

	if ( (buf=malloc(vlen+1)) )
	{ size_t vlen2 = form_argument_decode(eq, end-eq, buf, vlen+1);
	  int rc;

	  assert(vlen2 == vlen);
	  rc = (func)(formdata, nlen, buf, vlen2, closure);
	  free(buf);

	  if ( !rc )
	    return rc;
	} else
	  return ERROR_NOMEM;
      } else if ( vlen == (size_t)-1 )
      { return ERROR_SYNTAX_ERROR;
      } else
      { int rc = (func)(formdata, nlen, value, vlen, closure);

	if ( !rc )
	  return rc;
      }

      if ( *end )
	formdata = end+1;
      else
	formdata = end;
    }
  }

  return TRUE;
}


static char *
find_boundary(const char *data, const char *end, const char *boundary)
{ size_t blen = strlen(boundary);

  while ( data < end &&
	  !(strncmp(data, boundary, blen) == 0) )
    data++;

  if ( data < end )
  { while(data[-1] == '-')
      data--;
    return (char *)data;
  }

  return NULL;
}


/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Find a named attribute in a mime header of a multipart form
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

static char *
attribute_of_multipart_header(const char *name, char *header, char *endheader)
{ char *value;
  size_t nlen = strlen(name);

  while( header < endheader &&
	 !(header[nlen] == '=' && strncmp(header, name, nlen) == 0) )
    header++;

  if ( header < endheader )
  { header += nlen+1;

    if ( header[0] == '"' )
    { char *end;

      value = ++header;
      if ( (end = strchr(value, '"')) )
      { *end = '\0';
        return value;
      }
    } else
    { char *end;

      value = header;

      for(end=header; *end && isalnum(*end&0xff); end++)
	;
      *end = '\0';
      return value;
    }
  }

  return NULL;
}


static char *
looking_at_blank_lines(const char *line, int n)
{ while(n-- > 0)
  { if ( (line[0] == '\r' && line[1] == '\n') )
      line += 2;
    else if ( line[0] == '\n' )
      line += 1;
    else
      return NULL;
  }

  return (char *)line;
}


char *
next_line(const char *in)
{ if ( (in = strchr(in, '\n')) )
    return (char *)(in+1);

  return NULL;
}


int
break_multipart(char *formdata, size_t len,
		const char *boundary,
		int (*func)(const char *name,
			    size_t namelen,
			    const char *value,
			    size_t valuelen,
			    const char *filename,
			    void *closure),
		void *closure)
{ char *enddata = formdata+len;

  while(formdata < enddata)
  { char *header;
    char *name, *filename;
    char *data = NULL;
    char *end;

    if ( !(formdata=find_boundary(formdata, enddata, boundary)) ||
	 !(formdata=next_line(formdata)) )
      break;

    header = formdata;
					/* find the end of the header */
    for( ; formdata < enddata; formdata++ )
    { char *end;

      if ( (end = looking_at_blank_lines(formdata, 2)) )
      { formdata[0] = '\0';
	formdata = data = end;
	break;
      }
    }

    if ( !data )
      break;

    if ( !(name = attribute_of_multipart_header("name", header, data)) )
    { term_t t = PL_new_term_ref();
      PL_put_atom_chars(t, "name");

      return pl_error(NULL, 0, NULL, ERR_EXISTENCE, "field", t);
    }
    filename = attribute_of_multipart_header("filename", header, data);

    if ( !(formdata=find_boundary(data, enddata, boundary)) )
      break;
    end = formdata-1;
    if ( end[-1] == '\r' )
      end--;
    end[0] = '\0';

    if ( !(func)(name, strlen(name), data, end-data, filename, closure) )
      return FALSE;
  }

  return TRUE;
}


/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Get the raw data from the standard   input or QUERY_STRING. If `lenp' is
provided, it is filled with the length  of the contents. The input value
for lenp is the maximum acceptable content-length.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

int
get_raw_form_data(char **data, size_t *lenp, int *must_free)
{ char *method;
  char *s;

  if ( (method = getenv("REQUEST_METHOD")) &&
       strcmp(method, "POST") == 0 )
  { char *lenvar = getenv("CONTENT_LENGTH");
    char *q;
    long len;

    if ( !lenvar )
    { term_t env = PL_new_term_ref();
      PL_put_atom_chars(env, "CONTENT_LENGTH");

      return pl_error(NULL, 0, NULL, ERR_EXISTENCE, "environment", env);
    }
    len = atol(lenvar);
    if ( len < 0 )
    { term_t t = PL_new_term_ref();

      if ( !PL_put_integer(t, len) )
	return FALSE;
      return pl_error(NULL, 0, "< 0", ERR_DOMAIN, t, "content_length");
    }
    if ( lenp )
    { if ( *lenp && (size_t)len > *lenp )
      { term_t t = PL_new_term_ref();
	char msg[100];

	if ( !PL_put_integer(t, len) )
	  return FALSE;
	sprintf(msg, "> %ld", *lenp);

	return pl_error(NULL, 0, msg, ERR_DOMAIN, t, "content_length");
      }
      *lenp = len;
    }

    q = s = malloc(len+1);
    if ( !q )
      return pl_error(NULL, 0, NULL, ERR_RESOURCE, "memory");
    while(len > 0)
    { int done;

      while( (done=read(fileno(stdin), q, len)) > 0 )
      { q+=done;
	len-=done;
      }
      if ( done < 0 )
      { int e;
	term_t obj;

      no_data:
	e = errno;
	obj = PL_new_term_ref();

	free(s);
	PL_put_nil(obj);
	return pl_error(NULL, 0, NULL, ERR_ERRNO, e, "read", "cgi_data", obj);
      }
    }
    if ( len == 0 )
    { *q = '\0';
      *data = s;
      *must_free = TRUE;
      return TRUE;
    } else
      goto no_data;
  } else if ( (s = getenv("QUERY_STRING")) )
  { if ( lenp )
      *lenp = strlen(s);
    *data = s;
    *must_free = FALSE;
    return TRUE;
  } else
  { term_t env = PL_new_term_ref();
    PL_put_atom_chars(env, "QUERY_STRING");

    return pl_error(NULL, 0, NULL, ERR_EXISTENCE, "environment", env);
  }
}