Prev Up Next

In the example above, the parameter's name or the argument it assumed did not themselves contain any `&' or `=' characters. In general, they may. To accommodate such characters, and not have them be mistaken for separators, the CGI argument-passing mechanism treats all characters other than letters, digits, and the underscore, as special, and transmits them in an encoded form. A space is encoded as a `+'. For other special characters, the encoding is a three-character sequence, and consists of `%' followed the special character's hexadecimal code. Thus, the character sequence `20% + 30% = 50%, &c.' will be encoded as

20%25+%2b+30%25+%3d+50%25%2c+%26c%2e

(Space become `+'; `%' becomes `%25'; `+' becomes `%2b'; `=' becomes `%3d'; `,' becomes `%2c'; `&' becomes `%26'; and `.' becomes `%2e'.)

Instead of dealing anew with the task of getting and decoding the form data in each CGI script, it is convenient to collect some helpful procedures into a library file cgi.scm. testcgi2.scm can then be written more compactly as

#!/bin/sh
":";exec /usr/local/bin/mzscheme -r $0 "$@"

;Load the cgi utilities

(load-relatve "cgi.scm")

(display "content-type: text/plain") (newline)
(newline)

;Read the data input via the form

(parse-form-data)

;Get the envvar parameter

(define envvar (form-data-get/1 "envvar"))

;Display the value of the envvar

(display envvar)
(display " = ")
(display (getenv envvar))
(newline)

This shorter CGI script uses two utility procedures defined in cgi.scm. parse-form-data to read the data supplied by the user via the form. The data consists of parameters and their associated values. form-data-get/1 finds the value associated with a particular parameter.

cgi.scm defines a global table called *form-data-table* to store form data.

;Load our table definitions

(load-relative "table.scm")

;Define the *form-data-table*

(define *form-data-table* (make-table 'equ string=?))

An advantage of using a general mechanism such as the parse-form-data procedure is that we can hide the details of what method (get or put) was used.

(define parse-form-data
  (lambda ()
    ((if (string-ci=? (or (getenv "REQUEST_METHOD") "GET") "GET")
         parse-form-data-using-query-string
         parse-form-data-using-stdin))))

The environment variable REQUEST_METHOD tells which method was used to transmit the form data. If the method is GET, then the form data was sent as the string available via another environment variable, QUERY_STRING. The auxiliary procedure parse-form-data-using-query-string is used to pick apart QUERY_STRING:

(define parse-form-data-using-query-string
  (lambda ()
    (let ((query-string (or (getenv "QUERY_STRING") "")))
      (for-each
       (lambda (par=arg)
         (let ((par/arg (split #\= par=arg)))
           (let ((par (url-decode (car par/arg)))
                 (arg (url-decode (cadr par/arg))))
             (table-put! 
              *form-data-table* par
              (cons arg 
                    (table-get *form-data-table* par '()))))))
       (split #\& query-string)))))

The helper procedure split, and its helper string-index, are defined as in sec 17.2. As noted, the incoming form data is a sequence of name-value pairs separated by &s. Within each pair, the name comes first, followed by an = character, followed by the value. Each name-value combination is collected into a global table, the *form-data-table*.

Both name and value are encoded, so we need to decode them using the url-decode procedure to get their actual representation.

(define url-decode
  (lambda (s)
    (let ((s (string->list s)))
      (list->string
       (let loop ((s s))
         (if (null? s) '()
             (let ((a (car s)) (d (cdr s)))
               (case a
                 ((#\+) (cons #\space (loop d)))
                 ((#\%) (cons (hex->char (car d) (cadr d))
                              (loop (cddr d))))
                 (else (cons a (loop d)))))))))))

`+' is converted into space. A triliteral of the form `%xy' is converted, using the procedure hex->char into the character whose ascii encoding is the hex number `xy'.

(define hex->char
  (lambda (x y)
    (integer->char
     (string->number (string x y) 16))))

We still need a form-data parser for the case where the request method is POST. The auxiliary procedure parse-form-data-using-stdin does this.

(define parse-form-data-using-stdin
  (lambda ()
    (let* ((content-length (getenv "CONTENT_LENGTH"))
           (content-length (if content-length
                               (string->number content-length) 0))
           (i 0))
    (let par-loop ((par '()))
      (let ((c (read-char)))
        (set! i (+ i 1))
        (if (or (> i content-length) (eof-object? c) (char=? c #\=))
            (let arg-loop ((arg '()))
              (let ((c (read-char)))
                (set! i (+ i 1))
                (if (or (> i content-length) (eof-object? c) (char=? c #\&))
                    (let ((par (url-decode (list->string (reverse! par))))
                          (arg (url-decode (list->string (reverse! arg)))))
                      (table-put! *form-data-table* par
                                  (cons arg (table-get *form-data-table*
                                                       par '())))
                      (unless (or (> i content-length)
                                  (eof-object? c))
                        (par-loop '())))
                    (arg-loop (cons c arg)))))
            (par-loop (cons c par))))))))

The POST method sends form data via the script's stdin. The number of characters sent is placed in the environment variable CONTENT_LENGTH. parse-form-data-using-stdin reads the required number of characters from stdin, and populates the *form-data-table* as before, making sure to decode the parameters' names and values.

It remains to retrieve the values for specific parameters from the *form-data-table*. Note that the table associates a list with each parameter, in order to accommodate the possibility of multiple values for a parameter. form-data-get retrieves all the values assigned to a parameter. If there is only one value, it returns a singleton containing that value.

(define form-data-get
  (lambda (k)
    (table-get *form-data-table* k '())))

form-data-get/1 returns the first (or most significant) value associated with a parameter.

(define form-data-get/1
  (lambda (k . default)
    (let ((vv (form-data-get k)))
      (cond ((pair? vv) (car vv))
            ((pair? default) (car default))
            (else "")))))

In our examples so far, the CGI script has generated plain text. Generally, though, we will want to generate an HTML page. It is not uncommon for a combination of HTML form and CGI script to trigger a series of HTML pages with forms. It is also common to code all the action corresponding to these various forms in a single CGI script. In any case, it is helpful to have a utility procedure that writes out strings in HTML format, ie, with the HTML special characters encoded appropriately:

(define display-html
  (lambda (s . o)
    (let ((o (if (null? o) (current-output-port)
                 (car o))))
      (let ((n (string-length s)))
        (let loop ((i 0))
          (unless (>= i n)
            (let ((c (string-ref s i)))
              (display
               (case c
                 ((#\<) "&lt;")
                 ((#\>) "&gt;")
                 ((#\") "&quot;")
                 ((#\&) "&amp;")
                 (else c)) o)
              (loop (+ i 1)))))))))

Prev Up Next

Log in or register to write something here or to contact authors.