1# -*- coding: utf-8 -*-
2"""
3    jinja2.debug
4    ~~~~~~~~~~~~
5
6    Implements the debug interface for Jinja.  This module does some pretty
7    ugly stuff with the Python traceback system in order to achieve tracebacks
8    with correct line numbers, locals and contents.
9
10    :copyright: (c) 2017 by the Jinja Team.
11    :license: BSD, see LICENSE for more details.
12"""
13import sys
14import traceback
15from types import TracebackType, CodeType
16from jinja2.utils import missing, internal_code
17from jinja2.exceptions import TemplateSyntaxError
18from jinja2._compat import iteritems, reraise, PY2
19
20# on pypy we can take advantage of transparent proxies
21try:
22    from __pypy__ import tproxy
23except ImportError:
24    tproxy = None
25
26
27# how does the raise helper look like?
28try:
29    exec("raise TypeError, 'foo'")
30except SyntaxError:
31    raise_helper = 'raise __jinja_exception__[1]'
32except TypeError:
33    raise_helper = 'raise __jinja_exception__[0], __jinja_exception__[1]'
34
35
36class TracebackFrameProxy(object):
37    """Proxies a traceback frame."""
38
39    def __init__(self, tb):
40        self.tb = tb
41        self._tb_next = None
42
43    @property
44    def tb_next(self):
45        return self._tb_next
46
47    def set_next(self, next):
48        if tb_set_next is not None:
49            try:
50                tb_set_next(self.tb, next and next.tb or None)
51            except Exception:
52                # this function can fail due to all the hackery it does
53                # on various python implementations.  We just catch errors
54                # down and ignore them if necessary.
55                pass
56        self._tb_next = next
57
58    @property
59    def is_jinja_frame(self):
60        return '__jinja_template__' in self.tb.tb_frame.f_globals
61
62    def __getattr__(self, name):
63        return getattr(self.tb, name)
64
65
66def make_frame_proxy(frame):
67    proxy = TracebackFrameProxy(frame)
68    if tproxy is None:
69        return proxy
70    def operation_handler(operation, *args, **kwargs):
71        if operation in ('__getattribute__', '__getattr__'):
72            return getattr(proxy, args[0])
73        elif operation == '__setattr__':
74            proxy.__setattr__(*args, **kwargs)
75        else:
76            return getattr(proxy, operation)(*args, **kwargs)
77    return tproxy(TracebackType, operation_handler)
78
79
80class ProcessedTraceback(object):
81    """Holds a Jinja preprocessed traceback for printing or reraising."""
82
83    def __init__(self, exc_type, exc_value, frames):
84        assert frames, 'no frames for this traceback?'
85        self.exc_type = exc_type
86        self.exc_value = exc_value
87        self.frames = frames
88
89        # newly concatenate the frames (which are proxies)
90        prev_tb = None
91        for tb in self.frames:
92            if prev_tb is not None:
93                prev_tb.set_next(tb)
94            prev_tb = tb
95        prev_tb.set_next(None)
96
97    def render_as_text(self, limit=None):
98        """Return a string with the traceback."""
99        lines = traceback.format_exception(self.exc_type, self.exc_value,
100                                           self.frames[0], limit=limit)
101        return ''.join(lines).rstrip()
102
103    def render_as_html(self, full=False):
104        """Return a unicode string with the traceback as rendered HTML."""
105        from jinja2.debugrenderer import render_traceback
106        return u'%s\n\n<!--\n%s\n-->' % (
107            render_traceback(self, full=full),
108            self.render_as_text().decode('utf-8', 'replace')
109        )
110
111    @property
112    def is_template_syntax_error(self):
113        """`True` if this is a template syntax error."""
114        return isinstance(self.exc_value, TemplateSyntaxError)
115
116    @property
117    def exc_info(self):
118        """Exception info tuple with a proxy around the frame objects."""
119        return self.exc_type, self.exc_value, self.frames[0]
120
121    @property
122    def standard_exc_info(self):
123        """Standard python exc_info for re-raising"""
124        tb = self.frames[0]
125        # the frame will be an actual traceback (or transparent proxy) if
126        # we are on pypy or a python implementation with support for tproxy
127        if type(tb) is not TracebackType:
128            tb = tb.tb
129        return self.exc_type, self.exc_value, tb
130
131
132def make_traceback(exc_info, source_hint=None):
133    """Creates a processed traceback object from the exc_info."""
134    exc_type, exc_value, tb = exc_info
135    if isinstance(exc_value, TemplateSyntaxError):
136        exc_info = translate_syntax_error(exc_value, source_hint)
137        initial_skip = 0
138    else:
139        initial_skip = 1
140    return translate_exception(exc_info, initial_skip)
141
142
143def translate_syntax_error(error, source=None):
144    """Rewrites a syntax error to please traceback systems."""
145    error.source = source
146    error.translated = True
147    exc_info = (error.__class__, error, None)
148    filename = error.filename
149    if filename is None:
150        filename = '<unknown>'
151    return fake_exc_info(exc_info, filename, error.lineno)
152
153
154def translate_exception(exc_info, initial_skip=0):
155    """If passed an exc_info it will automatically rewrite the exceptions
156    all the way down to the correct line numbers and frames.
157    """
158    tb = exc_info[2]
159    frames = []
160
161    # skip some internal frames if wanted
162    for x in range(initial_skip):
163        if tb is not None:
164            tb = tb.tb_next
165    initial_tb = tb
166
167    while tb is not None:
168        # skip frames decorated with @internalcode.  These are internal
169        # calls we can't avoid and that are useless in template debugging
170        # output.
171        if tb.tb_frame.f_code in internal_code:
172            tb = tb.tb_next
173            continue
174
175        # save a reference to the next frame if we override the current
176        # one with a faked one.
177        next = tb.tb_next
178
179        # fake template exceptions
180        template = tb.tb_frame.f_globals.get('__jinja_template__')
181        if template is not None:
182            lineno = template.get_corresponding_lineno(tb.tb_lineno)
183            tb = fake_exc_info(exc_info[:2] + (tb,), template.filename,
184                               lineno)[2]
185
186        frames.append(make_frame_proxy(tb))
187        tb = next
188
189    # if we don't have any exceptions in the frames left, we have to
190    # reraise it unchanged.
191    # XXX: can we backup here?  when could this happen?
192    if not frames:
193        reraise(exc_info[0], exc_info[1], exc_info[2])
194
195    return ProcessedTraceback(exc_info[0], exc_info[1], frames)
196
197
198def get_jinja_locals(real_locals):
199    ctx = real_locals.get('context')
200    if ctx:
201        locals = ctx.get_all()
202    else:
203        locals = {}
204
205    local_overrides = {}
206
207    for name, value in iteritems(real_locals):
208        if not name.startswith('l_') or value is missing:
209            continue
210        try:
211            _, depth, name = name.split('_', 2)
212            depth = int(depth)
213        except ValueError:
214            continue
215        cur_depth = local_overrides.get(name, (-1,))[0]
216        if cur_depth < depth:
217            local_overrides[name] = (depth, value)
218
219    for name, (_, value) in iteritems(local_overrides):
220        if value is missing:
221            locals.pop(name, None)
222        else:
223            locals[name] = value
224
225    return locals
226
227
228def fake_exc_info(exc_info, filename, lineno):
229    """Helper for `translate_exception`."""
230    exc_type, exc_value, tb = exc_info
231
232    # figure the real context out
233    if tb is not None:
234        locals = get_jinja_locals(tb.tb_frame.f_locals)
235
236        # if there is a local called __jinja_exception__, we get
237        # rid of it to not break the debug functionality.
238        locals.pop('__jinja_exception__', None)
239    else:
240        locals = {}
241
242    # assamble fake globals we need
243    globals = {
244        '__name__':             filename,
245        '__file__':             filename,
246        '__jinja_exception__':  exc_info[:2],
247
248        # we don't want to keep the reference to the template around
249        # to not cause circular dependencies, but we mark it as Jinja
250        # frame for the ProcessedTraceback
251        '__jinja_template__':   None
252    }
253
254    # and fake the exception
255    code = compile('\n' * (lineno - 1) + raise_helper, filename, 'exec')
256
257    # if it's possible, change the name of the code.  This won't work
258    # on some python environments such as google appengine
259    try:
260        if tb is None:
261            location = 'template'
262        else:
263            function = tb.tb_frame.f_code.co_name
264            if function == 'root':
265                location = 'top-level template code'
266            elif function.startswith('block_'):
267                location = 'block "%s"' % function[6:]
268            else:
269                location = 'template'
270
271        if PY2:
272            code = CodeType(0, code.co_nlocals, code.co_stacksize,
273                            code.co_flags, code.co_code, code.co_consts,
274                            code.co_names, code.co_varnames, filename,
275                            location, code.co_firstlineno,
276                            code.co_lnotab, (), ())
277        else:
278            code = CodeType(0, code.co_kwonlyargcount,
279                            code.co_nlocals, code.co_stacksize,
280                            code.co_flags, code.co_code, code.co_consts,
281                            code.co_names, code.co_varnames, filename,
282                            location, code.co_firstlineno,
283                            code.co_lnotab, (), ())
284    except Exception as e:
285        pass
286
287    # execute the code and catch the new traceback
288    try:
289        exec(code, globals, locals)
290    except:
291        exc_info = sys.exc_info()
292        new_tb = exc_info[2].tb_next
293
294    # return without this frame
295    return exc_info[:2] + (new_tb,)
296
297
298def _init_ugly_crap():
299    """This function implements a few ugly things so that we can patch the
300    traceback objects.  The function returned allows resetting `tb_next` on
301    any python traceback object.  Do not attempt to use this on non cpython
302    interpreters
303    """
304    import ctypes
305    from types import TracebackType
306
307    if PY2:
308        # figure out size of _Py_ssize_t for Python 2:
309        if hasattr(ctypes.pythonapi, 'Py_InitModule4_64'):
310            _Py_ssize_t = ctypes.c_int64
311        else:
312            _Py_ssize_t = ctypes.c_int
313    else:
314        # platform ssize_t on Python 3
315        _Py_ssize_t = ctypes.c_ssize_t
316
317    # regular python
318    class _PyObject(ctypes.Structure):
319        pass
320    _PyObject._fields_ = [
321        ('ob_refcnt', _Py_ssize_t),
322        ('ob_type', ctypes.POINTER(_PyObject))
323    ]
324
325    # python with trace
326    if hasattr(sys, 'getobjects'):
327        class _PyObject(ctypes.Structure):
328            pass
329        _PyObject._fields_ = [
330            ('_ob_next', ctypes.POINTER(_PyObject)),
331            ('_ob_prev', ctypes.POINTER(_PyObject)),
332            ('ob_refcnt', _Py_ssize_t),
333            ('ob_type', ctypes.POINTER(_PyObject))
334        ]
335
336    class _Traceback(_PyObject):
337        pass
338    _Traceback._fields_ = [
339        ('tb_next', ctypes.POINTER(_Traceback)),
340        ('tb_frame', ctypes.POINTER(_PyObject)),
341        ('tb_lasti', ctypes.c_int),
342        ('tb_lineno', ctypes.c_int)
343    ]
344
345    def tb_set_next(tb, next):
346        """Set the tb_next attribute of a traceback object."""
347        if not (isinstance(tb, TracebackType) and
348                (next is None or isinstance(next, TracebackType))):
349            raise TypeError('tb_set_next arguments must be traceback objects')
350        obj = _Traceback.from_address(id(tb))
351        if tb.tb_next is not None:
352            old = _Traceback.from_address(id(tb.tb_next))
353            old.ob_refcnt -= 1
354        if next is None:
355            obj.tb_next = ctypes.POINTER(_Traceback)()
356        else:
357            next = _Traceback.from_address(id(next))
358            next.ob_refcnt += 1
359            obj.tb_next = ctypes.pointer(next)
360
361    return tb_set_next
362
363
364# try to get a tb_set_next implementation if we don't have transparent
365# proxies.
366tb_set_next = None
367if tproxy is None:
368    try:
369        tb_set_next = _init_ugly_crap()
370    except:
371        pass
372    del _init_ugly_crap
373