pylons.controllers.util
Covered: 118 lines
Missed: 33 lines
Skipped 58 lines
Percent: 78 %
  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`
  9
below.
 11
Functions available:
 13
:func:`abort`, :func:`forward`, :func:`etag_cache`, 
 14
:func:`mimetype` and :func:`redirect`
 15
"""
 16
import base64
 17
import binascii
 18
import hmac
 19
import logging
 20
import re
 21
try:
 22
    import cPickle as pickle
 23
except ImportError:
 24
    import pickle
 25
try:
 26
    from hashlib import sha1
 27
except ImportError:
 28
    import sha as sha1
 30
from webob import Request as WebObRequest
 31
from webob import Response as WebObResponse
 32
from webob.exc import status_map
 34
import pylons
 36
__all__ = ['abort', 'etag_cache', 'redirect', 'redirect_to', 'Request',
 37
           'Response']
 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.
 51
    """    
 52
    def determine_browser_charset(self):
 53
        """Legacy method to return the
 54
        :attr:`webob.Request.accept_charset`"""
 55
        return self.accept_charset
 57
    def languages(self):
 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
 72
        being returned.
 74
        """
 75
        cookie = self.str_cookies.get(name)
 76
        if not cookie:
 77
            return
 78
        try:
 79
            sig, pickled = cookie[:40], base64.decodestring(cookie[40:])
 80
        except binascii.Error:
 82
            return
 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.
 94
    """
 95
    content = WebObResponse.body
 97
    def determine_charset(self):
 98
        return self.charset
100
    def has_header(self, header):
101
        return header in self.headers
103
    def get_content(self):
104
        return self.body
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
118
        cookie value.
120
        """
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
140
        import pylons
142
        class YourController(BaseController):
143
            def index(self):
144
                etag_cache(key=1)
145
                return render('/splash.mako')
147
    .. note::
148
        This works because etag_cache will raise an HTTPNotModified
149
        exception if the ETag received matches the key provided.
151
    """
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
162
    else:
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'))
173
    """
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.
189
    """
190
    exc = status_map[status_code](detail=detail, headers=headers, 
191
                                  comment=comment)
192
    log.debug("Aborting request, status: %s, detail: %r, headers: %r, "
193
              "comment: %r", status_code, detail, headers, comment)
194
    raise exc.exception
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
201
    the redirect, ie::
203
        redirect(url(controller='home', action='index'), code=303)
205
    """
206
    log.debug("Generating %s redirect" % code)
207
    exc = status_map[code]
208
    raise exc(location=url).exception