1# (c) 2005-2006 James Gardner <james@pythonweb.org>
2# This module is part of the Python Paste Project and is released under
3# the MIT License: http://www.opensource.org/licenses/mit-license.php
4"""
5Middleware to display error documents for certain status codes
6
7The middleware in this module can be used to intercept responses with
8specified status codes and internally forward the request to an appropriate
9URL where the content can be displayed to the user as an error document.
10"""
11
12import warnings
13import sys
14from six.moves.urllib import parse as urlparse
15from paste.recursive import ForwardRequestException, RecursiveMiddleware, RecursionLoop
16from paste.util import converters
17from paste.response import replace_header
18import six
19
20def forward(app, codes):
21    """
22    Intercepts a response with a particular status code and returns the
23    content from a specified URL instead.
24
25    The arguments are:
26
27    ``app``
28        The WSGI application or middleware chain.
29
30    ``codes``
31        A dictionary of integer status codes and the URL to be displayed
32        if the response uses that code.
33
34    For example, you might want to create a static file to display a
35    "File Not Found" message at the URL ``/error404.html`` and then use
36    ``forward`` middleware to catch all 404 status codes and display the page
37    you created. In this example ``app`` is your exisiting WSGI
38    applicaiton::
39
40        from paste.errordocument import forward
41        app = forward(app, codes={404:'/error404.html'})
42
43    """
44    for code in codes:
45        if not isinstance(code, int):
46            raise TypeError('All status codes should be type int. '
47                '%s is not valid'%repr(code))
48
49    def error_codes_mapper(code, message, environ, global_conf, codes):
50        if code in codes:
51            return codes[code]
52        else:
53            return None
54
55    #return _StatusBasedRedirect(app, error_codes_mapper, codes=codes)
56    return RecursiveMiddleware(
57        StatusBasedForward(
58            app,
59            error_codes_mapper,
60            codes=codes,
61        )
62    )
63
64class StatusKeeper(object):
65    def __init__(self, app, status, url, headers):
66        self.app = app
67        self.status = status
68        self.url = url
69        self.headers = headers
70
71    def __call__(self, environ, start_response):
72        def keep_status_start_response(status, headers, exc_info=None):
73            for header, value in headers:
74                if header.lower() == 'set-cookie':
75                    self.headers.append((header, value))
76                else:
77                    replace_header(self.headers, header, value)
78            return start_response(self.status, self.headers, exc_info)
79        parts = self.url.split('?')
80        environ['PATH_INFO'] = parts[0]
81        if len(parts) > 1:
82            environ['QUERY_STRING'] = parts[1]
83        else:
84            environ['QUERY_STRING'] = ''
85        #raise Exception(self.url, self.status)
86        try:
87            return self.app(environ, keep_status_start_response)
88        except RecursionLoop as e:
89            line = 'Recursion error getting error page: %s\n' % e
90            if six.PY3:
91                line = line.encode('utf8')
92            environ['wsgi.errors'].write(line)
93            keep_status_start_response('500 Server Error', [('Content-type', 'text/plain')], sys.exc_info())
94            body = ('Error: %s.  (Error page could not be fetched)'
95                    % self.status)
96            if six.PY3:
97                body = body.encode('utf8')
98            return [body]
99
100
101class StatusBasedForward(object):
102    """
103    Middleware that lets you test a response against a custom mapper object to
104    programatically determine whether to internally forward to another URL and
105    if so, which URL to forward to.
106
107    If you don't need the full power of this middleware you might choose to use
108    the simpler ``forward`` middleware instead.
109
110    The arguments are:
111
112    ``app``
113        The WSGI application or middleware chain.
114
115    ``mapper``
116        A callable that takes a status code as the
117        first parameter, a message as the second, and accepts optional environ,
118        global_conf and named argments afterwards. It should return a
119        URL to forward to or ``None`` if the code is not to be intercepted.
120
121    ``global_conf``
122        Optional default configuration from your config file. If ``debug`` is
123        set to ``true`` a message will be written to ``wsgi.errors`` on each
124        internal forward stating the URL forwarded to.
125
126    ``**params``
127        Optional, any other configuration and extra arguments you wish to
128        pass which will in turn be passed back to the custom mapper object.
129
130    Here is an example where a ``404 File Not Found`` status response would be
131    redirected to the URL ``/error?code=404&message=File%20Not%20Found``. This
132    could be useful for passing the status code and message into another
133    application to display an error document:
134
135    .. code-block:: python
136
137        from paste.errordocument import StatusBasedForward
138        from paste.recursive import RecursiveMiddleware
139        from urllib import urlencode
140
141        def error_mapper(code, message, environ, global_conf, kw)
142            if code in [404, 500]:
143                params = urlencode({'message':message, 'code':code})
144                url = '/error?'%(params)
145                return url
146            else:
147                return None
148
149        app = RecursiveMiddleware(
150            StatusBasedForward(app, mapper=error_mapper),
151        )
152
153    """
154
155    def __init__(self, app, mapper, global_conf=None, **params):
156        if global_conf is None:
157            global_conf = {}
158        # @@: global_conf shouldn't really come in here, only in a
159        # separate make_status_based_forward function
160        if global_conf:
161            self.debug = converters.asbool(global_conf.get('debug', False))
162        else:
163            self.debug = False
164        self.application = app
165        self.mapper = mapper
166        self.global_conf = global_conf
167        self.params = params
168
169    def __call__(self, environ, start_response):
170        url = []
171
172        def change_response(status, headers, exc_info=None):
173            status_code = status.split(' ')
174            try:
175                code = int(status_code[0])
176            except (ValueError, TypeError):
177                raise Exception(
178                    'StatusBasedForward middleware '
179                    'received an invalid status code %s'%repr(status_code[0])
180                )
181            message = ' '.join(status_code[1:])
182            new_url = self.mapper(
183                code,
184                message,
185                environ,
186                self.global_conf,
187                **self.params
188            )
189            if not (new_url == None or isinstance(new_url, str)):
190                raise TypeError(
191                    'Expected the url to internally '
192                    'redirect to in the StatusBasedForward mapper'
193                    'to be a string or None, not %r' % new_url)
194            if new_url:
195                url.append([new_url, status, headers])
196                # We have to allow the app to write stuff, even though
197                # we'll ignore it:
198                return [].append
199            else:
200                return start_response(status, headers, exc_info)
201
202        app_iter = self.application(environ, change_response)
203        if url:
204            if hasattr(app_iter, 'close'):
205                app_iter.close()
206
207            def factory(app):
208                return StatusKeeper(app, status=url[0][1], url=url[0][0],
209                                    headers=url[0][2])
210            raise ForwardRequestException(factory=factory)
211        else:
212            return app_iter
213
214def make_errordocument(app, global_conf, **kw):
215    """
216    Paste Deploy entry point to create a error document wrapper.
217
218    Use like::
219
220        [filter-app:main]
221        use = egg:Paste#errordocument
222        next = real-app
223        500 = /lib/msg/500.html
224        404 = /lib/msg/404.html
225    """
226    map = {}
227    for status, redir_loc in kw.items():
228        try:
229            status = int(status)
230        except ValueError:
231            raise ValueError('Bad status code: %r' % status)
232        map[status] = redir_loc
233    forwarder = forward(app, map)
234    return forwarder
235
236__pudge_all__ = [
237    'forward',
238    'make_errordocument',
239    'empty_error',
240    'make_empty_error',
241    'StatusBasedForward',
242]
243
244
245###############################################################################
246## Deprecated
247###############################################################################
248
249def custom_forward(app, mapper, global_conf=None, **kw):
250    """
251    Deprectated; use StatusBasedForward instead.
252    """
253    warnings.warn(
254        "errordocuments.custom_forward has been deprecated; please "
255        "use errordocuments.StatusBasedForward",
256        DeprecationWarning, 2)
257    if global_conf is None:
258        global_conf = {}
259    return _StatusBasedRedirect(app, mapper, global_conf, **kw)
260
261class _StatusBasedRedirect(object):
262    """
263    Deprectated; use StatusBasedForward instead.
264    """
265    def __init__(self, app, mapper, global_conf=None, **kw):
266
267        warnings.warn(
268            "errordocuments._StatusBasedRedirect has been deprecated; please "
269            "use errordocuments.StatusBasedForward",
270            DeprecationWarning, 2)
271
272        if global_conf is None:
273            global_conf = {}
274        self.application = app
275        self.mapper = mapper
276        self.global_conf = global_conf
277        self.kw = kw
278        self.fallback_template = """
279            <html>
280            <head>
281            <title>Error %(code)s</title>
282            </html>
283            <body>
284            <h1>Error %(code)s</h1>
285            <p>%(message)s</p>
286            <hr>
287            <p>
288                Additionally an error occurred trying to produce an
289                error document.  A description of the error was logged
290                to <tt>wsgi.errors</tt>.
291            </p>
292            </body>
293            </html>
294        """
295
296    def __call__(self, environ, start_response):
297        url = []
298        code_message = []
299        try:
300            def change_response(status, headers, exc_info=None):
301                new_url = None
302                parts = status.split(' ')
303                try:
304                    code = int(parts[0])
305                except (ValueError, TypeError):
306                    raise Exception(
307                        '_StatusBasedRedirect middleware '
308                        'received an invalid status code %s'%repr(parts[0])
309                    )
310                message = ' '.join(parts[1:])
311                new_url = self.mapper(
312                    code,
313                    message,
314                    environ,
315                    self.global_conf,
316                    self.kw
317                )
318                if not (new_url == None or isinstance(new_url, str)):
319                    raise TypeError(
320                        'Expected the url to internally '
321                        'redirect to in the _StatusBasedRedirect error_mapper'
322                        'to be a string or None, not %s'%repr(new_url)
323                    )
324                if new_url:
325                    url.append(new_url)
326                code_message.append([code, message])
327                return start_response(status, headers, exc_info)
328            app_iter = self.application(environ, change_response)
329        except:
330            try:
331                import sys
332                error = str(sys.exc_info()[1])
333            except:
334                error = ''
335            try:
336                code, message = code_message[0]
337            except:
338                code, message = ['', '']
339            environ['wsgi.errors'].write(
340                'Error occurred in _StatusBasedRedirect '
341                'intercepting the response: '+str(error)
342            )
343            return [self.fallback_template
344                    % {'message': message, 'code': code}]
345        else:
346            if url:
347                url_ = url[0]
348                new_environ = {}
349                for k, v in environ.items():
350                    if k != 'QUERY_STRING':
351                        new_environ['QUERY_STRING'] = urlparse.urlparse(url_)[4]
352                    else:
353                        new_environ[k] = v
354                class InvalidForward(Exception):
355                    pass
356                def eat_start_response(status, headers, exc_info=None):
357                    """
358                    We don't want start_response to do anything since it
359                    has already been called
360                    """
361                    if status[:3] != '200':
362                        raise InvalidForward(
363                            "The URL %s to internally forward "
364                            "to in order to create an error document did not "
365                            "return a '200' status code." % url_
366                        )
367                forward = environ['paste.recursive.forward']
368                old_start_response = forward.start_response
369                forward.start_response = eat_start_response
370                try:
371                    app_iter = forward(url_, new_environ)
372                except InvalidForward:
373                    code, message = code_message[0]
374                    environ['wsgi.errors'].write(
375                        'Error occurred in '
376                        '_StatusBasedRedirect redirecting '
377                        'to new URL: '+str(url[0])
378                    )
379                    return [
380                        self.fallback_template%{
381                            'message':message,
382                            'code':code,
383                        }
384                    ]
385                else:
386                    forward.start_response = old_start_response
387                    return app_iter
388            else:
389                return app_iter
390