Latest Version: 0.9.6.2
/Users/bbangert/Programming/Python/Paste/paste/errordocument.py
0001# (c) 2005-2006 James Gardner <james@pythonweb.org>
0002# This module is part of the Python Paste Project and is released under
0003# the MIT License: http://www.opensource.org/licenses/mit-license.php
0004"""
0005Middleware to display error documents for certain status codes
0006
0007The middleware in this module can be used to intercept responses with
0008specified status codes and internally forward the request to an appropriate
0009URL where the content can be displayed to the user as an error document.
0010"""
0011
0012import warnings
0013from urlparse import urlparse
0014from paste.recursive import ForwardRequestException, RecursiveMiddleware
0015from paste.util import converters
0016from paste.response import replace_header
0017
0018def forward(app, codes):
0019    """
0020    Intercepts a response with a particular status code and returns the 
0021    content from a specified URL instead.
0022    
0023    The arguments are:
0024    
0025    ``app``
0026        The WSGI application or middleware chain.
0027
0028    ``codes``
0029        A dictionary of integer status codes and the URL to be displayed
0030        if the response uses that code.
0031        
0032    For example, you might want to create a static file to display a 
0033    "File Not Found" message at the URL ``/error404.html`` and then use
0034    ``forward`` middleware to catch all 404 status codes and display the page
0035    you created. In this example ``app`` is your exisiting WSGI 
0036    applicaiton::
0037
0038        from paste.errordocument import forward
0039        app = forward(app, codes={404:'/error404.html'})
0040
0041    """
0042    for code in codes:
0043        if not isinstance(code, int):
0044            raise TypeError('All status codes should be type int. '
0045                '%s is not valid'%repr(code))
0046
0047    def error_codes_mapper(code, message, environ, global_conf, codes):
0048        if codes.has_key(code):
0049            return codes[code]
0050        else:
0051            return None
0052
0053    #return _StatusBasedRedirect(app, error_codes_mapper, codes=codes)
0054    return RecursiveMiddleware(
0055        StatusBasedForward(
0056            app,
0057            error_codes_mapper,
0058            codes=codes,
0059        )
0060    )
0061
0062class StatusKeeper(object):
0063    def __init__(self, app, status, url, headers):
0064        self.app = app
0065        self.status = status
0066        self.url = url
0067        self.headers = headers
0068
0069    def __call__(self, environ, start_response):
0070        def keep_status_start_response(status, headers, exc_info=None):
0071            for header, value in headers:
0072                if header.lower() == 'set-cookie':
0073                    self.headers.append((header, value))
0074                else:
0075                    replace_header(self.headers, header, value)
0076            return start_response(self.status, self.headers, exc_info)
0077        parts = self.url.split('?')
0078        environ['PATH_INFO'] = parts[0]
0079        if len(parts) > 1:
0080            environ['QUERY_STRING'] = parts[1]
0081        else:
0082            environ['QUERY_STRING'] = ''
0083        #raise Exception(self.url, self.status)
0084        return self.app(environ, keep_status_start_response)
0085
0086class StatusBasedForward(object):
0087    """
0088    Middleware that lets you test a response against a custom mapper object to
0089    programatically determine whether to internally forward to another URL and
0090    if so, which URL to forward to.
0091    
0092    If you don't need the full power of this middleware you might choose to use
0093    the simpler ``forward`` middleware instead.
0094
0095    The arguments are:
0096    
0097    ``app``
0098        The WSGI application or middleware chain.
0099        
0100    ``mapper`` 
0101        A callable that takes a status code as the
0102        first parameter, a message as the second, and accepts optional environ,
0103        global_conf and named argments afterwards. It should return a
0104        URL to forward to or ``None`` if the code is not to be intercepted.
0105
0106    ``global_conf``
0107        Optional default configuration from your config file. If ``debug`` is
0108        set to ``true`` a message will be written to ``wsgi.errors`` on each
0109        internal forward stating the URL forwarded to.
0110    
0111    ``**params`` 
0112        Optional, any other configuration and extra arguments you wish to 
0113        pass which will in turn be passed back to the custom mapper object.
0114
0115    Here is an example where a ``404 File Not Found`` status response would be
0116    redirected to the URL ``/error?code=404&message=File%20Not%20Found``. This 
0117    could be useful for passing the status code and message into another 
0118    application to display an error document:
0119    
0120    .. code-block:: Python
0121    
0122        from paste.errordocument import StatusBasedForward
0123        from paste.recursive import RecursiveMiddleware
0124        from urllib import urlencode
0125        
0126        def error_mapper(code, message, environ, global_conf, kw)
0127            if code in [404, 500]:
0128                params = urlencode({'message':message, 'code':code})
0129                url = '/error?'%(params)
0130                return url
0131            else:
0132                return None
0133    
0134        app = RecursiveMiddleware(
0135            StatusBasedForward(app, mapper=error_mapper),
0136        )
0137
0138    """
0139
0140    def __init__(self, app, mapper, global_conf=None, **params):
0141        if global_conf is None:
0142            global_conf = {}
0143        # @@: global_conf shouldn't really come in here, only in a
0144        # separate make_status_based_forward function
0145        if global_conf:
0146            self.debug = converters.asbool(global_conf.get('debug', False))
0147        else:
0148            self.debug = False
0149        self.application = app
0150        self.mapper = mapper
0151        self.global_conf = global_conf
0152        self.params = params
0153
0154    def __call__(self, environ, start_response):
0155        url = []
0156
0157        def change_response(status, headers, exc_info=None):
0158            status_code = status.split(' ')
0159            try:
0160                code = int(status_code[0])
0161            except (ValueError, TypeError):
0162                raise Exception(
0163                    'StatusBasedForward middleware '
0164                    'received an invalid status code %s'%repr(status_code[0])
0165                )
0166            message = ' '.join(status_code[1:])
0167            new_url = self.mapper(
0168                code,
0169                message,
0170                environ,
0171                self.global_conf,
0172                **self.params
0173            )
0174            if not (new_url == None or isinstance(new_url, str)):
0175                raise TypeError(
0176                    'Expected the url to internally '
0177                    'redirect to in the StatusBasedForward mapper'
0178                    'to be a string or None, not %s'%repr(new_url)
0179                )
0180            if new_url:
0181                url.append([new_url, status, headers])
0182            else:
0183                return start_response(status, headers, exc_info)
0184
0185        app_iter = self.application(environ, change_response)
0186        if url:
0187            if hasattr(app_iter, 'close'):
0188                app_iter.close()
0189
0190            def factory(app):
0191                return StatusKeeper(app, status=url[0][1], url=url[0][0],
0192                                    headers=url[0][2])
0193            raise ForwardRequestException(factory=factory)
0194        else:
0195            return app_iter
0196
0197def make_errordocument(app, global_conf, **kw):
0198    """
0199    Paste Deploy entry point to create a error document wrapper. 
0200    
0201    Use like::
0202
0203        [filter-app:main]
0204        use = egg:Paste#errordocument
0205        next = real-app
0206        500 = /lib/msg/500.html
0207        404 = /lib/msg/404.html
0208    """
0209    map = {}
0210    for status, redir_loc in kw.items():
0211        try:
0212            status = int(status)
0213        except ValueError:
0214            raise ValueError('Bad status code: %r' % status)
0215        map[status] = redir_loc
0216    forwarder = forward(app, map)
0217    return forwarder
0218
0219__pudge_all__ = [
0220    'forward',
0221    'make_errordocument',
0222    'empty_error',
0223    'make_empty_error',
0224    'StatusBasedForward',
0225]
0226
0227
0228###############################################################################
0229## Deprecated
0230###############################################################################
0231
0232def custom_forward(app, mapper, global_conf=None, **kw):
0233    """
0234    Deprectated; use StatusBasedForward instead.
0235    """
0236    warnings.warn(
0237        "errordocuments.custom_forward has been deprecated; please "
0238        "use errordocuments.StatusBasedForward",
0239        DeprecationWarning, 2)
0240    if global_conf is None:
0241        global_conf = {}
0242    return _StatusBasedRedirect(app, mapper, global_conf, **kw)
0243
0244class _StatusBasedRedirect(object):
0245    """
0246    Deprectated; use StatusBasedForward instead.
0247    """
0248    def __init__(self, app, mapper, global_conf=None, **kw):
0249
0250        warnings.warn(
0251            "errordocuments._StatusBasedRedirect has been deprecated; please "
0252            "use errordocuments.StatusBasedForward",
0253            DeprecationWarning, 2)
0254
0255        if global_conf is None:
0256            global_conf = {}
0257        self.application = app
0258        self.mapper = mapper
0259        self.global_conf = global_conf
0260        self.kw = kw
0261        self.fallback_template = """
0262            <html>
0263            <head>
0264            <title>Error %(code)s</title>
0265            </html>
0266            <body>
0267            <h1>Error %(code)s</h1>
0268            <p>%(message)s</p>
0269            <hr>
0270            <p>
0271                Additionally an error occurred trying to produce an 
0272                error document.  A description of the error was logged
0273                to <tt>wsgi.errors</tt>.
0274            </p>
0275            </body>
0276            </html>                
0277        """
0278
0279    def __call__(self, environ, start_response):
0280        url = []
0281        code_message = []
0282        try:
0283            def change_response(status, headers, exc_info=None):
0284                new_url = None
0285                parts = status.split(' ')
0286                try:
0287                    code = int(parts[0])
0288                except ValueError, TypeError:
0289                    raise Exception(
0290                        '_StatusBasedRedirect middleware '
0291                        'received an invalid status code %s'%repr(parts[0])
0292                    )
0293                message = ' '.join(parts[1:])
0294                new_url = self.mapper(
0295                    code,
0296                    message,
0297                    environ,
0298                    self.global_conf,
0299                    self.kw
0300                )
0301                if not (new_url == None or isinstance(new_url, str)):
0302                    raise TypeError(
0303                        'Expected the url to internally '
0304                        'redirect to in the _StatusBasedRedirect error_mapper'
0305                        'to be a string or None, not %s'%repr(new_url)
0306                    )
0307                if new_url:
0308                    url.append(new_url)
0309                code_message.append([code, message])
0310                return start_response(status, headers, exc_info)
0311            app_iter = self.application(environ, change_response)
0312        except:
0313            try:
0314                import sys
0315                error = str(sys.exc_info()[1])
0316            except:
0317                error = ''
0318            try:
0319                code, message = code_message[0]
0320            except:
0321                code, message = ['', '']
0322            environ['wsgi.errors'].write(
0323                'Error occurred in _StatusBasedRedirect '
0324                'intercepting the response: '+str(error)
0325            )
0326            return [self.fallback_template
0327                    % {'message': message, 'code': code}]
0328        else:
0329            if url:
0330                url_ = url[0]
0331                new_environ = {}
0332                for k, v in environ.items():
0333                    if k != 'QUERY_STRING':
0334                        new_environ['QUERY_STRING'] = urlparse(url_)[4]
0335                    else:
0336                        new_environ[k] = v
0337                class InvalidForward(Exception):
0338                    pass
0339                def eat_start_response(status, headers, exc_info=None):
0340                    """
0341                    We don't want start_response to do anything since it
0342                    has already been called
0343                    """
0344                    if status[:3] != '200':
0345                        raise InvalidForward(
0346                            "The URL %s to internally forward "
0347                            "to in order to create an error document did not "
0348                            "return a '200' status code." % url_
0349                        )
0350                forward = environ['paste.recursive.forward']
0351                old_start_response = forward.start_response
0352                forward.start_response = eat_start_response
0353                try:
0354                    app_iter = forward(url_, new_environ)
0355                except InvalidForward, e:
0356                    code, message = code_message[0]
0357                    environ['wsgi.errors'].write(
0358                        'Error occurred in '
0359                        '_StatusBasedRedirect redirecting '
0360                        'to new URL: '+str(url[0])
0361                    )
0362                    return [
0363                        self.fallback_template%{
0364                            'message':message,
0365                            'code':code,
0366                        }
0367                    ]
0368                else:
0369                    forward.start_response = old_start_response
0370                    return app_iter
0371            else:
0372                return app_iter

Top