File: alerts.py

package info (click to toggle)
python-django-debug-toolbar 1%3A5.2.0-1
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 1,984 kB
  • sloc: python: 6,880; javascript: 631; makefile: 62; sh: 16
file content (153 lines) | stat: -rw-r--r-- 5,169 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
from html.parser import HTMLParser

from django.utils.translation import gettext_lazy as _

from debug_toolbar.panels import Panel
from debug_toolbar.utils import is_processable_html_response


class FormParser(HTMLParser):
    """
    HTML form parser, used to check for invalid configurations of forms that
    take file inputs.
    """

    def __init__(self):
        super().__init__()
        self.in_form = False
        self.current_form = {}
        self.forms = []
        self.form_ids = []
        self.referenced_file_inputs = []

    def handle_starttag(self, tag, attrs):
        attrs = dict(attrs)
        if tag == "form":
            self.in_form = True
            form_id = attrs.get("id")
            if form_id:
                self.form_ids.append(form_id)
            self.current_form = {
                "file_form": False,
                "form_attrs": attrs,
                "submit_element_attrs": [],
            }
        elif (
            self.in_form
            and tag == "input"
            and attrs.get("type") == "file"
            and (not attrs.get("form") or attrs.get("form") == "")
        ):
            self.current_form["file_form"] = True
        elif (
            self.in_form
            and (
                (tag == "input" and attrs.get("type") in {"submit", "image"})
                or tag == "button"
            )
            and (not attrs.get("form") or attrs.get("form") == "")
        ):
            self.current_form["submit_element_attrs"].append(attrs)
        elif tag == "input" and attrs.get("form"):
            self.referenced_file_inputs.append(attrs)

    def handle_endtag(self, tag):
        if tag == "form" and self.in_form:
            self.forms.append(self.current_form)
            self.in_form = False


class AlertsPanel(Panel):
    """
    A panel to alert users to issues.
    """

    messages = {
        "form_id_missing_enctype": _(
            'Form with id "{form_id}" contains file input, but does not have the attribute enctype="multipart/form-data".'
        ),
        "form_missing_enctype": _(
            'Form contains file input, but does not have the attribute enctype="multipart/form-data".'
        ),
        "input_refs_form_missing_enctype": _(
            'Input element references form with id "{form_id}", but the form does not have the attribute enctype="multipart/form-data".'
        ),
    }

    title = _("Alerts")

    is_async = True

    template = "debug_toolbar/panels/alerts.html"

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.alerts = []

    @property
    def nav_subtitle(self):
        if alerts := self.get_stats().get("alerts"):
            alert_text = "alert" if len(alerts) == 1 else "alerts"
            return f"{len(alerts)} {alert_text}"
        else:
            return ""

    def add_alert(self, alert):
        self.alerts.append(alert)

    def check_invalid_file_form_configuration(self, html_content):
        """
        Inspects HTML content for a form that includes a file input but does
        not have the encoding type set to multipart/form-data, and warns the
        user if so.
        """
        parser = FormParser()
        parser.feed(html_content)

        # Check for file inputs directly inside a form that do not reference
        # any form through the form attribute
        for form in parser.forms:
            if (
                form["file_form"]
                and form["form_attrs"].get("enctype") != "multipart/form-data"
                and not any(
                    elem.get("formenctype") == "multipart/form-data"
                    for elem in form["submit_element_attrs"]
                )
            ):
                if form_id := form["form_attrs"].get("id"):
                    alert = self.messages["form_id_missing_enctype"].format(
                        form_id=form_id
                    )
                else:
                    alert = self.messages["form_missing_enctype"]
                self.add_alert({"alert": alert})

        # Check for file inputs that reference a form
        form_attrs_by_id = {
            form["form_attrs"].get("id"): form["form_attrs"] for form in parser.forms
        }

        for attrs in parser.referenced_file_inputs:
            form_id = attrs.get("form")
            if form_id and attrs.get("type") == "file":
                form_attrs = form_attrs_by_id.get(form_id)
                if form_attrs and form_attrs.get("enctype") != "multipart/form-data":
                    alert = self.messages["input_refs_form_missing_enctype"].format(
                        form_id=form_id
                    )
                    self.add_alert({"alert": alert})

        return self.alerts

    def generate_stats(self, request, response):
        if not is_processable_html_response(response):
            return

        html_content = response.content.decode(response.charset)
        self.check_invalid_file_form_configuration(html_content)

        # Further alert checks can go here

        # Write all alerts to record_stats
        self.record_stats({"alerts": self.alerts})