summaryrefslogtreecommitdiff
path: root/paste/request.py
blob: a823d1a1f42ed90b71552da93d6abda83a53d8c5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
# (c) 2005 Ian Bicking and contributors
# This module is part of the Python Paste Project and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
"""
This module provides helper routines with work directly on a WSGI
environment to solve common requirements.

   * get_cookies(environ)
   * parse_querystring(environ)
   * parse_formvars(environ, all_as_list=False, include_get_vars=True)
   * construct_url(environ, with_query_string=True, with_path_info=True,
                   script_name=None, path_info=None, querystring=None)
   * path_info_split(path_info)
   * path_info_pop(environ)
   * resolve_relative_url(url, environ)

"""
import cgi
import textwrap
from Cookie import SimpleCookie
import urlparse
from util.UserDict24 import UserDict

__all__ = ['get_cookies', 'parse_querystring', 'parse_formvars',
           'construct_url', 'path_info_split', 'path_info_pop',
           'resolve_relative_url']

class environ_getter(object):
    """For delegating an attribute to a key in self.environ."""
    # @@: Also __set__?  Should setting be allowed?
    def __init__(self, key, default=''):
        self.key = key
        self.default = default
    def __get__(self, obj, type=None):
        if type == None:
            return self
        return obj.environ.get(self.key, self.default)

    def __repr__(self):
        return '<Proxy for WSGI environ %r key>' % self.key

class MultiDict(UserDict):
    """Acts as a normal dict, but assumes all values are lists, and
    retrieving an item retrieves the first value in the list. getlist
    retrieves the full list"""
    def __getitem__(self, key):
        return self.data[key][0]

    def getlist(self, key):
        return self.data[key]

    def update(self, dict):
        if isinstance(dct, UserDict):
            self.data.update(dict.data)
        elif isinstance(dict, type(self.data)):
            self.data.update(dict)
        else:
            for k, v in dict.items():
                if self.has_key(k) and isinstance(v, list):
                    self[k].extend(v)
                elif self.has_key(k):
                    self[k].append(v)
                elif not isinstance(v, list):
                    self[k] = [v]
                else:
                    self[k] = v


def get_cookies(environ):
    """
    Gets a cookie object (which is a dictionary-like object) from the
    request environment; caches this value in case get_cookies is
    called again for the same request.

    """
    header = environ.get('HTTP_COOKIE', '')
    if environ.has_key('paste.cookies'):
        cookies, check_header = environ['paste.cookies']
        if check_header == header:
            return cookies
    cookies = SimpleCookie()
    cookies.load(header)
    environ['paste.cookies'] = (cookies, header)
    return cookies

def parse_querystring(environ):
    """
    Parses a query string into a list like ``[(name, value)]``.
    Caches this value in case parse_querystring is called again
    for the same request.

    You can pass the result to ``dict()``, but be aware that keys that
    appear multiple times will be lost (only the last value will be
    preserved).

    """
    source = environ.get('QUERY_STRING', '')
    if not source:
        return []
    if 'paste.parsed_querystring' in environ:
        parsed, check_source = environ['paste.parsed_querystring']
        if check_source == source:
            return parsed
    parsed = cgi.parse_qsl(source, keep_blank_values=True,
                           strict_parsing=False)
    environ['paste.parsed_querystring'] = (parsed, source)
    return parsed

def parse_dict_querystring(environ):
    """Parses a query string like parse_querystring, but returns a multidict

    Caches this value in case parse_dict_querystring is called again
    for the same request.

    Example::

        #environ['QUERY_STRING'] -  day=Monday&user=fred&user=jane
        >>> parsed = parse_dict_querystring(environ)

        >>> parsed['day']
        'Monday'
        >>> parsed['user']
        'fred'
        >>> parsed.getlist['user']
        ['fred', 'jane']

    """
    source = environ.get('QUERY_STRING', '')
    if not source:
        return {}
    if 'paste.parsed_dict_querystring' in environ:
        parsed, check_source = environ['paste.parsed_dict_querystring']
        if check_source == source:
            return parsed
    parsed = cgi.parse_qs(source, keep_blank_values=True,
                           strict_parsing=False)
    multi = MultiDict()
    multi.update(parsed)
    environ['paste.parsed_dict_querystring'] = multi
    return multi

