Latest Version: 0.9.6.2
/Users/bbangert/Programming/Python/Paste/paste/request.py
0001# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
0002# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
0003# (c) 2005 Ian Bicking and contributors
0004# This module is part of the Python Paste Project and is released under
0005# the MIT License: http://www.opensource.org/licenses/mit-license.php
0006"""
0007This module provides helper routines with work directly on a WSGI
0008environment to solve common requirements.
0009
0010   * get_cookies(environ)
0011   * parse_querystring(environ)
0012   * parse_formvars(environ, include_get_vars=True)
0013   * construct_url(environ, with_query_string=True, with_path_info=True,
0014                   script_name=None, path_info=None, querystring=None)
0015   * path_info_split(path_info)
0016   * path_info_pop(environ)
0017   * resolve_relative_url(url, environ)
0018
0019"""
0020import cgi
0021from Cookie import SimpleCookie
0022from StringIO import StringIO
0023import urlparse
0024import urllib
0025try:
0026    from UserDict import DictMixin
0027except ImportError:
0028    from paste.util.UserDict24 import DictMixin
0029from paste.util.multidict import MultiDict
0030
0031__all__ = ['get_cookies', 'get_cookie_dict', 'parse_querystring',
0032           'parse_formvars', 'construct_url', 'path_info_split',
0033           'path_info_pop', 'resolve_relative_url', 'EnvironHeaders']
0034
0035def get_cookies(environ):
0036    """
0037    Gets a cookie object (which is a dictionary-like object) from the
0038    request environment; caches this value in case get_cookies is
0039    called again for the same request.
0040
0041    """
0042    header = environ.get('HTTP_COOKIE', '')
0043    if environ.has_key('paste.cookies'):
0044        cookies, check_header = environ['paste.cookies']
0045        if check_header == header:
0046            return cookies
0047    cookies = SimpleCookie()
0048    cookies.load(header)
0049    environ['paste.cookies'] = (cookies, header)
0050    return cookies
0051
0052def get_cookie_dict(environ):
0053    """Return a *plain* dictionary of cookies as found in the request.
0054
0055    Unlike ``get_cookies`` this returns a dictionary, not a
0056    ``SimpleCookie`` object.  For incoming cookies a dictionary fully
0057    represents the information.  Like ``get_cookies`` this caches and
0058    checks the cache.
0059    """
0060    header = environ.get('HTTP_COOKIE')
0061    if not header:
0062        return {}
0063    if environ.has_key('paste.cookies.dict'):
0064        cookies, check_header = environ['paste.cookies.dict']
0065        if check_header == header:
0066            return cookies
0067    cookies = SimpleCookie()
0068    cookies.load(header)
0069    result = {}
0070    for name in cookies:
0071        result[name] = cookies[name].value
0072    environ['paste.cookies.dict'] = (result, header)
0073    return result
0074
0075def parse_querystring(environ):
0076    """
0077    Parses a query string into a list like ``[(name, value)]``.
0078    Caches this value in case parse_querystring is called again
0079    for the same request.
0080
0081    You can pass the result to ``dict()``, but be aware that keys that
0082    appear multiple times will be lost (only the last value will be
0083    preserved).
0084
0085    """
0086    source = environ.get('QUERY_STRING', '')
0087    if not source:
0088        return []
0089    if 'paste.parsed_querystring' in environ:
0090        parsed, check_source = environ['paste.parsed_querystring']
0091        if check_source == source:
0092            return parsed
0093    parsed = cgi.parse_qsl(source, keep_blank_values=True,
0094                           strict_parsing=False)
0095    environ['paste.parsed_querystring'] = (parsed, source)
0096    return parsed
0097
0098def parse_dict_querystring(environ):
0099    """Parses a query string like parse_querystring, but returns a MultiDict
0100
0101    Caches this value in case parse_dict_querystring is called again
0102    for the same request.
0103
0104    Example::
0105
0106        >>> environ = {'QUERY_STRING': 'day=Monday&user=fred&user=jane'}
0107        >>> parsed = parse_dict_querystring(environ)
0108
0109        >>> parsed['day']
0110        'Monday'
0111        >>> parsed['user']
0112        'fred'
0113        >>> parsed.getall('user')
0114        ['fred', 'jane']
0115
0116    """
0117    source = environ.get('QUERY_STRING', '')
0118    if not source:
0119        return MultiDict()
0120    if 'paste.parsed_dict_querystring' in environ:
0121        parsed, check_source = environ['paste.parsed_dict_querystring']
0122        if check_source == source:
0123            return parsed
0124    parsed = cgi.parse_qsl(source, keep_blank_values=True,
0125                           strict_parsing=False)
0126    multi = MultiDict(parsed)
0127    environ['paste.parsed_dict_querystring'] = (multi, source)
0128    return multi
0129
0130def parse_formvars(environ, include_get_vars=True):
0131    """Parses the request, returning a MultiDict of form variables.
0132
0133    If ``include_get_vars`` is true then GET (query string) variables
0134    will also be folded into the MultiDict.
0135
0136    All values should be strings, except for file uploads which are
0137    left as ``FieldStorage`` instances.
0138
0139    If the request was not a normal form request (e.g., a POST with an
0140    XML body) then ``environ['wsgi.input']`` won't be read.
0141    """
0142    source = environ['wsgi.input']
0143    if 'paste.parsed_formvars' in environ:
0144        parsed, check_source = environ['paste.parsed_formvars']
0145        if check_source == source:
0146            if include_get_vars:
0147                parsed.update(parse_querystring(environ))
0148            return parsed
0149    # @@: Shouldn't bother FieldStorage parsing during GET/HEAD and
0150    # fake_out_cgi requests
0151    type = environ.get('CONTENT_TYPE', '').lower()
0152    if ';' in type:
0153        type = type.split(';', 1)[0]
0154    fake_out_cgi = type not in ('', 'application/x-www-form-urlencoded',
0155                                'multipart/form-data')
0156    # FieldStorage assumes a default CONTENT_LENGTH of -1, but a
0157    # default of 0 is better:
0158    if not environ.get('CONTENT_LENGTH'):
0159        environ['CONTENT_LENGTH'] = '0'
0160    # Prevent FieldStorage from parsing QUERY_STRING during GET/HEAD
0161    # requests
0162    old_query_string = environ.get('QUERY_STRING','')
0163    environ['QUERY_STRING'] = ''
0164    if fake_out_cgi:
0165        input = StringIO('')
0166        old_content_type = environ.get('CONTENT_TYPE')
0167        old_content_length = environ.get('CONTENT_LENGTH')
0168        environ['CONTENT_LENGTH'] = '0'
0169        environ['CONTENT_TYPE'] = ''
0170    else:
0171        input = environ['wsgi.input']
0172    fs = cgi.FieldStorage(fp=input,
0173                          environ=environ,
0174                          keep_blank_values=1)
0175    environ['QUERY_STRING'] = old_query_string
0176    if fake_out_cgi:
0177        environ['CONTENT_TYPE'] = old_content_type
0178        environ['CONTENT_LENGTH'] = old_content_length
0179    formvars = MultiDict()
0180    if isinstance(fs.value, list):
0181        for name in fs.keys():
0182            values = fs[name]
0183            if not isinstance(values, list):
0184                values = [values]
0185            for value in values:
0186                if not value.filename:
0187                    value = value.value
0188                formvars.add(name, value)
0189    environ['paste.parsed_formvars'] = (formvars, source)
0190    if include_get_vars:
0191        formvars.update(parse_querystring(environ))
0192    return formvars
0193
0194def construct_url(environ, with_query_string=True, with_path_info=True,
0195                  script_name=None, path_info=None, querystring=None):
0196    """Reconstructs the URL from the WSGI environment.
0197
0198    You may override SCRIPT_NAME, PATH_INFO, and QUERYSTRING with
0199    the keyword arguments.
0200
0201    """
0202    url = environ['wsgi.url_scheme']+'://'
0203
0204    if environ.get('HTTP_HOST'):
0205        host = environ['HTTP_HOST']
0206        port = None
0207        if ':' in host:
0208            host, port = host.split(':', 1)
0209            if environ['wsgi.url_scheme'] == 'https':
0210                if port == '443':
0211                    port = None
0212            elif environ['wsgi.url_scheme'] == 'http':
0213                if port == '80':
0214                    port = None
0215        url += host
0216        if port:
0217            url += ':%s' % port
0218    else:
0219        url += environ['SERVER_NAME']
0220        if environ['wsgi.url_scheme'] == 'https':
0221            if environ['SERVER_PORT'] != '443':
0222                url += ':' + environ['SERVER_PORT']
0223        else:
0224            if environ['SERVER_PORT'] != '80':
0225                url += ':' + environ['SERVER_PORT']
0226
0227    if script_name is None:
0228        url += urllib.quote(environ.get('SCRIPT_NAME',''))
0229    else:
0230        url += urllib.quote(script_name)
0231    if with_path_info:
0232        if path_info is None:
0233            url += urllib.quote(environ.get('PATH_INFO',''))
0234        else:
0235            url += urllib.quote(path_info)
0236    if with_query_string:
0237        if querystring is None:
0238            if environ.get('QUERY_STRING'):
0239                url += '?' + environ['QUERY_STRING']
0240        elif querystring:
0241            url += '?' + querystring
0242    return url
0243
0244def resolve_relative_url(url, environ):
0245    """
0246    Resolve the given relative URL as being relative to the
0247    location represented by the environment.  This can be used
0248    for redirecting to a relative path.  Note: if url is already
0249    absolute, this function will (intentionally) have no effect
0250    on it.
0251
0252    """
0253    cur_url = construct_url(environ, with_query_string=False)
0254    return urlparse.urljoin(cur_url, url)
0255
0256def path_info_split(path_info):
0257    """
0258    Splits off the first segment of the path.  Returns (first_part,
0259    rest_of_path).  first_part can be None (if PATH_INFO is empty), ''
0260    (if PATH_INFO is '/'), or a name without any /'s.  rest_of_path
0261    can be '' or a string starting with /.
0262
0263    """
0264    if not path_info:
0265        return None, ''
0266    assert path_info.startswith('/'), (
0267        "PATH_INFO should start with /: %r" % path_info)
0268    path_info = path_info.lstrip('/')
0269    if '/' in path_info:
0270        first, rest = path_info.split('/', 1)
0271        return first, '/' + rest
0272    else:
0273        return path_info, ''
0274
0275def path_info_pop(environ):
0276    """
0277    'Pops' off the next segment of PATH_INFO, pushing it onto
0278    SCRIPT_NAME, and returning that segment.
0279
0280    For instance::
0281
0282        >>> def call_it(script_name, path_info):
0283        ...     env = {'SCRIPT_NAME': script_name, 'PATH_INFO': path_info}
0284        ...     result = path_info_pop(env)
0285        ...     print 'SCRIPT_NAME=%r; PATH_INFO=%r; returns=%r' % (
0286        ...         env['SCRIPT_NAME'], env['PATH_INFO'], result)
0287        >>> call_it('/foo', '/bar')
0288        SCRIPT_NAME='/foo/bar'; PATH_INFO=''; returns='bar'
0289        >>> call_it('/foo/bar', '')
0290        SCRIPT_NAME='/foo/bar'; PATH_INFO=''; returns=None
0291        >>> call_it('/foo/bar', '/')
0292        SCRIPT_NAME='/foo/bar/'; PATH_INFO=''; returns=''
0293        >>> call_it('', '/1/2/3')
0294        SCRIPT_NAME='/1'; PATH_INFO='/2/3'; returns='1'
0295        >>> call_it('', '//1/2')
0296        SCRIPT_NAME='//1'; PATH_INFO='/2'; returns='1'
0297
0298    """
0299    path = environ.get('PATH_INFO', '')
0300    if not path:
0301        return None
0302    while path.startswith('/'):
0303        environ['SCRIPT_NAME'] += '/'
0304        path = path[1:]
0305    if '/' not in path:
0306        environ['SCRIPT_NAME'] += path
0307        environ['PATH_INFO'] = ''
0308        return path
0309    else:
0310        segment, path = path.split('/', 1)
0311        environ['PATH_INFO'] = '/' + path
0312        environ['SCRIPT_NAME'] += segment
0313        return segment
0314
0315_parse_headers_special = {
0316    # This is a Zope convention, but we'll allow it here:
0317    'HTTP_CGI_AUTHORIZATION': 'Authorization',
0318    'CONTENT_LENGTH': 'Content-Length',
0319    'CONTENT_TYPE': 'Content-Type',
0320    }
0321
0322def parse_headers(environ):
0323    """
0324    Parse the headers in the environment (like ``HTTP_HOST``) and
0325    yield a sequence of those (header_name, value) tuples.
0326    """
0327    # @@: Maybe should parse out comma-separated headers?
0328    for cgi_var, value in environ.iteritems():
0329        if cgi_var in _parse_headers_special:
0330            yield _parse_headers_special[cgi_var], value
0331        elif cgi_var.startswith('HTTP_'):
0332            yield cgi_var[5:].title().replace('_', '-'), value
0333
0334class EnvironHeaders(DictMixin):
0335    """An object that represents the headers as present in a
0336    WSGI environment.
0337
0338    This object is a wrapper (with no internal state) for a WSGI
0339    request object, representing the CGI-style HTTP_* keys as a
0340    dictionary.  Because a CGI environment can only hold one value for
0341    each key, this dictionary is single-valued (unlike outgoing
0342    headers).
0343    """
0344
0345    def __init__(self, environ):
0346        self.environ = environ
0347
0348    def _trans_name(self, name):
0349        key = 'HTTP_'+name.replace('-', '_').upper()
0350        if key == 'HTTP_CONTENT_LENGTH':
0351            key = 'CONTENT_LENGTH'
0352        elif key == 'HTTP_CONTENT_TYPE':
0353            key = 'CONTENT_TYPE'
0354        return key
0355
0356    def _trans_key(self, key):
0357        if key == 'CONTENT_TYPE':
0358            return 'Content-Type'
0359        elif key == 'CONTENT_LENGTH':
0360            return 'Content-Length'
0361        elif key.startswith('HTTP_'):
0362            return key[5:].replace('_', '-').title()
0363        else:
0364            return None
0365
0366    def __getitem__(self, item):
0367        return self.environ[self._trans_name(item)]
0368
0369    def __setitem__(self, item, value):
0370        # @@: Should this dictionary be writable at all?
0371        self.environ[self._trans_name(item)] = value
0372
0373    def __delitem__(self, item):
0374        del self.environ[self._trans_name(item)]
0375
0376    def __iter__(self):
0377        for key in self.environ:
0378            name = self._trans_key(key)
0379            if name is not None:
0380                yield name
0381
0382    def keys(self):
0383        return list(iter(self))
0384
0385    def __contains__(self, item):
0386        return self._trans_name(item) in self.environ
0387
0388def _cgi_FieldStorage__repr__patch(self):
0389    """ monkey patch for FieldStorage.__repr__
0390
0391    Unbelievely, the default __repr__ on FieldStorage reads
0392    the entire file content instead of being sane about it.
0393    This is a simple replacement that doesn't do that
0394    """
0395    if self.file:
0396        return "FieldStorage(%r, %r)" % (
0397                self.name, self.filename)
0398    return "FieldStorage(%r, %r, %r)" % (
0399             self.name, self.filename, self.value)
0400
0401cgi.FieldStorage.__repr__ = _cgi_FieldStorage__repr__patch
0402
0403if __name__ == '__main__':
0404    import doctest
0405    doctest.testmod()

Top