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"""WSGI Wrappers for a Request and Response
0004
0005The WSGIRequest and WSGIResponse objects are light wrappers to make it easier
0006to deal with an incoming request and sending a response.
0007"""
0008import re
0009import warnings
0010from pprint import pformat
0011from Cookie import SimpleCookie
0012from paste.request import EnvironHeaders, get_cookie_dict, parse_dict_querystring, parse_formvars
0014from paste.util.multidict import MultiDict, UnicodeMultiDict
0015from paste.registry import StackedObjectProxy
0016from paste.response import HeaderDict
0017from paste.wsgilib import encode_unicode_app_iter
0018from paste.httpheaders import ACCEPT_LANGUAGE
0019from paste.util.mimeparse import desired_matches
0020
0021__all__ = ['WSGIRequest', 'WSGIResponse']
0022
0023_CHARSET_RE = re.compile(r'.*;\s*charset=(.*?)(;|$)', re.I)
0024
0025class DeprecatedSettings(StackedObjectProxy):
0026 def _push_object(self, obj):
0027 warnings.warn('paste.wsgiwrappers.settings is deprecated: Please use '
0028 'paste.wsgiwrappers.WSGIRequest.defaults instead',
0029 DeprecationWarning, 3)
0030 WSGIResponse.defaults._push_object(obj)
0031 StackedObjectProxy._push_object(self, obj)
0032
0033# settings is deprecated: use WSGIResponse.defaults instead
0034settings = DeprecatedSettings(default=dict())
0035
0036class environ_getter(object):
0037 """For delegating an attribute to a key in self.environ."""
0038 # @@: Also __set__? Should setting be allowed?
0039 def __init__(self, key, default='', default_factory=None):
0040 self.key = key
0041 self.default = default
0042 self.default_factory = default_factory
0043 def __get__(self, obj, type=None):
0044 if type is None:
0045 return self
0046 if self.key not in obj.environ:
0047 if self.default_factory:
0048 val = obj.environ[self.key] = self.default_factory()
0049 return val
0050 else:
0051 return self.default
0052 return obj.environ[self.key]
0053
0054 def __repr__(self):
0055 return '<Proxy for WSGI environ %r key>' % self.key
0056
0057class WSGIRequest(object):
0058 """WSGI Request API Object
0059
0060 This object represents a WSGI request with a more friendly interface.
0061 This does not expose every detail of the WSGI environment, and attempts
0062 to express nothing beyond what is available in the environment
0063 dictionary.
0064
0065 The only state maintained in this object is the desired ``charset``,
0066 its associated ``errors`` handler, and the ``decode_param_names``
0067 option.
0068
0069 The incoming parameter values will be automatically coerced to unicode
0070 objects of the ``charset`` encoding when ``charset`` is set. The
0071 incoming parameter names are not decoded to unicode unless the
0072 ``decode_param_names`` option is enabled.
0073
0074 When unicode is expected, ``charset`` will overridden by the the
0075 value of the ``Content-Type`` header's charset parameter if one was
0076 specified by the client.
0077
0078 The class variable ``defaults`` specifies default values for
0079 ``charset``, ``errors``, and ``langauge``. These can be overridden for the
0080 current request via the registry.
0081
0082 The ``language`` default value is considered the fallback during i18n
0083 translations to ensure in odd cases that mixed languages don't occur should
0084 the ``language`` file contain the string but not another language in the
0085 accepted languages list. The ``language`` value only applies when getting
0086 a list of accepted languages from the HTTP Accept header.
0087
0088 This behavior is duplicated from Aquarium, and may seem strange but is
0089 very useful. Normally, everything in the code is in "en-us". However,
0090 the "en-us" translation catalog is usually empty. If the user requests
0091 ``["en-us", "zh-cn"]`` and a translation isn't found for a string in
0092 "en-us", you don't want gettext to fallback to "zh-cn". You want it to
0093 just use the string itself. Hence, if a string isn't found in the
0094 ``language`` catalog, the string in the source code will be used.
0095
0096 *All* other state is kept in the environment dictionary; this is
0097 essential for interoperability.
0098
0099 You are free to subclass this object.
0100
0101 """
0102 defaults = StackedObjectProxy(default=dict(charset=None, errors='replace',
0103 decode_param_names=False,
0104 language='en-us'))
0105 def __init__(self, environ):
0106 self.environ = environ
0107 # This isn't "state" really, since the object is derivative:
0108 self.headers = EnvironHeaders(environ)
0109
0110 defaults = self.defaults._current_obj()
0111 self.charset = defaults.get('charset')
0112 if self.charset:
0113 # There's a charset: params will be coerced to unicode. In that
0114 # case, attempt to use the charset specified by the browser
0115 browser_charset = self.determine_browser_charset()
0116 if browser_charset:
0117 self.charset = browser_charset
0118 self.errors = defaults.get('errors', 'strict')
0119 self.decode_param_names = defaults.get('decode_param_names', False)
0120 self._languages = None
0121
0122 body = environ_getter('wsgi.input')
0123 scheme = environ_getter('wsgi.url_scheme')
0124 method = environ_getter('REQUEST_METHOD')
0125 script_name = environ_getter('SCRIPT_NAME')
0126 path_info = environ_getter('PATH_INFO')
0127
0128 def urlvars(self):
0129 """
0130 Return any variables matched in the URL (e.g.,
0131 ``wsgiorg.routing_args``).
0132 """
0133 if 'paste.urlvars' in self.environ:
0134 return self.environ['paste.urlvars']
0135 elif 'wsgiorg.routing_args' in self.environ:
0136 return self.environ['wsgiorg.routing_args'][1]
0137 else:
0138 return {}
0139 urlvars = property(urlvars, doc=urlvars.__doc__)
0140
0141 def is_xhr(self):
0142 """Returns a boolean if X-Requested-With is present and a XMLHttpRequest"""
0143 return self.environ.get('HTTP_X_REQUESTED_WITH', '') == 'XMLHttpRequest'
0144 is_xhr = property(is_xhr, doc=is_xhr.__doc__)
0145
0146 def host(self):
0147 """Host name provided in HTTP_HOST, with fall-back to SERVER_NAME"""
0148 return self.environ.get('HTTP_HOST', self.environ.get('SERVER_NAME'))
0149 host = property(host, doc=host.__doc__)
0150
0151 def languages(self):
0152 """Return a list of preferred languages, most preferred first.
0153
0154 The list may be empty.
0155 """
0156 if self._languages is not None:
0157 return self._languages
0158 acceptLanguage = self.environ.get('HTTP_ACCEPT_LANGUAGE')
0159 langs = ACCEPT_LANGUAGE.parse(self.environ)
0160 fallback = self.defaults.get('language', 'en-us')
0161 if not fallback:
0162 return langs
0163 if fallback not in langs:
0164 langs.append(fallback)
0165 index = langs.index(fallback)
0166 langs[index+1:] = []
0167 self._languages = langs
0168 return self._languages
0169 languages = property(languages, doc=languages.__doc__)
0170
0171 def _GET(self):
0172 return parse_dict_querystring(self.environ)
0173
0174 def GET(self):
0175 """
0176 Dictionary-like object representing the QUERY_STRING
0177 parameters. Always present, if possibly empty.
0178
0179 If the same key is present in the query string multiple times, a
0180 list of its values can be retrieved from the ``MultiDict`` via
0181 the ``getall`` method.
0182
0183 Returns a ``MultiDict`` container or a ``UnicodeMultiDict`` when
0184 ``charset`` is set.
0185 """
0186 params = self._GET()
0187 if self.charset:
0188 params = UnicodeMultiDict(params, encoding=self.charset,
0189 errors=self.errors,
0190 decode_keys=self.decode_param_names)
0191 return params
0192 GET = property(GET, doc=GET.__doc__)
0193
0194 def _POST(self):
0195 return parse_formvars(self.environ, include_get_vars=False)
0196
0197 def POST(self):
0198 """Dictionary-like object representing the POST body.
0199
0200 Most values are encoded strings, or unicode strings when
0201 ``charset`` is set. There may also be FieldStorage objects
0202 representing file uploads. If this is not a POST request, or the
0203 body is not encoded fields (e.g., an XMLRPC request) then this
0204 will be empty.
0205
0206 This will consume wsgi.input when first accessed if applicable,
0207 but the raw version will be put in
0208 environ['paste.parsed_formvars'].
0209
0210 Returns a ``MultiDict`` container or a ``UnicodeMultiDict`` when
0211 ``charset`` is set.
0212 """
0213 params = self._POST()
0214 if self.charset:
0215 params = UnicodeMultiDict(params, encoding=self.charset,
0216 errors=self.errors,
0217 decode_keys=self.decode_param_names)
0218 return params
0219 POST = property(POST, doc=POST.__doc__)
0220
0221 def params(self):
0222 """Dictionary-like object of keys from POST, GET, URL dicts
0223
0224 Return a key value from the parameters, they are checked in the
0225 following order: POST, GET, URL
0226
0227 Additional methods supported:
0228
0229 ``getlist(key)``
0230 Returns a list of all the values by that key, collected from
0231 POST, GET, URL dicts
0232
0233 Returns a ``MultiDict`` container or a ``UnicodeMultiDict`` when
0234 ``charset`` is set.
0235 """
0236 params = MultiDict()
0237 params.update(self._POST())
0238 params.update(self._GET())
0239 if self.charset:
0240 params = UnicodeMultiDict(params, encoding=self.charset,
0241 errors=self.errors,
0242 decode_keys=self.decode_param_names)
0243 return params
0244 params = property(params, doc=params.__doc__)
0245
0246 def cookies(self):
0247 """Dictionary of cookies keyed by cookie name.
0248
0249 Just a plain dictionary, may be empty but not None.
0250
0251 """
0252 return get_cookie_dict(self.environ)
0253 cookies = property(cookies, doc=cookies.__doc__)
0254
0255 def determine_browser_charset(self):
0256 """
0257 Determine the encoding as specified by the browser via the
0258 Content-Type's charset parameter, if one is set
0259 """
0260 charset_match = _CHARSET_RE.match(self.headers.get('Content-Type', ''))
0261 if charset_match:
0262 return charset_match.group(1)
0263
0264 def match_accept(self, mimetypes):
0265 """Return a list of specified mime-types that the browser's HTTP Accept
0266 header allows in the order provided."""
0267 return desired_matches(mimetypes,
0268 self.environ.get('HTTP_ACCEPT', '*/*'))
0269
0270 def __repr__(self):
0271 """Show important attributes of the WSGIRequest"""
0272 pf = pformat
0273 msg = '<%s.%s object at 0x%x method=%s,' % (self.__class__.__module__, self.__class__.__name__,
0275 id(self), pf(self.method))
0276 msg += '\nscheme=%s, host=%s, script_name=%s, path_info=%s,' % (pf(self.scheme), pf(self.host), pf(self.script_name),
0278 pf(self.path_info))
0279 msg += '\nlanguges=%s,' % pf(self.languages)
0280 if self.charset:
0281 msg += ' charset=%s, errors=%s,' % (pf(self.charset),
0282 pf(self.errors))
0283 msg += '\nGET=%s,' % pf(self.GET)
0284 msg += '\nPOST=%s,' % pf(self.POST)
0285 msg += '\ncookies=%s>' % pf(self.cookies)
0286 return msg
0287
0288class WSGIResponse(object):
0289 """A basic HTTP response with content, headers, and out-bound cookies
0290
0291 The class variable ``defaults`` specifies default values for
0292 ``content_type``, ``charset`` and ``errors``. These can be overridden
0293 for the current request via the registry.
0294
0295 """
0296 defaults = StackedObjectProxy(
0297 default=dict(content_type='text/html', charset='utf-8',
0298 errors='strict', headers={'Cache-Control':'no-cache'})
0299 )
0300 def __init__(self, content='', mimetype=None, code=200):
0301 self._iter = None
0302 self._is_str_iter = True
0303
0304 self.content = content
0305 self.headers = HeaderDict()
0306 self.cookies = SimpleCookie()
0307 self.status_code = code
0308
0309 defaults = self.defaults._current_obj()
0310 if not mimetype:
0311 mimetype = defaults.get('content_type', 'text/html')
0312 charset = defaults.get('charset')
0313 if charset:
0314 mimetype = '%s; charset=%s' % (mimetype, charset)
0315 self.headers.update(defaults.get('headers', {}))
0316 self.headers['Content-Type'] = mimetype
0317 self.errors = defaults.get('errors', 'strict')
0318
0319 def __str__(self):
0320 """Returns a rendition of the full HTTP message, including headers.
0321
0322 When the content is an iterator, the actual content is replaced with the
0323 output of str(iterator) (to avoid exhausting the iterator).
0324 """
0325 if self._is_str_iter:
0326 content = ''.join(self.get_content())
0327 else:
0328 content = str(self.content)
0329 return '\n'.join(['%s: %s' % (key, value)
0330 for key, value in self.headers.headeritems()]) + '\n\n' + content
0332
0333 def __call__(self, environ, start_response):
0334 """Convenience call to return output and set status information
0335
0336 Conforms to the WSGI interface for calling purposes only.
0337
0338 Example usage:
0339
0340 .. code-block:: Python
0341
0342 def wsgi_app(environ, start_response):
0343 response = WSGIResponse()
0344 response.write("Hello world")
0345 response.headers['Content-Type'] = 'latin1'
0346 return response(environ, start_response)
0347
0348 """
0349 status_text = STATUS_CODE_TEXT[self.status_code]
0350 status = '%s %s' % (self.status_code, status_text)
0351 response_headers = self.headers.headeritems()
0352 for c in self.cookies.values():
0353 response_headers.append(('Set-Cookie', c.output(header='')))
0354 start_response(status, response_headers)
0355 is_file = isinstance(self.content, file)
0356 if 'wsgi.file_wrapper' in environ and is_file:
0357 return environ['wsgi.file_wrapper'](self.content)
0358 elif is_file:
0359 return iter(lambda: self.content.read(), '')
0360 return self.get_content()
0361
0362 def determine_charset(self):
0363 """
0364 Determine the encoding as specified by the Content-Type's charset
0365 parameter, if one is set
0366 """
0367 charset_match = _CHARSET_RE.match(self.headers.get('Content-Type', ''))
0368 if charset_match:
0369 return charset_match.group(1)
0370
0371 def has_header(self, header):
0372 """
0373 Case-insensitive check for a header
0374 """
0375 warnings.warn('WSGIResponse.has_header is deprecated, use '
0376 'WSGIResponse.headers.has_key instead', DeprecationWarning,
0377 2)
0378 return self.headers.has_key(header)
0379
0380 def set_cookie(self, key, value='', max_age=None, expires=None, path='/',
0381 domain=None, secure=None):
0382 """
0383 Define a cookie to be sent via the outgoing HTTP headers
0384 """
0385 self.cookies[key] = value
0386 for var_name, var_value in [
0387 ('max_age', max_age), ('path', path), ('domain', domain),
0388 ('secure', secure), ('expires', expires)]:
0389 if var_value is not None and var_value is not False:
0390 self.cookies[key][var_name.replace('_', '-')] = var_value
0391
0392 def delete_cookie(self, key, path='/', domain=None):
0393 """
0394 Notify the browser the specified cookie has expired and should be
0395 deleted (via the outgoing HTTP headers)
0396 """
0397 self.cookies[key] = ''
0398 if path is not None:
0399 self.cookies[key]['path'] = path
0400 if domain is not None:
0401 self.cookies[key]['domain'] = path
0402 self.cookies[key]['expires'] = 0
0403 self.cookies[key]['max-age'] = 0
0404
0405 def _set_content(self, content):
0406 if hasattr(content, '__iter__'):
0407 self._iter = content
0408 if isinstance(content, list):
0409 self._is_str_iter = True
0410 else:
0411 self._is_str_iter = False
0412 else:
0413 self._iter = [content]
0414 self._is_str_iter = True
0415 content = property(lambda self: self._iter, _set_content,
0416 doc='Get/set the specified content, where content can '
0417 'be: a string, a list of strings, a generator function '
0418 'that yields strings, or an iterable object that '
0419 'produces strings.')
0420
0421 def get_content(self):
0422 """
0423 Returns the content as an iterable of strings, encoding each element of
0424 the iterator from a Unicode object if necessary.
0425 """
0426 charset = self.determine_charset()
0427 if charset:
0428 return encode_unicode_app_iter(self.content, charset, self.errors)
0429 else:
0430 return self.content
0431
0432 def wsgi_response(self):
0433 """
0434 Return this WSGIResponse as a tuple of WSGI formatted data, including:
0435 (status, headers, iterable)
0436 """
0437 status_text = STATUS_CODE_TEXT[self.status_code]
0438 status = '%s %s' % (self.status_code, status_text)
0439 response_headers = self.headers.headeritems()
0440 for c in self.cookies.values():
0441 response_headers.append(('Set-Cookie', c.output(header='')))
0442 return status, response_headers, self.get_content()
0443
0444 # The remaining methods partially implement the file-like object interface.
0445 # See http://docs.python.org/lib/bltin-file-objects.html
0446 def write(self, content):
0447 if not self._is_str_iter:
0448 raise IOError, "This %s instance's content is not writable: (content " 'is an iterator)' % self.__class__.__name__
0450 self.content.append(content)
0451
0452 def flush(self):
0453 pass
0454
0455 def tell(self):
0456 if not self._is_str_iter:
0457 raise IOError, 'This %s instance cannot tell its position: (content ' 'is an iterator)' % self.__class__.__name__
0459 return sum([len(chunk) for chunk in self._iter])
0460
0461 ########################################
0462 ## Content-type and charset
0463
0464 def charset__get(self):
0465 """
0466