1import codecs
2import inspect
3import os
4import re
5import sys
6import sysconfig
7import traceback
8import typing as t
9from html import escape
10from tokenize import TokenError
11from types import CodeType
12from types import TracebackType
13
14from .._internal import _to_str
15from ..filesystem import get_filesystem_encoding
16from ..utils import cached_property
17from .console import Console
18
19_coding_re = re.compile(br"coding[:=]\s*([-\w.]+)")
20_line_re = re.compile(br"^(.*?)$", re.MULTILINE)
21_funcdef_re = re.compile(r"^(\s*def\s)|(.*(?<!\w)lambda(:|\s))|^(\s*@)")
22
23HEADER = """\
24<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
25  "http://www.w3.org/TR/html4/loose.dtd">
26<html>
27  <head>
28    <title>%(title)s // Werkzeug Debugger</title>
29    <link rel="stylesheet" href="?__debugger__=yes&amp;cmd=resource&amp;f=style.css"
30        type="text/css">
31    <!-- We need to make sure this has a favicon so that the debugger does
32         not accidentally trigger a request to /favicon.ico which might
33         change the application's state. -->
34    <link rel="shortcut icon"
35        href="?__debugger__=yes&amp;cmd=resource&amp;f=console.png">
36    <script src="?__debugger__=yes&amp;cmd=resource&amp;f=debugger.js"></script>
37    <script type="text/javascript">
38      var TRACEBACK = %(traceback_id)d,
39          CONSOLE_MODE = %(console)s,
40          EVALEX = %(evalex)s,
41          EVALEX_TRUSTED = %(evalex_trusted)s,
42          SECRET = "%(secret)s";
43    </script>
44  </head>
45  <body style="background-color: #fff">
46    <div class="debugger">
47"""
48FOOTER = """\
49      <div class="footer">
50        Brought to you by <strong class="arthur">DON'T PANIC</strong>, your
51        friendly Werkzeug powered traceback interpreter.
52      </div>
53    </div>
54
55    <div class="pin-prompt">
56      <div class="inner">
57        <h3>Console Locked</h3>
58        <p>
59          The console is locked and needs to be unlocked by entering the PIN.
60          You can find the PIN printed out on the standard output of your
61          shell that runs the server.
62        <form>
63          <p>PIN:
64            <input type=text name=pin size=14>
65            <input type=submit name=btn value="Confirm Pin">
66        </form>
67      </div>
68    </div>
69  </body>
70</html>
71"""
72
73PAGE_HTML = (
74    HEADER
75    + """\
76<h1>%(exception_type)s</h1>
77<div class="detail">
78  <p class="errormsg">%(exception)s</p>
79</div>
80<h2 class="traceback">Traceback <em>(most recent call last)</em></h2>
81%(summary)s
82<div class="plain">
83    <p>
84      This is the Copy/Paste friendly version of the traceback.
85    </p>
86    <textarea cols="50" rows="10" name="code" readonly>%(plaintext)s</textarea>
87</div>
88<div class="explanation">
89  The debugger caught an exception in your WSGI application.  You can now
90  look at the traceback which led to the error.  <span class="nojavascript">
91  If you enable JavaScript you can also use additional features such as code
92  execution (if the evalex feature is enabled), automatic pasting of the
93  exceptions and much more.</span>
94</div>
95"""
96    + FOOTER
97    + """
98<!--
99
100%(plaintext_cs)s
101
102-->
103"""
104)
105
106CONSOLE_HTML = (
107    HEADER
108    + """\
109<h1>Interactive Console</h1>
110<div class="explanation">
111In this console you can execute Python expressions in the context of the
112application.  The initial namespace was created by the debugger automatically.
113</div>
114<div class="console"><div class="inner">The Console requires JavaScript.</div></div>
115"""
116    + FOOTER
117)
118
119SUMMARY_HTML = """\
120<div class="%(classes)s">
121  %(title)s
122  <ul>%(frames)s</ul>
123  %(description)s
124</div>
125"""
126
127FRAME_HTML = """\
128<div class="frame" id="frame-%(id)d">
129  <h4>File <cite class="filename">"%(filename)s"</cite>,
130      line <em class="line">%(lineno)s</em>,
131      in <code class="function">%(function_name)s</code></h4>
132  <div class="source %(library)s">%(lines)s</div>
133</div>
134"""
135
136SOURCE_LINE_HTML = """\
137<tr class="%(classes)s">
138  <td class=lineno>%(lineno)s</td>
139  <td>%(code)s</td>
140</tr>
141"""
142
143
144def render_console_html(secret: str, evalex_trusted: bool = True) -> str:
145    return CONSOLE_HTML % {
146        "evalex": "true",
147        "evalex_trusted": "true" if evalex_trusted else "false",
148        "console": "true",
149        "title": "Console",
150        "secret": secret,
151        "traceback_id": -1,
152    }
153
154
155def get_current_traceback(
156    ignore_system_exceptions: bool = False,
157    show_hidden_frames: bool = False,
158    skip: int = 0,
159) -> "Traceback":
160    """Get the current exception info as `Traceback` object.  Per default
161    calling this method will reraise system exceptions such as generator exit,
162    system exit or others.  This behavior can be disabled by passing `False`
163    to the function as first parameter.
164    """
165    info = t.cast(
166        t.Tuple[t.Type[BaseException], BaseException, TracebackType], sys.exc_info()
167    )
168    exc_type, exc_value, tb = info
169
170    if ignore_system_exceptions and exc_type in {
171        SystemExit,
172        KeyboardInterrupt,
173        GeneratorExit,
174    }:
175        raise
176    for _ in range(skip):
177        if tb.tb_next is None:
178            break
179        tb = tb.tb_next
180    tb = Traceback(exc_type, exc_value, tb)
181    if not show_hidden_frames:
182        tb.filter_hidden_frames()
183    return tb
184
185
186class Line:
187    """Helper for the source renderer."""
188
189    __slots__ = ("lineno", "code", "in_frame", "current")
190
191    def __init__(self, lineno: int, code: str) -> None:
192        self.lineno = lineno
193        self.code = code
194        self.in_frame = False
195        self.current = False
196
197    @property
198    def classes(self) -> t.List[str]:
199        rv = ["line"]
200        if self.in_frame:
201            rv.append("in-frame")
202        if self.current:
203            rv.append("current")
204        return rv
205
206    def render(self) -> str:
207        return SOURCE_LINE_HTML % {
208            "classes": " ".join(self.classes),
209            "lineno": self.lineno,
210            "code": escape(self.code),
211        }
212
213
214class Traceback:
215    """Wraps a traceback."""
216
217    def __init__(
218        self,
219        exc_type: t.Type[BaseException],
220        exc_value: BaseException,
221        tb: TracebackType,
222    ) -> None:
223        self.exc_type = exc_type
224        self.exc_value = exc_value
225        self.tb = tb
226
227        exception_type = exc_type.__name__
228        if exc_type.__module__ not in {"builtins", "__builtin__", "exceptions"}:
229            exception_type = f"{exc_type.__module__}.{exception_type}"
230        self.exception_type = exception_type
231
232        self.groups = []
233        memo = set()
234        while True:
235            self.groups.append(Group(exc_type, exc_value, tb))
236            memo.add(id(exc_value))
237            exc_value = exc_value.__cause__ or exc_value.__context__  # type: ignore
238            if exc_value is None or id(exc_value) in memo:
239                break
240            exc_type = type(exc_value)
241            tb = exc_value.__traceback__  # type: ignore
242        self.groups.reverse()
243        self.frames = [frame for group in self.groups for frame in group.frames]
244
245    def filter_hidden_frames(self) -> None:
246        """Remove the frames according to the paste spec."""
247        for group in self.groups:
248            group.filter_hidden_frames()
249
250        self.frames[:] = [frame for group in self.groups for frame in group.frames]
251
252    @property
253    def is_syntax_error(self) -> bool:
254        """Is it a syntax error?"""
255        return isinstance(self.exc_value, SyntaxError)
256
257    @property
258    def exception(self) -> str:
259        """String representation of the final exception."""
260        return self.groups[-1].exception
261
262    def log(self, logfile: t.Optional[t.IO[str]] = None) -> None:
263        """Log the ASCII traceback into a file object."""
264        if logfile is None:
265            logfile = sys.stderr
266        tb = f"{self.plaintext.rstrip()}\n"
267        logfile.write(tb)
268
269    def render_summary(self, include_title: bool = True) -> str:
270        """Render the traceback for the interactive console."""
271        title = ""
272        classes = ["traceback"]
273        if not self.frames:
274            classes.append("noframe-traceback")
275            frames = []
276        else:
277            library_frames = sum(frame.is_library for frame in self.frames)
278            mark_lib = 0 < library_frames < len(self.frames)
279            frames = [group.render(mark_lib=mark_lib) for group in self.groups]
280
281        if include_title:
282            if self.is_syntax_error:
283                title = "Syntax Error"
284            else:
285                title = "Traceback <em>(most recent call last)</em>:"
286
287        if self.is_syntax_error:
288            description = f"<pre class=syntaxerror>{escape(self.exception)}</pre>"
289        else:
290            description = f"<blockquote>{escape(self.exception)}</blockquote>"
291
292        return SUMMARY_HTML % {
293            "classes": " ".join(classes),
294            "title": f"<h3>{title if title else ''}</h3>",
295            "frames": "\n".join(frames),
296            "description": description,
297        }
298
299    def render_full(
300        self,
301        evalex: bool = False,
302        secret: t.Optional[str] = None,
303        evalex_trusted: bool = True,
304    ) -> str:
305        """Render the Full HTML page with the traceback info."""
306        exc = escape(self.exception)
307        return PAGE_HTML % {
308            "evalex": "true" if evalex else "false",
309            "evalex_trusted": "true" if evalex_trusted else "false",
310            "console": "false",
311            "title": exc,
312            "exception": exc,
313            "exception_type": escape(self.exception_type),
314            "summary": self.render_summary(include_title=False),
315            "plaintext": escape(self.plaintext),
316            "plaintext_cs": re.sub("-{2,}", "-", self.plaintext),
317            "traceback_id": self.id,
318            "secret": secret,
319        }
320
321    @cached_property
322    def plaintext(self) -> str:
323        return "\n".join([group.render_text() for group in self.groups])
324
325    @property
326    def id(self) -> int:
327        return id(self)
328
329
330class Group:
331    """A group of frames for an exception in a traceback. If the
332    exception has a ``__cause__`` or ``__context__``, there are multiple
333    exception groups.
334    """
335
336    def __init__(
337        self,
338        exc_type: t.Type[BaseException],
339        exc_value: BaseException,
340        tb: TracebackType,
341    ) -> None:
342        self.exc_type = exc_type
343        self.exc_value = exc_value
344        self.info = None
345        if exc_value.__cause__ is not None:
346            self.info = (
347                "The above exception was the direct cause of the following exception"
348            )
349        elif exc_value.__context__ is not None:
350            self.info = (
351                "During handling of the above exception, another exception occurred"
352            )
353
354        self.frames = []
355        while tb is not None:
356            self.frames.append(Frame(exc_type, exc_value, tb))
357            tb = tb.tb_next  # type: ignore
358
359    def filter_hidden_frames(self) -> None:
360        # An exception may not have a traceback to filter frames, such
361        # as one re-raised from ProcessPoolExecutor.
362        if not self.frames:
363            return
364
365        new_frames: t.List[Frame] = []
366        hidden = False
367
368        for frame in self.frames:
369            hide = frame.hide
370            if hide in ("before", "before_and_this"):
371                new_frames = []
372                hidden = False
373                if hide == "before_and_this":
374                    continue
375            elif hide in ("reset", "reset_and_this"):
376                hidden = False
377                if hide == "reset_and_this":
378                    continue
379            elif hide in ("after", "after_and_this"):
380                hidden = True
381                if hide == "after_and_this":
382                    continue
383            elif hide or hidden:
384                continue
385            new_frames.append(frame)
386
387        # if we only have one frame and that frame is from the codeop
388        # module, remove it.
389        if len(new_frames) == 1 and self.frames[0].module == "codeop":
390            del self.frames[:]
391
392        # if the last frame is missing something went terrible wrong :(
393        elif self.frames[-1] in new_frames:
394            self.frames[:] = new_frames
395
396    @property
397    def exception(self) -> str:
398        """String representation of the exception."""
399        buf = traceback.format_exception_only(self.exc_type, self.exc_value)
400        rv = "".join(buf).strip()
401        return _to_str(rv, "utf-8", "replace")
402
403    def render(self, mark_lib: bool = True) -> str:
404        out = []
405        if self.info is not None:
406            out.append(f'<li><div class="exc-divider">{self.info}:</div>')
407        for frame in self.frames:
408            title = f' title="{escape(frame.info)}"' if frame.info else ""
409            out.append(f"<li{title}>{frame.render(mark_lib=mark_lib)}")
410        return "\n".join(out)
411
412    def render_text(self) -> str:
413        out = []
414        if self.info is not None:
415            out.append(f"\n{self.info}:\n")
416        out.append("Traceback (most recent call last):")
417        for frame in self.frames:
418            out.append(frame.render_text())
419        out.append(self.exception)
420        return "\n".join(out)
421
422
423class Frame:
424    """A single frame in a traceback."""
425
426    def __init__(
427        self,
428        exc_type: t.Type[BaseException],
429        exc_value: BaseException,
430        tb: TracebackType,
431    ) -> None:
432        self.lineno = tb.tb_lineno
433        self.function_name = tb.tb_frame.f_code.co_name
434        self.locals = tb.tb_frame.f_locals
435        self.globals = tb.tb_frame.f_globals
436
437        fn = inspect.getsourcefile(tb) or inspect.getfile(tb)
438        if fn[-4:] in (".pyo", ".pyc"):
439            fn = fn[:-1]
440        # if it's a file on the file system resolve the real filename.
441        if os.path.isfile(fn):
442            fn = os.path.realpath(fn)
443        self.filename = _to_str(fn, get_filesystem_encoding())
444        self.module = self.globals.get("__name__", self.locals.get("__name__"))
445        self.loader = self.globals.get("__loader__", self.locals.get("__loader__"))
446        self.code = tb.tb_frame.f_code
447
448        # support for paste's traceback extensions
449        self.hide = self.locals.get("__traceback_hide__", False)
450        info = self.locals.get("__traceback_info__")
451        if info is not None:
452            info = _to_str(info, "utf-8", "replace")
453        self.info = info
454
455    def render(self, mark_lib: bool = True) -> str:
456        """Render a single frame in a traceback."""
457        return FRAME_HTML % {
458            "id": self.id,
459            "filename": escape(self.filename),
460            "lineno": self.lineno,
461            "function_name": escape(self.function_name),
462            "lines": self.render_line_context(),
463            "library": "library" if mark_lib and self.is_library else "",
464        }
465
466    @cached_property
467    def is_library(self) -> bool:
468        return any(
469            self.filename.startswith(os.path.realpath(path))
470            for path in sysconfig.get_paths().values()
471        )
472
473    def render_text(self) -> str:
474        return (
475            f'  File "{self.filename}", line {self.lineno}, in {self.function_name}\n'
476            f"    {self.current_line.strip()}"
477        )
478
479    def render_line_context(self) -> str:
480        before, current, after = self.get_context_lines()
481        rv = []
482
483        def render_line(line: str, cls: str) -> None:
484            line = line.expandtabs().rstrip()
485            stripped_line = line.strip()
486            prefix = len(line) - len(stripped_line)
487            rv.append(
488                f'<pre class="line {cls}"><span class="ws">{" " * prefix}</span>'
489                f"{escape(stripped_line) if stripped_line else ' '}</pre>"
490            )
491
492        for line in before:
493            render_line(line, "before")
494        render_line(current, "current")
495        for line in after:
496            render_line(line, "after")
497
498        return "\n".join(rv)
499
500    def get_annotated_lines(self) -> t.List[Line]:
501        """Helper function that returns lines with extra information."""
502        lines = [Line(idx + 1, x) for idx, x in enumerate(self.sourcelines)]
503
504        # find function definition and mark lines
505        if hasattr(self.code, "co_firstlineno"):
506            lineno = self.code.co_firstlineno - 1
507            while lineno > 0:
508                if _funcdef_re.match(lines[lineno].code):
509                    break
510                lineno -= 1
511            try:
512                offset = len(inspect.getblock([f"{x.code}\n" for x in lines[lineno:]]))
513            except TokenError:
514                offset = 0
515            for line in lines[lineno : lineno + offset]:
516                line.in_frame = True
517
518        # mark current line
519        try:
520            lines[self.lineno - 1].current = True
521        except IndexError:
522            pass
523
524        return lines
525
526    def eval(self, code: t.Union[str, CodeType], mode: str = "single") -> t.Any:
527        """Evaluate code in the context of the frame."""
528        if isinstance(code, str):
529            code = compile(code, "<interactive>", mode)
530        return eval(code, self.globals, self.locals)
531
532    @cached_property
533    def sourcelines(self) -> t.List[str]:
534        """The sourcecode of the file as list of strings."""
535        # get sourcecode from loader or file
536        source = None
537        if self.loader is not None:
538            try:
539                if hasattr(self.loader, "get_source"):
540                    source = self.loader.get_source(self.module)
541                elif hasattr(self.loader, "get_source_by_code"):
542                    source = self.loader.get_source_by_code(self.code)
543            except Exception:
544                # we munch the exception so that we don't cause troubles
545                # if the loader is broken.
546                pass
547
548        if source is None:
549            try:
550                with open(self.filename, mode="rb") as f:
551                    source = f.read()
552            except OSError:
553                return []
554
555        # already str?  return right away
556        if isinstance(source, str):
557            return source.splitlines()
558
559        charset = "utf-8"
560        if source.startswith(codecs.BOM_UTF8):
561            source = source[3:]
562        else:
563            for idx, match in enumerate(_line_re.finditer(source)):
564                coding_match = _coding_re.search(match.group())
565                if coding_match is not None:
566                    charset = coding_match.group(1).decode("utf-8")
567                    break
568                if idx > 1:
569                    break
570
571        # on broken cookies we fall back to utf-8 too
572        charset = _to_str(charset)
573        try:
574            codecs.lookup(charset)
575        except LookupError:
576            charset = "utf-8"
577
578        return source.decode(charset, "replace").splitlines()
579
580    def get_context_lines(
581        self, context: int = 5
582    ) -> t.Tuple[t.List[str], str, t.List[str]]:
583        before = self.sourcelines[self.lineno - context - 1 : self.lineno - 1]
584        past = self.sourcelines[self.lineno : self.lineno + context]
585        return (before, self.current_line, past)
586
587    @property
588    def current_line(self) -> str:
589        try:
590            return self.sourcelines[self.lineno - 1]
591        except IndexError:
592            return ""
593
594    @cached_property
595    def console(self) -> Console:
596        return Console(self.globals, self.locals)
597
598    @property
599    def id(self) -> int:
600        return id(self)
601