def parse_formvars(environ, all_as_list=False, include_get_vars=True):
    """Parses the request, returning a dictionary of the keys.

    If ``all_as_list`` is true, then all values will be lists.  If
    not, then only values that show up multiple times will be lists.

    If ``include_get_vars`` is true and this was a POST request, then
    GET (query string) variables will also be folded into the
    dictionary.

    All values should be strings, except for file uploads which are
    left as FieldStorage instances.

    """
    source = (environ.get('QUERY_STRING', ''),
              environ['wsgi.input'], environ['REQUEST_METHOD'],
              all_as_list, include_get_vars)
    if 'paste.parsed_formvars' in environ:
        parsed, check_source = environ['paste.parsed_formvars']
        if check_source == source:
            return parsed
    fs = cgi.FieldStorage(fp=environ['wsgi.input'],
                          environ=environ,
                          keep_blank_values=1)
    formvars = {}
    for name in fs.keys():
        values = fs[name]
        if not isinstance(values, list):
            values = [values]
        for value in values:
            if not value.filename:
                value = value.value
            if name in formvars:
                if isinstance(formvars[name], list):
                    formvars[name].append(value)
                else:
                    formvars[name] = [formvars[name], value]
            elif all_as_list:
                formvars[name] = [value]
            else:
                formvars[name] = value
    if environ['REQUEST_METHOD'] == 'POST' and include_get_vars:
        for name, value in parse_querystring(environ):
            if name in formvars:
                if isinstance(formvars[name], list):
                    formvars[name].append(value)
                else:
                    formvars[name] = [formvars[name], value]
            elif all_as_list:
                formvars[name] = [value]
            else:
                formvars[name] = value
    environ['paste.parsed_formvars'] = (formvars, source)
    return formvars

def construct_url(environ, with_query_string=True, with_path_info=True,
                  script_name=None, path_info=None, querystring=None):
    """Reconstructs the URL from the WSGI environment.

    You may override SCRIPT_NAME, PATH_INFO, and QUERYSTRING with
    the keyword arguments.

    """
    url = environ['wsgi.url_scheme']+'://'

    if environ.get('HTTP_HOST'):
        url += environ['HTTP_HOST'].split(':')[0]
    else:
        url += environ['SERVER_NAME']

    if environ['wsgi.url_scheme'] == 'https':
        if environ['SERVER_PORT'] != '443':
            url += ':' + environ['SERVER_PORT']
    else:
        if environ['SERVER_PORT'] != '80':
            url += ':' + environ['SERVER_PORT']

    if script_name is None:
        url += environ.get('SCRIPT_NAME','')
    else:
        url += script_name
    if with_path_info:
        if path_info is None:
            url += environ.get('PATH_INFO','')
        else:
            url += path_info
    if with_query_string:
        if querystring is None:
            if environ.get('QUERY_STRING'):
                url += '?' + environ['QUERY_STRING']
        elif querystring:
            url += '?' + querystring
    return url

def resolve_relative_url(url, environ):
    """
    Resolve the given relative URL as being relative to the
    location represented by the environment.  This can be used
    for redirecting to a relative path.  Note: if url is already
    absolute, this function will (intentionally) have no effect
    on it.

    """
    cur_url = construct_url(environ, with_query_string=False)
    return urlparse.urljoin(cur_url, url)

def path_info_split(path_info):
    """
    Splits off the first segment of the path.  Returns (first_part,
    rest_of_path).  first_part can be None (if PATH_INFO is empty), ''
    (if PATH_INFO is '/'), or a name without any /'s.  rest_of_path
    can be '' or a string starting with /.

    """
    if not path_info:
        return None, ''
    assert path_info.startswith('/'), (
        "PATH_INFO should start with /: %r" % path_info)
    path_info = path_info.lstrip('/')
    if '/' in path_info:
        first, rest = path_info.split('/', 1)
        return first, '/' + rest
    else:
        return path_info, ''

def path_info_pop(environ):
    """
    'Pops' off the next segment of PATH_INFO, pushing it onto
    SCRIPT_NAME, and returning that segment.

    For instance::

        >>> def call_it(script_name, path_info):
        ...     env = {'SCRIPT_NAME': script_name, 'PATH_INFO': path_info}
        ...     result = path_info_pop(env)
        ...     print 'SCRIPT_NAME=%r; PATH_INFO=%r; returns=%r' % (
        ...         env['SCRIPT_NAME'], env['PATH_INFO'], result)
        >>> call_it('/foo', '/bar')
        SCRIPT_NAME='/foo/bar'; PATH_INFO=''; returns='bar'
        >>> call_it('/foo/bar', '')
        SCRIPT_NAME='/foo/bar'; PATH_INFO=''; returns=None
        >>> call_it('/foo/bar', '/')
        SCRIPT_NAME='/foo/bar/'; PATH_INFO=''; returns=''
        >>> call_it('', '/1/2/3')
        SCRIPT_NAME='/1'; PATH_INFO='/2/3'; returns='1'
        >>> call_it('', '//1/2')
        SCRIPT_NAME='//1'; PATH_INFO='/2'; returns='1'

    """
    path = environ.get('PATH_INFO', '')
    if not path:
        return None
    while path.startswith('/'):
        environ['SCRIPT_NAME'] += '/'
        path = path[1:]
    if '/' not in path:
        environ['SCRIPT_NAME'] += path
        environ['PATH_INFO'] = ''
        return path
    else:
        segment, path = path.split('/', 1)
        environ['PATH_INFO'] = '/' + path
        environ['SCRIPT_NAME'] += segment
        return segment

