1# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
2# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
3"""
4Middleware that displays everything that is printed inline in
5application pages.
6
7Anything printed during the request will get captured and included on
8the page.  It will usually be included as a floating element in the
9top right hand corner of the page.  If you want to override this
10you can include a tag in your template where it will be placed::
11
12  <pre id="paste-debug-prints"></pre>
13
14You might want to include ``style="white-space: normal"``, as all the
15whitespace will be quoted, and this allows the text to wrap if
16necessary.
17
18"""
19
20from cStringIO import StringIO
21import re
22import cgi
23from paste.util import threadedprint
24from paste import wsgilib
25from paste import response
26import six
27import sys
28
29_threadedprint_installed = False
30
31__all__ = ['PrintDebugMiddleware']
32
33class TeeFile(object):
34
35    def __init__(self, files):
36        self.files = files
37
38    def write(self, v):
39        if isinstance(v, unicode):
40            # WSGI is picky in this case
41            v = str(v)
42        for file in self.files:
43            file.write(v)
44
45class PrintDebugMiddleware(object):
46
47    """
48    This middleware captures all the printed statements, and inlines
49    them in HTML pages, so that you can see all the (debug-intended)
50    print statements in the page itself.
51
52    There are two keys added to the environment to control this:
53    ``environ['paste.printdebug_listeners']`` is a list of functions
54    that will be called everytime something is printed.
55
56    ``environ['paste.remove_printdebug']`` is a function that, if
57    called, will disable printing of output for that request.
58
59    If you have ``replace_stdout=True`` then stdout is replaced, not
60    captured.
61    """
62
63    log_template = (
64        '<pre style="width: 40%%; border: 2px solid #000; white-space: normal; '
65        'background-color: #ffd; color: #000; float: right;">'
66        '<b style="border-bottom: 1px solid #000">Log messages</b><br>'
67        '%s</pre>')
68
69    def __init__(self, app, global_conf=None, force_content_type=False,
70                 print_wsgi_errors=True, replace_stdout=False):
71        # @@: global_conf should be handled separately and only for
72        # the entry point
73        self.app = app
74        self.force_content_type = force_content_type
75        if isinstance(print_wsgi_errors, six.string_types):
76            from paste.deploy.converters import asbool
77            print_wsgi_errors = asbool(print_wsgi_errors)
78        self.print_wsgi_errors = print_wsgi_errors
79        self.replace_stdout = replace_stdout
80        self._threaded_print_stdout = None
81
82    def __call__(self, environ, start_response):
83        global _threadedprint_installed
84        if environ.get('paste.testing'):
85            # In a testing environment this interception isn't
86            # useful:
87            return self.app(environ, start_response)
88        if (not _threadedprint_installed
89            or self._threaded_print_stdout is not sys.stdout):
90            # @@: Not strictly threadsafe
91            _threadedprint_installed = True
92            threadedprint.install(leave_stdout=not self.replace_stdout)
93            self._threaded_print_stdout = sys.stdout
94        removed = []
95        def remove_printdebug():
96            removed.append(None)
97        environ['paste.remove_printdebug'] = remove_printdebug
98        logged = StringIO()
99        listeners = [logged]
100        environ['paste.printdebug_listeners'] = listeners
101        if self.print_wsgi_errors:
102            listeners.append(environ['wsgi.errors'])
103        replacement_stdout = TeeFile(listeners)
104        threadedprint.register(replacement_stdout)
105        try:
106            status, headers, body = wsgilib.intercept_output(
107                environ, self.app)
108            if status is None:
109                # Some error occurred
110                status = '500 Server Error'
111                headers = [('Content-type', 'text/html')]
112                start_response(status, headers)
113                if not body:
114                    body = 'An error occurred'
115            content_type = response.header_value(headers, 'content-type')
116            if (removed or
117                (not self.force_content_type and
118                 (not content_type
119                  or not content_type.startswith('text/html')))):
120                if replacement_stdout == logged:
121                    # Then the prints will be lost, unless...
122                    environ['wsgi.errors'].write(logged.getvalue())
123                start_response(status, headers)
124                return [body]
125            response.remove_header(headers, 'content-length')
126            body = self.add_log(body, logged.getvalue())
127            start_response(status, headers)
128            return [body]
129        finally:
130            threadedprint.deregister()
131
132    _body_re = re.compile(r'<body[^>]*>', re.I)
133    _explicit_re = re.compile(r'<pre\s*[^>]*id="paste-debug-prints".*?>',
134                              re.I+re.S)
135
136    def add_log(self, html, log):
137        if not log:
138            return html
139        text = cgi.escape(log)
140        text = text.replace('\n', '<br>')
141        text = text.replace('  ', '&nbsp; ')
142        match = self._explicit_re.search(html)
143        if not match:
144            text = self.log_template % text
145            match = self._body_re.search(html)
146        if not match:
147            return text + html
148        else:
149            return html[:match.end()] + text + html[match.end():]
150