pylons.controllers.util
Covered: 118 lines
Missed: 33 lines
Skipped 58 lines
Percent: 78 %

      1

    """Utility functions and classes available for use by Controllers

      2

      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.

      7

      8

    For reference use, refer to the :class:`Request` and :class:`Response`

      9

    below.

     10

     11

    Functions available:

     12

     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

     29

     30

    from webob import Request as WebObRequest

     31

    from webob import Response as WebObResponse

     32

    from webob.exc import status_map

     33

     34

    import pylons

     35

     36

    __all__ = ['abort', 'etag_cache', 'redirect', 'redirect_to', 'Request',

     37

               'Response']

     38

     39

    log = logging.getLogger(__name__)

     40

     41

    IF_NONE_MATCH = re.compile('(?:W/)?(?:"([^"]*)",?\s*)')

     42

     43

     44

    class Request(WebObRequest):

     45

        """WebOb Request subclass

     46

     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.

     50

     51

        """

     52

        def determine_browser_charset(self):

     53

            """Legacy method to return the

     54

            :attr:`webob.Request.accept_charset`"""

     55

            return self.accept_charset

     56

     57

        def languages(self):

     58

            return self.accept_language.best_matches(self.language)

     59

        languages = property(languages)

     60

     61

        def match_accept(self, mimetypes):

     62

            return self.accept.first_match(mimetypes)

     63

     64

        def signed_cookie(self, name, secret):

     65

            """Extract a signed cookie of ``name`` from the request

     66

     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.

     70

     71

            Any failure in the signature of the data will result in None

     72

            being returned.

     73

     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:

     81

                # Badly formed data can make base64 die

     82

                return

     83

            if hmac.new(secret, pickled, sha1).hexdigest() == sig:

     84

                return pickle.loads(pickled)

     85

     86

     87

    class Response(WebObResponse):

     88

        """WebOb Response subclass

     89

     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.

     93

     94

        """

     95

        content = WebObResponse.body

     96

     97

        def determine_charset(self):

     98

            return self.charset

     99

    100

        def has_header(self, header):

    101

            return header in self.headers

    102

    103

        def get_content(self):

    104

            return self.body

    105

    106

        def write(self, content):

    107

            self.body_file.write(content)

    108

    109

        def wsgi_response(self):

    110

            return self.status, self.headers, self.body

    111

    112

        def signed_cookie(self, name, data, secret=None, **kwargs):

    113

            """Save a signed cookie with ``secret`` signature

    114

    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.

    119

    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)

    124

    125

    126

    def etag_cache(key=None):

    127

        """Use the HTTP Entity Tag cache for Browser side caching

    128

    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.

    132

    133

        Otherwise, the ETag header will be added to the response headers.

    134

    135

    136

        Suggested use is within a Controller Action like so:

    137

    138

        .. code-block:: python

    139

    140

            import pylons

    141

    142

            class YourController(BaseController):

    143

                def index(self):

    144

                    etag_cache(key=1)

    145

                    return render('/splash.mako')

    146

    147

        .. note::

    148

            This works because etag_cache will raise an HTTPNotModified

    149

            exception if the ETag received matches the key provided.

    150

    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")

    164

    165

    166

    def forward(wsgi_app):

    167

        """Forward the request to a WSGI application. Returns its response.

    168

    169

        .. code-block:: python

    170

    171

            return forward(FileApp('filename'))

    172

    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)

    180

    181

    182

    def abort(status_code=None, detail="", headers=None, comment=None):

    183

        """Aborts the request immediately by returning an HTTP exception

    184

    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.

    188

    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

    195

    196

    197

    def redirect(url, code=302):

    198

        """Raises a redirect exception to the specified URL

    199

    200

        Optionally, a code variable may be passed with the status code of

    201

        the redirect, ie::

    202

    203

            redirect(url(controller='home', action='index'), code=303)

    204

    205

        """

    206

        log.debug("Generating %s redirect" % code)

    207

        exc = status_map[code]

    208

        raise exc(location=url).exception

    209