_parse_headers_special = {
    # This is a Zope convention, but we'll allow it here:
    'HTTP_CGI_AUTHORIZATION': 'Authorization',
    'CONTENT_LENGTH': 'Content-Length',
    'CONTENT_TYPE': 'Content-Type',
    }

def parse_headers(environ):
    """
    Parse the headers in the environment (like ``HTTP_HOST``) and
    yield a sequence of those (header_name, value) tuples.
    """
    # @@: Maybe should parse out comma-separated headers?
    for cgi_var, value in environ.iteritems():
        if cgi_var in _parse_headers_special:
            yield _parse_headers_special[cgi_var], value
        elif cgi_var.startswith('HTTP_'):
            yield cgi_var[5:].title().replace('_', '-'), value

class LazyCache(object):
    """Lazy and Caching Function Executer

    LazyCache takes a function, and will hold onto it to be called at a
    later time. When the function is called, its result will be cached and
    used when called in the future.

    This style is ideal for functions that may require processing that is
    only done in rare cases, but when it is done, caching the results is
    desired.

    """
    def __init__(self, func):
        self.fn = func
        self.result = None

    def __call__(self, *args):
        if not self.result:
            self.result = self.fn(*args)
        return self.result

class WSGIRequest(object):
    """WSGI Request API Object

    This object represents a WSGI request with a more friendly interface.
    This does not expose every detail of the WSGI environment, and does not
    in any way express anything beyond what is available in the environment
    dictionary.  *All* state is kept in the environment dictionary; this
    is essential for interoperability.

    You are free to subclass this object.

    """

    def __init__(self, environ, urlvars={}):
        self.environ = environ
        self.urlvars = urlvars
    
    body = environ_getter('wsgi.input')
    scheme = environ_getter('wsgi.url_scheme') # ?
    method = environ_getter('REQUEST_METHOD')
    # wsgi.config would be better, of course:
    config = environ_getter('paste.config')
    script_name = environ_getter('SCRIPT_NAME')
    path_info = environ_getter('PATH_INFO')
    # Other possible variables:
    # REMOTE_USER, CONTENT_TYPE, CONTENT_LENGTH?  Probably not
    # REMOTE_ADDR, REMOTE_HOST, AUTH_TYPE?  Even less interesting
    # SERVER_PORT?  Maybe
    # SERVER_PROTOCOL, SERVER_SOFTWARE, GATEWAY_INTERFACE?  Definitely not

    def host(self):
        """Host name provided in HTTP_HOST, with fall-back to SERVER_NAME"""
        return self.environ.get('HTTP_HOST', self.environ.get('SERVER_NAME'))
    host = property(host, doc=host.__doc__)

    def get(self):
        """
        Dictionary-like object representing the QUERY_STRING
        parameters. Always present, if possibly empty.

        If the same key is present in the query string multiple
        times, it will be present as a list.
        """
        return parse_dict_querystring(self.environ)
    get = property(get, doc=get.__doc__)

    def post(self):
        """Dictionary-like object representing the POST body.

        Most values are strings, but file uploads can be FieldStorage
        objects. If this is not a POST request, or the body is not
        encoded fields (e.g., an XMLRPC request) then this will be empty.

        This will consume wsgi.input when first accessed if applicable,
        but the output will be put in environ['paste.post_vars']
        
        """
        formvars = MultiDict()
        formvars.update(parse_formvars(self.environ, all_as_list=True, include_get_vars=False))
        return formvars
    post = property(LazyCache(post), doc=post.__doc__)

    def params(self):
        """MultiDict of keys from POST, GET, URL dicts

        Return a key value from the parameters, they are checked in the
        following order:
            POST, GET, URL

        Additional methods supported:

        getlist(key)
            Returns a list of all the values by that key, collected from
            POST, GET, URL dicts
        """
        pms = MultiDict()
        pms.update(self.post)
        pms.update(self.get)
        return pms
    params = property(params, doc=params.__doc__)

    def urlvars(self):
        """
        Return a plain dictionary representing any variables
        captured from the URL parsing (the parsed URL portion is in
        SCRIPT_NAME); frequently {}, but never None
        """
        return self.urlvars
    urlvars = property(urlvars, doc=urlvars.__doc__)

    def cookies(self):
        """Dictionary of cookies keyed by cookie name.

        Just a plain dictionary, may be empty but not None.
        
        """
        fresh_cookies = get_cookies(self.environ)
        cookie_dict = {}
        for k, morsel in fresh_cookies.iteritems():
            cookie_dict[k] = morsel.value
        return cookie_dict
    cookies = property(LazyCache(cookies), doc=cookies.__doc__)

    def headers(self):
        """Access to incoming headers"""
        headertable = {}
        for key in self.environ.keys():
            if key.startswith('HTTP'):
                headertable[key[5:]] = self.environ[key]
        return headertable
    headers = property(LazyCache(headers), doc=headers.__doc__)


if __name__ == '__main__':
    import doctest
    doctest.testmod()