Skip to content

Commit afeee86

Browse files
authored
Check for for StreamingHttpResponse when generating stats in Alert (#1946)
* Check for for StreamingHttpResponse when generating stats in Alert panel.
1 parent c79f249 commit afeee86

File tree

3 files changed

+269
-249
lines changed

3 files changed

+269
-249
lines changed

debug_toolbar/panels/alerts.py

Lines changed: 152 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -1,148 +1,152 @@
1-
from html.parser import HTMLParser
2-
3-
from django.utils.translation import gettext_lazy as _
4-
5-
from debug_toolbar.panels import Panel
6-
7-
8-
class FormParser(HTMLParser):
9-
"""
10-
HTML form parser, used to check for invalid configurations of forms that
11-
take file inputs.
12-
"""
13-
14-
def __init__(self):
15-
super().__init__()
16-
self.in_form = False
17-
self.current_form = {}
18-
self.forms = []
19-
self.form_ids = []
20-
self.referenced_file_inputs = []
21-
22-
def handle_starttag(self, tag, attrs):
23-
attrs = dict(attrs)
24-
if tag == "form":
25-
self.in_form = True
26-
form_id = attrs.get("id")
27-
if form_id:
28-
self.form_ids.append(form_id)
29-
self.current_form = {
30-
"file_form": False,
31-
"form_attrs": attrs,
32-
"submit_element_attrs": [],
33-
}
34-
elif (
35-
self.in_form
36-
and tag == "input"
37-
and attrs.get("type") == "file"
38-
and (not attrs.get("form") or attrs.get("form") == "")
39-
):
40-
self.current_form["file_form"] = True
41-
elif (
42-
self.in_form
43-
and (
44-
(tag == "input" and attrs.get("type") in {"submit", "image"})
45-
or tag == "button"
46-
)
47-
and (not attrs.get("form") or attrs.get("form") == "")
48-
):
49-
self.current_form["submit_element_attrs"].append(attrs)
50-
elif tag == "input" and attrs.get("form"):
51-
self.referenced_file_inputs.append(attrs)
52-
53-
def handle_endtag(self, tag):
54-
if tag == "form" and self.in_form:
55-
self.forms.append(self.current_form)
56-
self.in_form = False
57-
58-
59-
class AlertsPanel(Panel):
60-
"""
61-
A panel to alert users to issues.
62-
"""
63-
64-
messages = {
65-
"form_id_missing_enctype": _(
66-
'Form with id "{form_id}" contains file input, but does not have the attribute enctype="multipart/form-data".'
67-
),
68-
"form_missing_enctype": _(
69-
'Form contains file input, but does not have the attribute enctype="multipart/form-data".'
70-
),
71-
"input_refs_form_missing_enctype": _(
72-
'Input element references form with id "{form_id}", but the form does not have the attribute enctype="multipart/form-data".'
73-
),
74-
}
75-
76-
title = _("Alerts")
77-
78-
template = "debug_toolbar/panels/alerts.html"
79-
80-
def __init__(self, *args, **kwargs):
81-
super().__init__(*args, **kwargs)
82-
self.alerts = []
83-
84-
@property
85-
def nav_subtitle(self):
86-
alerts = self.get_stats()["alerts"]
87-
if alerts:
88-
alert_text = "alert" if len(alerts) == 1 else "alerts"
89-
return f"{len(alerts)} {alert_text}"
90-
else:
91-
return ""
92-
93-
def add_alert(self, alert):
94-
self.alerts.append(alert)
95-
96-
def check_invalid_file_form_configuration(self, html_content):
97-
"""
98-
Inspects HTML content for a form that includes a file input but does
99-
not have the encoding type set to multipart/form-data, and warns the
100-
user if so.
101-
"""
102-
parser = FormParser()
103-
parser.feed(html_content)
104-
105-
# Check for file inputs directly inside a form that do not reference
106-
# any form through the form attribute
107-
for form in parser.forms:
108-
if (
109-
form["file_form"]
110-
and form["form_attrs"].get("enctype") != "multipart/form-data"
111-
and not any(
112-
elem.get("formenctype") == "multipart/form-data"
113-
for elem in form["submit_element_attrs"]
114-
)
115-
):
116-
if form_id := form["form_attrs"].get("id"):
117-
alert = self.messages["form_id_missing_enctype"].format(
118-
form_id=form_id
119-
)
120-
else:
121-
alert = self.messages["form_missing_enctype"]
122-
self.add_alert({"alert": alert})
123-
124-
# Check for file inputs that reference a form
125-
form_attrs_by_id = {
126-
form["form_attrs"].get("id"): form["form_attrs"] for form in parser.forms
127-
}
128-
129-
for attrs in parser.referenced_file_inputs:
130-
form_id = attrs.get("form")
131-
if form_id and attrs.get("type") == "file":
132-
form_attrs = form_attrs_by_id.get(form_id)
133-
if form_attrs and form_attrs.get("enctype") != "multipart/form-data":
134-
alert = self.messages["input_refs_form_missing_enctype"].format(
135-
form_id=form_id
136-
)
137-
self.add_alert({"alert": alert})
138-
139-
return self.alerts
140-
141-
def generate_stats(self, request, response):
142-
html_content = response.content.decode(response.charset)
143-
self.check_invalid_file_form_configuration(html_content)
144-
145-
# Further alert checks can go here
146-
147-
# Write all alerts to record_stats
148-
self.record_stats({"alerts": self.alerts})
1+
from html.parser import HTMLParser
2+
3+
from django.utils.translation import gettext_lazy as _
4+
5+
from debug_toolbar.panels import Panel
6+
7+
8+
class FormParser(HTMLParser):
9+
"""
10+
HTML form parser, used to check for invalid configurations of forms that
11+
take file inputs.
12+
"""
13+
14+
def __init__(self):
15+
super().__init__()
16+
self.in_form = False
17+
self.current_form = {}
18+
self.forms = []
19+
self.form_ids = []
20+
self.referenced_file_inputs = []
21+
22+
def handle_starttag(self, tag, attrs):
23+
attrs = dict(attrs)
24+
if tag == "form":
25+
self.in_form = True
26+
form_id = attrs.get("id")
27+
if form_id:
28+
self.form_ids.append(form_id)
29+
self.current_form = {
30+
"file_form": False,
31+
"form_attrs": attrs,
32+
"submit_element_attrs": [],
33+
}
34+
elif (
35+
self.in_form
36+
and tag == "input"
37+
and attrs.get("type") == "file"
38+
and (not attrs.get("form") or attrs.get("form") == "")
39+
):
40+
self.current_form["file_form"] = True
41+
elif (
42+
self.in_form
43+
and (
44+
(tag == "input" and attrs.get("type") in {"submit", "image"})
45+
or tag == "button"
46+
)
47+
and (not attrs.get("form") or attrs.get("form") == "")
48+
):
49+
self.current_form["submit_element_attrs"].append(attrs)
50+
elif tag == "input" and attrs.get("form"):
51+
self.referenced_file_inputs.append(attrs)
52+
53+
def handle_endtag(self, tag):
54+
if tag == "form" and self.in_form:
55+
self.forms.append(self.current_form)
56+
self.in_form = False
57+
58+
59+
class AlertsPanel(Panel):
60+
"""
61+
A panel to alert users to issues.
62+
"""
63+
64+
messages = {
65+
"form_id_missing_enctype": _(
66+
'Form with id "{form_id}" contains file input, but does not have the attribute enctype="multipart/form-data".'
67+
),
68+
"form_missing_enctype": _(
69+
'Form contains file input, but does not have the attribute enctype="multipart/form-data".'
70+
),
71+
"input_refs_form_missing_enctype": _(
72+
'Input element references form with id "{form_id}", but the form does not have the attribute enctype="multipart/form-data".'
73+
),
74+
}
75+
76+
title = _("Alerts")
77+
78+
template = "debug_toolbar/panels/alerts.html"
79+
80+
def __init__(self, *args, **kwargs):
81+
super().__init__(*args, **kwargs)
82+
self.alerts = []
83+
84+
@property
85+
def nav_subtitle(self):
86+
alerts = self.get_stats()["alerts"]
87+
if alerts:
88+
alert_text = "alert" if len(alerts) == 1 else "alerts"
89+
return f"{len(alerts)} {alert_text}"
90+
else:
91+
return ""
92+
93+
def add_alert(self, alert):
94+
self.alerts.append(alert)
95+
96+
def check_invalid_file_form_configuration(self, html_content):
97+
"""
98+
Inspects HTML content for a form that includes a file input but does
99+
not have the encoding type set to multipart/form-data, and warns the
100+
user if so.
101+
"""
102+
parser = FormParser()
103+
parser.feed(html_content)
104+
105+
# Check for file inputs directly inside a form that do not reference
106+
# any form through the form attribute
107+
for form in parser.forms:
108+
if (
109+
form["file_form"]
110+
and form["form_attrs"].get("enctype") != "multipart/form-data"
111+
and not any(
112+
elem.get("formenctype") == "multipart/form-data"
113+
for elem in form["submit_element_attrs"]
114+
)
115+
):
116+
if form_id := form["form_attrs"].get("id"):
117+
alert = self.messages["form_id_missing_enctype"].format(
118+
form_id=form_id
119+
)
120+
else:
121+
alert = self.messages["form_missing_enctype"]
122+
self.add_alert({"alert": alert})
123+
124+
# Check for file inputs that reference a form
125+
form_attrs_by_id = {
126+
form["form_attrs"].get("id"): form["form_attrs"] for form in parser.forms
127+
}
128+
129+
for attrs in parser.referenced_file_inputs:
130+
form_id = attrs.get("form")
131+
if form_id and attrs.get("type") == "file":
132+
form_attrs = form_attrs_by_id.get(form_id)
133+
if form_attrs and form_attrs.get("enctype") != "multipart/form-data":
134+
alert = self.messages["input_refs_form_missing_enctype"].format(
135+
form_id=form_id
136+
)
137+
self.add_alert({"alert": alert})
138+
139+
return self.alerts
140+
141+
def generate_stats(self, request, response):
142+
# check if streaming response
143+
if getattr(response, "streaming", True):
144+
return
145+
146+
html_content = response.content.decode(response.charset)
147+
self.check_invalid_file_form_configuration(html_content)
148+
149+
# Further alert checks can go here
150+
151+
# Write all alerts to record_stats
152+
self.record_stats({"alerts": self.alerts})

docs/changes.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ Change log
44
Pending
55
-------
66

7+
4.4.3 (2024-07-05)
8+
------------------
9+
10+
* Added check for StreamingHttpResponse in Alert Panel
11+
712
4.4.3 (2024-07-04)
813
------------------
914

0 commit comments

Comments
 (0)