]> granicus.if.org Git - python/commitdiff
Reformatted with 4-space indentation. Added some quick docs to the
authorGuido van Rossum <guido@python.org>
Thu, 7 Mar 1996 18:00:44 +0000 (18:00 +0000)
committerGuido van Rossum <guido@python.org>
Thu, 7 Mar 1996 18:00:44 +0000 (18:00 +0000)
FieldStorage class.

Lib/cgi.py

index fd4ce32aeed818cf6fedbddcadcbc383ee7ac8e3..37deabd5860f1196b1dc8a900d563a0d3ad5152c 100755 (executable)
@@ -2,8 +2,8 @@
 
 """Support module for CGI (Common Gateway Interface) scripts.
 
-This module defines a number of utilities for use by CGI scripts written in 
-Python.
+This module defines a number of utilities for use by CGI scripts
+written in Python.
 
 
 Introduction
@@ -78,10 +78,15 @@ If you have an input item of type "file" in your form and the client
 supports file uploads, the value for that field, if present in the
 form, is not a string but a tuple of (filename, content-type, data).
 
+A more flexible alternative to [Sv]FormContentDict is the class
+FieldStorage.  See that class's doc string.
+
 
 Overview of classes
 -------------------
 
+FieldStorage: new more flexible class; described above.
+
 SvFormContentDict: single value form content as dictionary; described
 above.
 
@@ -352,155 +357,155 @@ environ = os.environ
 # =================
 
 def parse(fp=None):
-       """Parse a query in the environment or from a file (default stdin)"""
-       if not fp:
-               fp = sys.stdin
-       if not environ.has_key('REQUEST_METHOD'):
-               environ['REQUEST_METHOD'] = 'GET'       # For testing
-       if environ['REQUEST_METHOD'] == 'POST':
-               ctype, pdict = parse_header(environ['CONTENT_TYPE'])
-               if ctype == 'multipart/form-data':
-                       return parse_multipart(fp, ctype, pdict)
-               elif ctype == 'application/x-www-form-urlencoded':
-                       clength = string.atoi(environ['CONTENT_LENGTH'])
-                       qs = fp.read(clength)
-               else:
-                       qs = ''         # Bad content-type
-               environ['QUERY_STRING'] = qs
-       elif environ.has_key('QUERY_STRING'):
-               qs = environ['QUERY_STRING']
+    """Parse a query in the environment or from a file (default stdin)"""
+    if not fp:
+       fp = sys.stdin
+    if not environ.has_key('REQUEST_METHOD'):
+       environ['REQUEST_METHOD'] = 'GET'       # For testing stand-alone
+    if environ['REQUEST_METHOD'] == 'POST':
+       ctype, pdict = parse_header(environ['CONTENT_TYPE'])
+       if ctype == 'multipart/form-data':
+           return parse_multipart(fp, ctype, pdict)
+       elif ctype == 'application/x-www-form-urlencoded':
+           clength = string.atoi(environ['CONTENT_LENGTH'])
+           qs = fp.read(clength)
        else:
-               if sys.argv[1:]:
-                       qs = sys.argv[1]
-               else:
-                       qs = ""
-               environ['QUERY_STRING'] = qs
-       return parse_qs(qs)
+           qs = ''             # Bad content-type
+       environ['QUERY_STRING'] = qs    # XXX Shouldn't, really
+    elif environ.has_key('QUERY_STRING'):
+       qs = environ['QUERY_STRING']
+    else:
+       if sys.argv[1:]:
+           qs = sys.argv[1]
+       else:
+           qs = ""
+       environ['QUERY_STRING'] = qs    # XXX Shouldn't, really
+    return parse_qs(qs)
 
 
 def parse_qs(qs):
-       """Parse a query given as a string argument"""
-       name_value_pairs = string.splitfields(qs, '&')
-       dict = {}
-       for name_value in name_value_pairs:
-               nv = string.splitfields(name_value, '=')
-               if len(nv) != 2:
-                       continue
-               name = nv[0]
-               value = urllib.unquote(regsub.gsub('+', ' ', nv[1]))
-               if len(value):
-                       if dict.has_key (name):
-                               dict[name].append(value)
-                       else:
-                               dict[name] = [value]
-       return dict
+    """Parse a query given as a string argument"""
+    name_value_pairs = string.splitfields(qs, '&')
+    dict = {}
+    for name_value in name_value_pairs:
+       nv = string.splitfields(name_value, '=')
+       if len(nv) != 2:
+           continue
+       name = nv[0]
+       value = urllib.unquote(regsub.gsub('+', ' ', nv[1]))
+       if len(value):
+           if dict.has_key (name):
+               dict[name].append(value)
+           else:
+               dict[name] = [value]
+    return dict
 
 
 def parse_multipart(fp, ctype, pdict):
-       """Parse multipart input.
-
-       Arguments:
-       fp   : input file
-       ctype: content-type
-       pdict: dictionary containing other parameters of conten-type header
-
-       Returns a dictionary just like parse_qs() (keys are the field
-       names, each value is a list of values for that field) except
-       that if the value was an uploaded file, it is a tuple of the
-       form (filename, content-type, data).  Note that content-type
-       is the raw, unparsed contents of the content-type header.
-
-       XXX Should we parse further when the content-type is
-       multipart/*?
-
-       """
-       import mimetools
-       if pdict.has_key('boundary'):
-               boundary = pdict['boundary']
+    """Parse multipart input.
+
+    Arguments:
+    fp   : input file
+    ctype: content-type
+    pdict: dictionary containing other parameters of conten-type header
+
+    Returns a dictionary just like parse_qs() (keys are the field
+    names, each value is a list of values for that field) except that
+    if the value was an uploaded file, it is a tuple of the form
+    (filename, content-type, data).  Note that content-type is the
+    raw, unparsed contents of the content-type header.
+
+    XXX Should we parse further when the content-type is
+    multipart/*?
+
+    """
+    import mimetools
+    if pdict.has_key('boundary'):
+       boundary = pdict['boundary']
+    else:
+       boundary = ""
+    nextpart = "--" + boundary
+    lastpart = "--" + boundary + "--"
+    partdict = {}
+    terminator = ""
+
+    while terminator != lastpart:
+       bytes = -1
+       data = None
+       if terminator:
+           # At start of next part.  Read headers first.
+           headers = mimetools.Message(fp)
+           clength = headers.getheader('content-length')
+           if clength:
+               try:
+                   bytes = string.atoi(clength)
+               except string.atoi_error:
+                   pass
+           if bytes > 0:
+               data = fp.read(bytes)
+           else:
+               data = ""
+       # Read lines until end of part.
+       lines = []
+       while 1:
+           line = fp.readline()
+           if not line:
+               terminator = lastpart # End outer loop
+               break
+           if line[:2] == "--":
+               terminator = string.strip(line)
+               if terminator in (nextpart, lastpart):
+                   break
+           if line[-2:] == '\r\n':
+               line = line[:-2]
+           elif line[-1:] == '\n':
+               line = line[:-1]
+           lines.append(line)
+       # Done with part.
+       if data is None:
+           continue
+       if bytes < 0:
+           data = string.joinfields(lines, "\n")
+       line = headers['content-disposition']
+       if not line:
+           continue
+       key, params = parse_header(line)
+       if key != 'form-data':
+           continue
+       if params.has_key('name'):
+           name = params['name']
        else:
-               boundary = ""
-       nextpart = "--" + boundary
-       lastpart = "--" + boundary + "--"
-       partdict = {}
-       terminator = ""
-
-       while terminator != lastpart:
-               bytes = -1
-               data = None
-               if terminator:
-                       # At start of next part.  Read headers first.
-                       headers = mimetools.Message(fp)
-                       clength = headers.getheader('content-length')
-                       if clength:
-                               try:
-                                       bytes = string.atoi(clength)
-                               except string.atoi_error:
-                                       pass
-                       if bytes > 0:
-                               data = fp.read(bytes)
-                       else:
-                               data = ""
-               # Read lines until end of part.
-               lines = []
-               while 1:
-                       line = fp.readline()
-                       if not line:
-                               terminator = lastpart # End outer loop
-                               break
-                       if line[:2] == "--":
-                               terminator = string.strip(line)
-                               if terminator in (nextpart, lastpart):
-                                       break
-                       if line[-2:] == '\r\n':
-                               line = line[:-2]
-                       elif line[-1:] == '\n':
-                               line = line[:-1]
-                       lines.append(line)
-               # Done with part.
-               if data is None:
-                       continue
-               if bytes < 0:
-                       data = string.joinfields(lines, "\n")
-               line = headers['content-disposition']
-               if not line:
-                       continue
-               key, params = parse_header(line)
-               if key != 'form-data':
-                       continue
-               if params.has_key('name'):
-                       name = params['name']
-               else:
-                       continue
-               if params.has_key('filename'):
-                       data = (params['filename'],
-                               headers.getheader('content-type'), data)
-               if partdict.has_key(name):
-                       partdict[name].append(data)
-               else:
-                       partdict[name] = [data]
-
-       return partdict
+           continue
+       if params.has_key('filename'):
+           data = (params['filename'],
+                   headers.getheader('content-type'), data)
+       if partdict.has_key(name):
+           partdict[name].append(data)
+       else:
+           partdict[name] = [data]
+
+    return partdict
 
 
 def parse_header(line):
-       """Parse a Content-type like header.
-       
-       Return the main content-type and a dictionary of options.
-       
-       """
-       plist = map(string.strip, string.splitfields(line, ';'))
-       key = string.lower(plist[0])
-       del plist[0]
-       pdict = {}
-       for p in plist:
-               i = string.find(p, '=')
-               if i >= 0:
-                       name = string.lower(string.strip(p[:i]))
-                       value = string.strip(p[i+1:])
-                       if len(value) >= 2 and value[0] == value[-1] == '"':
-                               value = value[1:-1]
-                       pdict[name] = value
-       return key, pdict
+    """Parse a Content-type like header.
+
+    Return the main content-type and a dictionary of options.
+
+    """
+    plist = map(string.strip, string.splitfields(line, ';'))
+    key = string.lower(plist[0])
+    del plist[0]
+    pdict = {}
+    for p in plist:
+       i = string.find(p, '=')
+       if i >= 0:
+           name = string.lower(string.strip(p[:i]))
+           value = string.strip(p[i+1:])
+           if len(value) >= 2 and value[0] == value[-1] == '"':
+               value = value[1:-1]
+           pdict[name] = value
+    return key, pdict
 
 
 # Classes for field storage
@@ -508,444 +513,508 @@ def parse_header(line):
 
 class MiniFieldStorage:
 
-       """Internal: dummy FieldStorage, used with query string format."""
+    """Internal: dummy FieldStorage, used with query string format."""
 
-       def __init__(self, name, value):
-               """Constructor from field name and value."""
-               self.name = name
-               self.value = value
-               from StringIO import StringIO
-               self.filename = None
-               self.list = None
-               self.file = StringIO(value)
+    # Dummy attributes
+    filename = None
+    list = None
+    type = None
+    typ_options = {}
+    disposition = None
+    disposition_options = {}
+    headers = {}
 
-       def __repr__(self):
-               """Return printable representation."""
-               return "MiniFieldStorage(%s, %s)" % (`self.name`,
-                                                    `self.value`)
+    def __init__(self, name, value):
+       """Constructor from field name and value."""
+       from StringIO import StringIO
+       self.name = name
+       self.value = value
+       self.file = StringIO(value)
+
+    def __repr__(self):
+       """Return printable representation."""
+       return "MiniFieldStorage(%s, %s)" % (`self.name`, `self.value`)
 
 
 class FieldStorage:
 
-       """Store a sequence of fields, reading multipart/form-data."""
-
-       def __init__(self, fp=None, headers=None, outerboundary=""):
-               """Constructor.  Read multipart/* until last part."""
-               method = None
-               if environ.has_key('REQUEST_METHOD'):
-                       method = string.upper(environ['REQUEST_METHOD'])
-               if not fp and method == 'GET':
-                       qs = None
-                       if environ.has_key('QUERY_STRING'):
-                               qs = environ['QUERY_STRING']
-                       from StringIO import StringIO
-                       fp = StringIO(qs or "")
-                       if headers is None:
-                               headers = {'content-type':
-                                          "application/x-www-form-urlencoded"}
-               if headers is None:
-                       headers = {}
-                       if environ.has_key('CONTENT_TYPE'):
-                               headers['content-type'] = environ['CONTENT_TYPE']
-                       if environ.has_key('CONTENT_LENGTH'):
-                               headers['content-length'] = environ['CONTENT_LENGTH']
-               self.fp = fp or sys.stdin
-               self.headers = headers
-               self.outerboundary = outerboundary
-               
-               # Process content-disposition header
-               cdisp, pdict = "", {}
-               if self.headers.has_key('content-disposition'):
-                       cdisp, pdict = parse_header(self.headers['content-disposition'])
-               self.disposition = cdisp
-               self.disposition_options = pdict
-               self.name = None
-               if pdict.has_key('name'):
-                       self.name = pdict['name']
-               self.filename = None
-               if pdict.has_key('filename'):
-                       self.filename = pdict['filename']
-               
-               # Process content-type header
-               ctype, pdict = "text/plain", {}
-               if self.headers.has_key('content-type'):
-                       ctype, pdict = parse_header(self.headers['content-type'])
-               self.type = ctype
-               self.type_options = pdict
-               self.innerboundary = ""
-               if pdict.has_key('boundary'):
-                       self.innerboundary = pdict['boundary']
-               clen = -1
-               if self.headers.has_key('content-length'):
-                       try:
-                               clen = string.atoi(self.headers['content-length'])
-                       except:
-                               pass
-               self.length = clen
-
-               self.list = self.file = None
-               self.done = 0
-               self.lines = []
-               if ctype == 'application/x-www-form-urlencoded':
-                       self.read_urlencoded()
-               elif ctype[:10] == 'multipart/':
-                       self.read_multi()
-               else:
-                       self.read_single()
-       
-       def __repr__(self):
-               """Return a printable representation."""
-               return "FieldStorage(%s, %s, %s)" % (
-                       `self.name`, `self.filename`, `self.value`)
-
-       def __getattr__(self, name):
-               if name != 'value':
-                       raise AttributeError, name
-               if self.file:
-                       self.file.seek(0)
-                       value = self.file.read()
-                       self.file.seek(0)
-               elif self.list is not None:
-                       value = self.list
-               else:
-                       value = None
-               return value
-       
-       def __getitem__(self, key):
-               """Dictionary style indexing."""
-               if self.list is None:
-                       raise TypeError, "not indexable"
-               found = []
-               for item in self.list:
-                       if item.name == key: found.append(item)
-               if not found:
-                       raise KeyError, key
-               return found
-       
-       def keys(self):
-               """Dictionary style keys() method."""
-               if self.list is None:
-                       raise TypeError, "not indexable"
-               keys = []
-               for item in self.list:
-                       if item.name not in keys: keys.append(item.name)
-               return keys
-
-       def read_urlencoded(self):
-               """Internal: read data in query string format."""
-               qs = self.fp.read(self.length)
-               dict = parse_qs(qs)
-               self.list = []
-               for key, valuelist in dict.items():
-                       for value in valuelist:
-                               self.list.append(MiniFieldStorage(key, value))
-               self.skip_lines()
-       
-       def read_multi(self):
-               """Internal: read a part that is itself multipart."""
-               import rfc822
-               self.list = []
-               part = self.__class__(self.fp, {}, self.innerboundary)
-               # Throw first part away
-               while not part.done:
-                       headers = rfc822.Message(self.fp)
-                       part = self.__class__(self.fp, headers, self.innerboundary)
-                       self.list.append(part)
-               self.skip_lines()
-       
-       def read_single(self):
-               """Internal: read an atomic part."""
-               if self.length >= 0:
-                       self.read_binary()
-                       self.skip_lines()
-               else:
-                       self.read_lines()
-               self.file.seek(0)
-       
-       bufsize = 8*1024                # I/O buffering size for copy to file
-       
-       def read_binary(self):
-               """Internal: read binary data."""
-               self.file = self.make_file('b')
-               todo = self.length
-               if todo >= 0:
-                       while todo > 0:
-                               data = self.fp.read(min(todo, self.bufsize))
-                               if not data:
-                                       self.done = -1
-                                       break
-                               self.file.write(data)
-                               todo = todo - len(data)
-       
-       def read_lines(self):
-               """Internal: read lines until EOF or outerboundary."""
-               self.file = self.make_file('')
-               if self.outerboundary:
-                       self.read_lines_to_outerboundary()
-               else:
-                       self.read_lines_to_eof()
-       
-       def read_lines_to_eof(self):
-               """Internal: read lines until EOF."""
-               while 1:
-                       line = self.fp.readline()
-                       if not line:
-                               self.done = -1
-                               break
-                       self.lines.append(line)
-                       if line[-2:] == '\r\n':
-                               line = line[:-2] + '\n'
-                       self.file.write(line)
-       
-       def read_lines_to_outerboundary(self):
-               """Internal: read lines until outerboundary."""
-               next = "--" + self.outerboundary
-               last = next + "--"
-               delim = ""
-               while 1:
-                       line = self.fp.readline()
-                       if not line:
-                               self.done = -1
-                               break
-                       self.lines.append(line)
-                       if line[:2] == "--":
-                               strippedline = string.strip(line)
-                               if strippedline == next:
-                                       break
-                               if strippedline == last:
-                                       self.done = 1
-                                       break
-                       if line[-2:] == "\r\n":
-                               line = line[:-2]
-                       elif line[-1] == "\n":
-                               line = line[:-1]
-                       self.file.write(delim + line)
-                       delim = "\n"
-       
-       def skip_lines(self):
-               """Internal: skip lines until outer boundary if defined."""
-               if not self.outerboundary or self.done:
-                       return
-               next = "--" + self.outerboundary
-               last = next + "--"
-               while 1:
-                       line = self.fp.readline()
-                       if not line:
-                               self.done = -1
-                               break
-                       self.lines.append(line)
-                       if line[:2] == "--":
-                               strippedline = string.strip(line)
-                               if strippedline == next:
-                                       break
-                               if strippedline == last:
-                                       self.done = 1
-                                       break
-       
-       def make_file(self, binary):
-               """Overridable: return a readable & writable file.
-               
-               The file will be used as follows:
-               - data is written to it
-               - seek(0)
-               - data is read from it
-               
-               The 'binary' argument is 'b' if the file should be created in
-               binary mode (on non-Unix systems), '' otherwise.
-               
-               The intention is that you can override this method to selectively
-               create a real (temporary) file or use a memory file dependent on
-               the perceived size of the file or the presence of a filename, etc.
-               
-               """
-               
-               # Prefer ArrayIO over StringIO, if it's available
-               try:
-                       from ArrayIO import ArrayIO
-                       ioclass = ArrayIO
-               except ImportError:
-                       from StringIO import StringIO
-                       ioclass = StringIO
-               return ioclass()
+    """Store a sequence of fields, reading multipart/form-data.
+
+    This class provides naming, typing, files stored on disk, and
+    more.  At the top level, it is accessible like a dictionary, whose
+    keys are the field names.  (Note: None can occur as a field name.)
+    The items are either a Python list (if there's multiple values) or
+    another FieldStorage or MiniFieldStorage object.  If it's a single
+    object, it has the following attributes:
+
+    name: the field name, if specified; otherwise None
+
+    filename: the filename, if specified; otherwise None; this is the
+       client side filename, *not* the file name on which it is
+       stored (that's a temporary you don't deal with)
+
+    value: the value as a *string*; for file uploads, this
+       transparently reads the file every time you request the value
+
+    file: the file(-like) object from which you can read the data;
+       None if the data is stored a simple string
+
+    type: the content-type, or None if not specified
+
+    type_options: dictionary of options specified on the content-type
+       line
+
+    disposition: content-disposition, or None if not specified
+
+    disposition_options: dictionary of corresponding options
+
+    headers: a dictionary(-like) object (sometimes rfc822.Message or a
+       subclass thereof) containing *all* headers
+
+    The class is subclassable, mostly for the purpose of overriding
+    the make_file() method, which is called internally to come up with
+    a file open for reading and writing.  This makes it possible to
+    override the default choice of storing all files in a temporary
+    directory and unlinking them as soon as they have been opened.
+
+    """
+
+    def __init__(self, fp=None, headers=None, outerboundary=""):
+       """Constructor.  Read multipart/* until last part.
+
+       Arguments, all optional:
+
+       fp              : file pointer; default: sys.stdin
+
+       headers         : header dictionary-like object; default:
+           taken from environ as per CGI spec
+
+       outerboundary   : optional terminating multipart boundary
+           (for internal use only)
+
+       """
+       method = None
+       if environ.has_key('REQUEST_METHOD'):
+           method = string.upper(environ['REQUEST_METHOD'])
+       if not fp and method == 'GET':
+           qs = None
+           if environ.has_key('QUERY_STRING'):
+               qs = environ['QUERY_STRING']
+           from StringIO import StringIO
+           fp = StringIO(qs or "")
+           if headers is None:
+               headers = {'content-type':
+                          "application/x-www-form-urlencoded"}
+       if headers is None:
+           headers = {}
+           if environ.has_key('CONTENT_TYPE'):
+               headers['content-type'] = environ['CONTENT_TYPE']
+           if environ.has_key('CONTENT_LENGTH'):
+               headers['content-length'] = environ['CONTENT_LENGTH']
+       self.fp = fp or sys.stdin
+       self.headers = headers
+       self.outerboundary = outerboundary
+
+       # Process content-disposition header
+       cdisp, pdict = "", {}
+       if self.headers.has_key('content-disposition'):
+           cdisp, pdict = parse_header(self.headers['content-disposition'])
+       self.disposition = cdisp
+       self.disposition_options = pdict
+       self.name = None
+       if pdict.has_key('name'):
+           self.name = pdict['name']
+       self.filename = None
+       if pdict.has_key('filename'):
+           self.filename = pdict['filename']
+
+       # Process content-type header
+       ctype, pdict = "text/plain", {}
+       if self.headers.has_key('content-type'):
+           ctype, pdict = parse_header(self.headers['content-type'])
+       self.type = ctype
+       self.type_options = pdict
+       self.innerboundary = ""
+       if pdict.has_key('boundary'):
+           self.innerboundary = pdict['boundary']
+       clen = -1
+       if self.headers.has_key('content-length'):
+           try:
+               clen = string.atoi(self.headers['content-length'])
+           except:
+               pass
+       self.length = clen
+
+       self.list = self.file = None
+       self.done = 0
+       self.lines = []
+       if ctype == 'application/x-www-form-urlencoded':
+           self.read_urlencoded()
+       elif ctype[:10] == 'multipart/':
+           self.read_multi()
+       else:
+           self.read_single()
+
+    def __repr__(self):
+       """Return a printable representation."""
+       return "FieldStorage(%s, %s, %s)" % (
+               `self.name`, `self.filename`, `self.value`)
+
+    def __getattr__(self, name):
+       if name != 'value':
+           raise AttributeError, name
+       if self.file:
+           self.file.seek(0)
+           value = self.file.read()
+           self.file.seek(0)
+       elif self.list is not None:
+           value = self.list
+       else:
+           value = None
+       return value
+
+    def __getitem__(self, key):
+       """Dictionary style indexing."""
+       if self.list is None:
+           raise TypeError, "not indexable"
+       found = []
+       for item in self.list:
+           if item.name == key: found.append(item)
+       if not found:
+           raise KeyError, key
+       return found
+
+    def keys(self):
+       """Dictionary style keys() method."""
+       if self.list is None:
+           raise TypeError, "not indexable"
+       keys = []
+       for item in self.list:
+           if item.name not in keys: keys.append(item.name)
+       return keys
+
+    def read_urlencoded(self):
+       """Internal: read data in query string format."""
+       qs = self.fp.read(self.length)
+       dict = parse_qs(qs)
+       self.list = []
+       for key, valuelist in dict.items():
+           for value in valuelist:
+               self.list.append(MiniFieldStorage(key, value))
+       self.skip_lines()
+
+    def read_multi(self):
+       """Internal: read a part that is itself multipart."""
+       import rfc822
+       self.list = []
+       part = self.__class__(self.fp, {}, self.innerboundary)
+       # Throw first part away
+       while not part.done:
+           headers = rfc822.Message(self.fp)
+           part = self.__class__(self.fp, headers, self.innerboundary)
+           self.list.append(part)
+       self.skip_lines()
+
+    def read_single(self):
+       """Internal: read an atomic part."""
+       if self.length >= 0:
+           self.read_binary()
+           self.skip_lines()
+       else:
+           self.read_lines()
+       self.file.seek(0)
+
+    bufsize = 8*1024           # I/O buffering size for copy to file
+
+    def read_binary(self):
+       """Internal: read binary data."""
+       self.file = self.make_file('b')
+       todo = self.length
+       if todo >= 0:
+           while todo > 0:
+               data = self.fp.read(min(todo, self.bufsize))
+               if not data:
+                   self.done = -1
+                   break
+               self.file.write(data)
+               todo = todo - len(data)
+
+    def read_lines(self):
+       """Internal: read lines until EOF or outerboundary."""
+       self.file = self.make_file('')
+       if self.outerboundary:
+           self.read_lines_to_outerboundary()
+       else:
+           self.read_lines_to_eof()
+
+    def read_lines_to_eof(self):
+       """Internal: read lines until EOF."""
+       while 1:
+           line = self.fp.readline()
+           if not line:
+               self.done = -1
+               break
+           self.lines.append(line)
+           if line[-2:] == '\r\n':
+               line = line[:-2] + '\n'
+           self.file.write(line)
+
+    def read_lines_to_outerboundary(self):
+       """Internal: read lines until outerboundary."""
+       next = "--" + self.outerboundary
+       last = next + "--"
+       delim = ""
+       while 1:
+           line = self.fp.readline()
+           if not line:
+               self.done = -1
+               break
+           self.lines.append(line)
+           if line[:2] == "--":
+               strippedline = string.strip(line)
+               if strippedline == next:
+                   break
+               if strippedline == last:
+                   self.done = 1
+                   break
+           if line[-2:] == "\r\n":
+               line = line[:-2]
+           elif line[-1] == "\n":
+               line = line[:-1]
+           self.file.write(delim + line)
+           delim = "\n"
+
+    def skip_lines(self):
+       """Internal: skip lines until outer boundary if defined."""
+       if not self.outerboundary or self.done:
+           return
+       next = "--" + self.outerboundary
+       last = next + "--"
+       while 1:
+           line = self.fp.readline()
+           if not line:
+               self.done = -1
+               break
+           self.lines.append(line)
+           if line[:2] == "--":
+               strippedline = string.strip(line)
+               if strippedline == next:
+                   break
+               if strippedline == last:
+                   self.done = 1
+                   break
+
+    def make_file(self, binary):
+       """Overridable: return a readable & writable file.
+
+       The file will be used as follows:
+       - data is written to it
+       - seek(0)
+       - data is read from it
+
+       The 'binary' argument is 'b' if the file should be created in
+       binary mode (on non-Unix systems), '' otherwise.
+
+       The intention is that you can override this method to
+       selectively create a real (temporary) file or use a memory
+       file dependent on the perceived size of the file or the
+       presence of a filename, etc.
+
+       """
+
+       # Prefer ArrayIO over StringIO, if it's available
+       try:
+           from ArrayIO import ArrayIO
+           ioclass = ArrayIO
+       except ImportError:
+           from StringIO import StringIO
+           ioclass = StringIO
+       return ioclass()
 
 
 # Main classes
 # ============
 
 class FormContentDict:
-       """Basic (multiple values per field) form content as dictionary.
-       
-       form = FormContentDict()
-       
-       form[key] -> [value, value, ...]
-       form.has_key(key) -> Boolean
-       form.keys() -> [key, key, ...]
-       form.values() -> [[val, val, ...], [val, val, ...], ...]
-       form.items() ->  [(key, [val, val, ...]), (key, [val, val, ...]), ...]
-       form.dict == {key: [val, val, ...], ...}
-
-       """
-       def __init__( self ):
-               self.dict = parse()
-               self.query_string = environ['QUERY_STRING']
-       def __getitem__(self,key):
-               return self.dict[key]
-       def keys(self):
-               return self.dict.keys()
-       def has_key(self, key):
-               return self.dict.has_key(key)
-       def values(self):
-               return self.dict.values()
-       def items(self):
-               return self.dict.items() 
-       def __len__( self ):
-               return len(self.dict)
+    """Basic (multiple values per field) form content as dictionary.
+
+    form = FormContentDict()
+
+    form[key] -> [value, value, ...]
+    form.has_key(key) -> Boolean
+    form.keys() -> [key, key, ...]
+    form.values() -> [[val, val, ...], [val, val, ...], ...]
+    form.items() ->  [(key, [val, val, ...]), (key, [val, val, ...]), ...]
+    form.dict == {key: [val, val, ...], ...}
+
+    """
+    def __init__( self ):
+       self.dict = parse()
+       self.query_string = environ['QUERY_STRING']
+    def __getitem__(self,key):
+       return self.dict[key]
+    def keys(self):
+       return self.dict.keys()
+    def has_key(self, key):
+       return self.dict.has_key(key)
+    def values(self):
+       return self.dict.values()
+    def items(self):
+       return self.dict.items() 
+    def __len__( self ):
+       return len(self.dict)
 
 
 class SvFormContentDict(FormContentDict):
-       """Strict single-value expecting form content as dictionary.
-       
-       IF you only expect a single value for each field, then
-       form[key] will return that single value.  It will raise an
-       IndexError if that expectation is not true.  IF you expect a
-       field to have possible multiple values, than you can use
-       form.getlist(key) to get all of the values.  values() and
-       items() are a compromise: they return single strings where
-       there is a single value, and lists of strings otherwise.
-       
-       """
-       def __getitem__(self, key):
-               if len(self.dict[key]) > 1: 
-                       raise IndexError, 'expecting a single value' 
-               return self.dict[key][0]
-       def getlist(self, key):
-               return self.dict[key]
-       def values(self):
-               lis = []
-               for each in self.dict.values(): 
-                       if len( each ) == 1 : 
-                               lis.append(each[0])
-                       else: lis.append(each)
-               return lis
-       def items(self):
-               lis = []
-               for key,value in self.dict.items():
-                       if len(value) == 1 :
-                               lis.append((key, value[0]))
-                       else:   lis.append((key, value))
-               return lis
+    """Strict single-value expecting form content as dictionary.
+
+    IF you only expect a single value for each field, then form[key]
+    will return that single value.  It will raise an IndexError if
+    that expectation is not true.  IF you expect a field to have
+    possible multiple values, than you can use form.getlist(key) to
+    get all of the values.  values() and items() are a compromise:
+    they return single strings where there is a single value, and
+    lists of strings otherwise.
+
+    """
+    def __getitem__(self, key):
+       if len(self.dict[key]) > 1: 
+           raise IndexError, 'expecting a single value' 
+       return self.dict[key][0]
+    def getlist(self, key):
+       return self.dict[key]
+    def values(self):
+       lis = []
+       for each in self.dict.values(): 
+           if len( each ) == 1 : 
+               lis.append(each[0])
+           else: lis.append(each)
+       return lis
+    def items(self):
+       lis = []
+       for key,value in self.dict.items():
+           if len(value) == 1 :
+               lis.append((key, value[0]))
+           else:       lis.append((key, value))
+       return lis
 
 
 class InterpFormContentDict(SvFormContentDict):
-       """This class is present for backwards compatibility only.""" 
-       def __getitem__( self, key ):
-               v = SvFormContentDict.__getitem__( self, key )
-               if v[0] in string.digits+'+-.' : 
-                       try:  return  string.atoi( v ) 
-                       except ValueError:
-                               try:    return string.atof( v )
-                               except ValueError: pass
-               return string.strip(v)
-       def values( self ):
-               lis = [] 
-               for key in self.keys():
-                       try:
-                               lis.append( self[key] )
-                       except IndexError:
-                               lis.append( self.dict[key] )
-               return lis
-       def items( self ):
-               lis = [] 
-               for key in self.keys():
-                       try:
-                               lis.append( (key, self[key]) )
-                       except IndexError:
-                               lis.append( (key, self.dict[key]) )
-               return lis
+    """This class is present for backwards compatibility only.""" 
+    def __getitem__( self, key ):
+       v = SvFormContentDict.__getitem__( self, key )
+       if v[0] in string.digits+'+-.' : 
+           try:  return  string.atoi( v ) 
+           except ValueError:
+               try:    return string.atof( v )
+               except ValueError: pass
+       return string.strip(v)
+    def values( self ):
+       lis = [] 
+       for key in self.keys():
+           try:
+               lis.append( self[key] )
+           except IndexError:
+               lis.append( self.dict[key] )
+       return lis
+    def items( self ):
+       lis = [] 
+       for key in self.keys():
+           try:
+               lis.append( (key, self[key]) )
+           except IndexError:
+               lis.append( (key, self.dict[key]) )
+       return lis
 
 
 class FormContent(FormContentDict):
-       """This class is present for backwards compatibility only.""" 
-       def values(self,key):
-               if self.dict.has_key(key):return self.dict[key]
-               else: return None
-       def indexed_value(self,key, location):
-               if self.dict.has_key(key):
-                       if len (self.dict[key]) > location:
-                               return self.dict[key][location]
-                       else: return None
-               else: return None
-       def value(self,key):
-               if self.dict.has_key(key):return self.dict[key][0]
-               else: return None
-       def length(self,key):
-               return len (self.dict[key])
-       def stripped(self,key):
-               if self.dict.has_key(key):return string.strip(self.dict[key][0])
-               else: return None
-       def pars(self):
-               return self.dict
+    """This class is present for backwards compatibility only.""" 
+    def values(self,key):
+       if self.dict.has_key(key):return self.dict[key]
+       else: return None
+    def indexed_value(self,key, location):
+       if self.dict.has_key(key):
+           if len (self.dict[key]) > location:
+               return self.dict[key][location]
+           else: return None
+       else: return None
+    def value(self,key):
+       if self.dict.has_key(key):return self.dict[key][0]
+       else: return None
+    def length(self,key):
+       return len (self.dict[key])
+    def stripped(self,key):
+       if self.dict.has_key(key):return string.strip(self.dict[key][0])
+       else: return None
+    def pars(self):
+       return self.dict
 
 
 # Test/debug code
 # ===============
 
 def test():
-       """Robust test CGI script.
-       
-       Dump all information provided to the script in HTML form.
-
-       """
-       import traceback
-       print "Content-type: text/html"
-       print
-       sys.stderr = sys.stdout
-       try:
-               print_environ()
-               print_form(FieldStorage())
-               print
-               print "<H3>Current Working Directory:</H3>"
-               try:
-                       pwd = os.getcwd()
-               except os.error, msg:
-                       print "os.error:", escape(str(msg))
-               else:
-                       print escape(pwd)
-               print
-       except:
-               print "\n\n<PRE>"       # Turn of word wrap
-               traceback.print_exc()
+    """Robust test CGI script, usable as main program.
+
+    Write minimal HTTP headers and dump all information provided to
+    the script in HTML form.
+
+    """
+    import traceback
+    print "Content-type: text/html"
+    print
+    sys.stderr = sys.stdout
+    try:
+       print_form(FieldStorage())
+       print_environ()
+       print_directory()
+       print_environ_usage()
+    except:
+       print "\n\n<PRE>"       # Turn off HTML word wrap
+       traceback.print_exc()
 
 def print_environ():
-       """Dump the shell environment in HTML form."""
-       keys = environ.keys()
-       keys.sort()
-       print
-       print "<H3>Shell environment:</H3>"
-       print "<DL>"
-       for key in keys:
-               print "<DT>", escape(key), "<DD>", escape(environ[key])
-       print "</DL>" 
-       print
+    """Dump the shell environment as HTML."""
+    keys = environ.keys()
+    keys.sort()
+    print
+    print "<H3>Shell environment:</H3>"
+    print "<DL>"
+    for key in keys:
+       print "<DT>", escape(key), "<DD>", escape(environ[key])
+    print "</DL>" 
+    print
 
 def print_form(form):
-       """Dump the contents of a form in HTML form."""
-       keys = form.keys()
-       keys.sort()
-       print
-       print "<H3>Form contents:</H3>"
-       print "<DL>"
-       for key in keys:
-               print "<DT>" + escape(key) + ":",
-               value = form[key]
-               print "<i>" + escape(`type(value)`) + "</i>"
-               print "<DD>" + escape(`value`)
-       print "</DL>"
-       print
+    """Dump the contents of a form as HTML."""
+    keys = form.keys()
+    keys.sort()
+    print
+    print "<H3>Form contents:</H3>"
+    print "<DL>"
+    for key in keys:
+       print "<DT>" + escape(key) + ":",
+       value = form[key]
+       print "<i>" + escape(`type(value)`) + "</i>"
+       print "<DD>" + escape(`value`)
+    print "</DL>"
+    print
+
+def print_directory():
+    """Dump the current directory as HTML."""
+    print
+    print "<H3>Current Working Directory:</H3>"
+    try:
+       pwd = os.getcwd()
+    except os.error, msg:
+       print "os.error:", escape(str(msg))
+    else:
+       print escape(pwd)
+    print
 
 def print_environ_usage():
-       """Print a list of environment variables used by the CGI protocol."""
-       print """
+    """Dump a list of environment variables used by CGI as HTML."""
+    print """
 <H3>These environment variables could have been set:</H3>
 <UL>
 <LI>AUTH_TYPE
@@ -974,6 +1043,16 @@ def print_environ_usage():
 <LI>SERVER_ROOT
 <LI>SERVER_SOFTWARE
 </UL>
+In addition, HTTP headers sent by the server may be passed in the
+environment as well.  Here are some common variable names:
+<UL>
+<LI>HTTP_ACCEPT
+<LI>HTTP_CONNECTION
+<LI>HTTP_HOST
+<LI>HTTP_PRAGMA
+<LI>HTTP_REFERER
+<LI>HTTP_USER_AGENT
+</UL>
 """
 
 
@@ -981,11 +1060,11 @@ def print_environ_usage():
 # =========
 
 def escape(s):
-       """Replace special characters '&', '<' and '>' by SGML entities."""
-       s = regsub.gsub("&", "&amp;", s)        # Must be done first!
-       s = regsub.gsub("<", "&lt;", s)
-       s = regsub.gsub(">", "&gt;", s)
-       return s
+    """Replace special characters '&', '<' and '>' by SGML entities."""
+    s = regsub.gsub("&", "&amp;", s)   # Must be done first!
+    s = regsub.gsub("<", "&lt;", s)
+    s = regsub.gsub(">", "&gt;", s)
+    return s
 
 
 # Invoke mainline
@@ -993,4 +1072,4 @@ def escape(s):
 
 # Call test() when this file is run as a script (not imported as a module)
 if __name__ == '__main__': 
-       test()
+    test()