1
"""Utility functions and classes available for use by Controllers
3
Pylons subclasses the `WebOb <http://pythonpaste.org/webob/>`_
4
:class:`webob.Request` and :class:`webob.Response` classes to provide
5
backwards compatible functions for earlier versions of Pylons as well
6
as add a few helper functions to assist with signed cookies.
8
For reference use, refer to the :class:`Request` and :class:`Response`
13
:func:`abort`, :func:`forward`, :func:`etag_cache`,
14
:func:`mimetype` and :func:`redirect`
22
import cPickle as pickle
26
from hashlib import sha1
30
from webob import Request as WebObRequest
31
from webob import Response as WebObResponse
32
from webob.exc import status_map
36
__all__ = ['abort', 'etag_cache', 'redirect', 'redirect_to', 'Request',
39
log = logging.getLogger(__name__)
41
IF_NONE_MATCH = re.compile('(?:W/)?(?:"([^"]*)",?\s*)')
44
class Request(WebObRequest):
45
"""WebOb Request subclass
47
The WebOb :class:`webob.Request` has no charset, or other defaults. This subclass
48
adds defaults, along with several methods for backwards
49
compatibility with paste.wsgiwrappers.WSGIRequest.
52
def determine_browser_charset(self):
53
"""Legacy method to return the
54
:attr:`webob.Request.accept_charset`"""
55
return self.accept_charset
58
return self.accept_language.best_matches(self.language)
59
languages = property(languages)
61
def match_accept(self, mimetypes):
62
return self.accept.first_match(mimetypes)
64
def signed_cookie(self, name, secret):
65
"""Extract a signed cookie of ``name`` from the request
67
The cookie is expected to have been created with
68
``Response.signed_cookie``, and the ``secret`` should be the
69
same as the one used to sign it.
71
Any failure in the signature of the data will result in None
75
cookie = self.str_cookies.get(name)
79
sig, pickled = cookie[:40], base64.decodestring(cookie[40:])
80
except binascii.Error:
81
# Badly formed data can make base64 die
83
if hmac.new(secret, pickled, sha1).hexdigest() == sig:
84
return pickle.loads(pickled)
87
class Response(WebObResponse):
88
"""WebOb Response subclass
90
The WebOb Response has no default content type, or error defaults.
91
This subclass adds defaults, along with several methods for
92
backwards compatibility with paste.wsgiwrappers.WSGIResponse.
95
content = WebObResponse.body
97
def determine_charset(self):
100
def has_header(self, header):
101
return header in self.headers
103
def get_content(self):
106
def write(self, content):
107
self.body_file.write(content)
109
def wsgi_response(self):
110
return self.status, self.headers, self.body
112
def signed_cookie(self, name, data, secret=None, **kwargs):
113
"""Save a signed cookie with ``secret`` signature
115
Saves a signed cookie of the pickled data. All other keyword
116
arguments that ``WebOb.set_cookie`` accepts are usable and
117
passed to the WebOb set_cookie method after creating the signed
121
pickled = pickle.dumps(data, pickle.HIGHEST_PROTOCOL)
122
sig = hmac.new(secret, pickled, sha1).hexdigest()
123
self.set_cookie(name, sig + base64.encodestring(pickled), **kwargs)
126
def etag_cache(key=None):
127
"""Use the HTTP Entity Tag cache for Browser side caching
129
If a "If-None-Match" header is found, and equivilant to ``key``,
130
then a ``304`` HTTP message will be returned with the ETag to tell
131
the browser that it should use its current cache of the page.
133
Otherwise, the ETag header will be added to the response headers.
136
Suggested use is within a Controller Action like so:
138
.. code-block:: python
142
class YourController(BaseController):
145
return render('/splash.mako')
148
This works because etag_cache will raise an HTTPNotModified
149
exception if the ETag received matches the key provided.
152
if_none_matches = IF_NONE_MATCH.findall(
153
pylons.request.environ.get('HTTP_IF_NONE_MATCH', ''))
154
response = pylons.response._current_obj()
155
response.headers['ETag'] = '"%s"' % key
156
if str(key) in if_none_matches:
157
log.debug("ETag match, returning 304 HTTP Not Modified Response")
158
response.headers.pop('Content-Type', None)
159
response.headers.pop('Cache-Control', None)
160
response.headers.pop('Pragma', None)
161
raise status_map[304]().exception
163
log.debug("ETag didn't match, returning response object")
166
def forward(wsgi_app):
167
"""Forward the request to a WSGI application. Returns its response.
169
.. code-block:: python
171
return forward(FileApp('filename'))
174
environ = pylons.request.environ
175
controller = environ.get('pylons.controller')
176
if not controller or not hasattr(controller, 'start_response'):
177
raise RuntimeError("Unable to forward: environ['pylons.controller'] "
178
"is not a valid Pylons controller")
179
return wsgi_app(environ, controller.start_response)
182
def abort(status_code=None, detail="", headers=None, comment=None):
183
"""Aborts the request immediately by returning an HTTP exception
185
In the event that the status_code is a 300 series error, the detail
186
attribute will be used as the Location header should one not be
187
specified in the headers attribute.
190
exc = status_map[status_code](detail=detail, headers=headers,
192
log.debug("Aborting request, status: %s, detail: %r, headers: %r, "
193
"comment: %r", status_code, detail, headers, comment)
197
def redirect(url, code=302):
198
"""Raises a redirect exception to the specified URL
200
Optionally, a code variable may be passed with the status code of
203
redirect(url(controller='home', action='index'), code=303)
206
log.debug("Generating %s redirect" % code)
207
exc = status_map[code]
208
raise exc(location=url).exception