;;;; cgi.scm: Common Gateway Interface support for WWW scripts.

(define-module (www cgi)
  :use-module (www url))

;;;; 	Copyright (C) 1997 Free Software Foundation, Inc.
;;;; 
;;;; This program is free software; you can redistribute it and/or modify
;;;; it under the terms of the GNU General Public License as published by
;;;; the Free Software Foundation; either version 2, or (at your option)
;;;; any later version.
;;;; 
;;;; This program 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 General Public License for more details.
;;;; 
;;;; You should have received a copy of the GNU General Public License
;;;; along with this software; see the file COPYING.  If not, write to
;;;; the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
;;;; Boston, MA 02111-1307 USA
;;;; 

(define form-variables '())

;;; CGI environment variables.
;;; Should these all be public?

(define-public cgi-server-software-type #f)
(define-public cgi-server-software-version #f)
(define-public cgi-server-hostname #f)
(define-public cgi-gateway-interface #f)
(define-public cgi-server-protocol-name #f)
(define-public cgi-server-protocol-version #f)
(define-public cgi-server-port #f)
(define-public cgi-request-method #f)
(define-public cgi-path-info #f)
(define-public cgi-path-translated #f)
(define-public cgi-script-name #f)
(define-public cgi-query-string #f)
(define-public cgi-remote-host #f)
(define-public cgi-remote-addr #f)
(define-public cgi-authentication-type #f)
(define-public cgi-remote-user #f)
(define-public cgi-remote-ident #f)
(define-public cgi-content-type #f)
(define-public cgi-content-length #f)
(define-public cgi-http-accept-types #f)
(define-public cgi-http-user-agent #f)


;;; CGI high-level interface
;;;
;;; A typical CGI program will first call (cgi:init) to initialize
;;; the environment and read in any data returned from a form.  Form
;;; data can be extracted conveniently with these functions:
;;;
;;; (cgi:values NAME)
;;;	Fetch any values associated with NAME found in the form data.
;;;	Returned value is a list, even if it contains only one element.
;;; (cgi:value NAME)
;;;	Fetch only the CAR from (cgi:values NAME).  Convenient for when
;;;	you are certain that NAME is associated with only one value.

(define-public (cgi:init)
  (init-environment)
  (and cgi-content-length
       (parse-form (read-raw-form-data)))
  (and cgi-query-string
       (parse-form cgi-query-string)))

(define-public (cgi:values name)
  (assoc-ref form-variables name))

(define-public (cgi:value name)
  ;; syntactic sugar for obtaining just one value from a particular key
  (let ((values (cgi:values name)))
    (and values (car values))))

(define-public (cgi:names) (map car form-variables))

(define-public (cgi:form-data?) (not (null? form-variables)))


;;;; Internal functions.
;;;;
;;;; (parse-form DATA): parse DATA as raw form response data, adding
;;;;   values as necessary to `form-variables'.
;;;; (read-raw-form-data): read in `content-length' bytes from stdin
;;;; (init-environment): initialize CGI environment from Unix env vars.

(define (parse-form raw-data)
  ;; get-name and get-value are used to parse individual `name=value' pairs.
  ;; Values are URL-encoded, so url:decode must be called on each one.
  (define (get-name pair)
    (let ((p (string-index pair #\=)))
      (and p (substring pair 0 p))))
  (define (get-value pair)
    (let ((p (string-index pair #\=)))
      (and p (url:decode (substring pair (+ p 1))))))
  (for-each (lambda (pair)
	      (let* ((name (get-name pair))
		     (value (get-value pair))
		     (old-value (cgi:values name)))
		(set! form-variables
		      (assoc-set! form-variables
				  name
				  (cons value (or old-value '()))))))
	    (separate-fields-discarding-char #\& raw-data)))

(define (read-raw-form-data)
  (and cgi-content-length (read-n-chars cgi-content-length)))

(define (init-environment)

  ;; SERVER_SOFTWARE format: name/version
  (let ((server-software (getenv "SERVER_SOFTWARE")))
    (if server-software
	(let ((slash (string-index server-software #\/)))
	  (set! cgi-server-software-type    (substring server-software 0 slash))
	  (set! cgi-server-software-version (substring server-software (1+ slash))))))

  (set! cgi-server-hostname	   (getenv "SERVER_NAME"))
  (set! cgi-gateway-interface	   (getenv "GATEWAY_INTERFACE"));"CGI/revision"

  (let* ((server-protocol (getenv "SERVER_PROTOCOL")))
    (if server-protocol
	(let ((slash (string-index server-protocol #\/)))
	  (set! cgi-server-protocol-name     (substring server-protocol 0 slash))
	  (set! cgi-server-protocol-version  (substring server-protocol (1+ slash))))))

  (let ((port (getenv "SERVER_PORT")))
    (set! cgi-server-port (and port (string->number port))))

  (set! cgi-request-method	   (getenv "REQUEST_METHOD"))
  (set! cgi-path-info		   (getenv "PATH_INFO"))
  (set! cgi-path-translated	   (getenv "PATH_TRANSLATED"))
  (set! cgi-script-name		   (getenv "SCRIPT_NAME"))
  (set! cgi-remote-host		   (getenv "REMOTE_HOST"))
  (set! cgi-remote-addr		   (getenv "REMOTE_ADDR"))
  (set! cgi-authentication-type	   (getenv "AUTH_TYPE"))
  (set! cgi-remote-user		   (getenv "REMOTE_USER"))
  (set! cgi-remote-ident	   (getenv "REMOTE_IDENT"))
  (set! cgi-content-type	   (getenv "CONTENT_TYPE"))
  (set! cgi-query-string	   (getenv "QUERY_STRING"))

  (and cgi-query-string
       (string-null? cgi-query-string)
       (set! cgi-query-string #f))

  (let ((contlen (getenv "CONTENT_LENGTH")))
    (set! cgi-content-length (and contlen (string->number contlen))))

  ;; HTTP_ACCEPT is a list of MIME types separated by commas.
  (let ((types (getenv "HTTP_ACCEPT")))
    (set! cgi-http-accept-types
	  (and types (separate-fields-discarding-char #\, types))))

  ;; HTTP_USER_AGENT format: software/version library/version.
  (set! cgi-http-user-agent		   (getenv "HTTP_USER_AGENT")))


;;; System I/O and low-level stuff.

(define (read-n-chars num . port-arg)
  (let ((p (if (null? port-arg)
	       (current-input-port)
	       (car port-arg)))
	(s (make-string num)))
    (do ((i   0              (+ i 1))
	 (ch  (read-char p)  (read-char p)))
	((or (>= i num) (eof-object? ch)) s)
      (string-set! s i ch))))

;; This is defined in #/ice-9/string-fun, but the interface is
;; weird, the semantics perverse, and it doesn't work.  We use
;; a working copy here.
(define (separate-fields-discarding-char ch str)
  (let loop ((fields '())
             (str str))
    (let ((pos (string-rindex str ch)))
      (if pos
	  (loop (cons (substring str (+ 1 pos)) fields)
		(substring str 0 pos))
	  (cons str fields)))))
