Skip to content

Commit 97fa2e5

Browse files
committed
Fixed issue #1682 -- alert user when using file field without proper encoding
1 parent 9c4eb67 commit 97fa2e5

File tree

8 files changed

+178
-0
lines changed

8 files changed

+178
-0
lines changed

debug_toolbar/panels/alerts.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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+
20+
def handle_starttag(self, tag, attrs):
21+
attrs = dict(attrs)
22+
if tag == "form":
23+
self.in_form = True
24+
self.current_form = {
25+
"file_form": False,
26+
"form_attrs": attrs,
27+
"submit_element_attrs": [],
28+
}
29+
elif self.in_form and tag == "input" and attrs.get("type") == "file":
30+
self.current_form["file_form"] = True
31+
elif self.in_form and (
32+
(tag == "input" and attrs.get("type") in {"submit", "image"})
33+
or tag == "button"
34+
):
35+
self.current_form["submit_element_attrs"].append(attrs)
36+
37+
def handle_endtag(self, tag):
38+
if tag == "form" and self.in_form:
39+
self.forms.append(self.current_form)
40+
self.in_form = False
41+
42+
43+
class AlertsPanel(Panel):
44+
"""
45+
A panel to alert users to issues.
46+
"""
47+
48+
title = _("Alerts")
49+
50+
template = "debug_toolbar/panels/alerts.html"
51+
52+
def __init__(self, *args, **kwargs):
53+
super().__init__(*args, **kwargs)
54+
self.issues = []
55+
56+
@property
57+
def nav_subtitle(self):
58+
if self.issues:
59+
issue_text = "issue" if len(self.issues) == 1 else "issues"
60+
return f"{len(self.issues)} {issue_text} found"
61+
else:
62+
return ""
63+
64+
def add_issue(self, issue):
65+
self.issues.append(issue)
66+
67+
def check_invalid_file_form_configuration(self, html_content):
68+
parser = FormParser()
69+
parser.feed(html_content)
70+
71+
for form in parser.forms:
72+
if (
73+
form["file_form"]
74+
and form["form_attrs"].get("enctype") != "multipart/form-data"
75+
and not any(
76+
elem.get("formenctype") == "multipart/form-data"
77+
for elem in form["submit_element_attrs"]
78+
)
79+
):
80+
form_id = form["form_attrs"].get("id", "no form id")
81+
issue = (
82+
f'Form with id "{form_id}" contains file input but '
83+
"does not have multipart/form-data encoding."
84+
)
85+
self.add_issue({"issue": issue})
86+
return self.issues
87+
88+
def generate_stats(self, request, response):
89+
html_content = response.content.decode(response.charset)
90+
self.check_invalid_file_form_configuration(html_content)
91+
92+
# Further issue checks can go here
93+
94+
# Write all issues to record_stats
95+
self.record_stats({"issues": self.issues})

debug_toolbar/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def get_config():
5858

5959

6060
PANELS_DEFAULTS = [
61+
"debug_toolbar.panels.alerts.AlertsPanel",
6162
"debug_toolbar.panels.history.HistoryPanel",
6263
"debug_toolbar.panels.versions.VersionsPanel",
6364
"debug_toolbar.panels.timer.TimerPanel",
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{% load i18n %}
2+
3+
{% if issues %}
4+
<h4>{% trans "Issues found" %}</h4>
5+
{% for issue in issues %}
6+
<ul>
7+
<li>{{ issue.issue }}</li>
8+
</ul>
9+
{% endfor %}
10+
{% else %}
11+
<p>No issues found.</p>
12+
{% endif %}

docs/changes.rst

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

7+
* Added alert panel with warning when form is using file fields
8+
without proper encoding type.
79
* Fixed overriding font-family for both light and dark themes.
810
* Restored compatibility with ``iptools.IpRangeList``.
911

docs/configuration.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ included in the toolbar. It works like Django's ``MIDDLEWARE`` setting. The
2020
default value is::
2121

2222
DEBUG_TOOLBAR_PANELS = [
23+
'debug_toolbar.panels.alerts.AlertsPanel',
2324
'debug_toolbar.panels.history.HistoryPanel',
2425
'debug_toolbar.panels.versions.VersionsPanel',
2526
'debug_toolbar.panels.timer.TimerPanel',

docs/panels.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@ Default built-in panels
99

1010
The following panels are enabled by default.
1111

12+
Alerts
13+
~~~~~~~
14+
15+
.. class:: debug_toolbar.panels.alerts.AlertsPanel
16+
17+
This panel shows alerts for a set of pre-defined issues. Currently, the only
18+
issue it checks for is the encoding of a form that takes a file input not
19+
being set to ``multipart/form-data``.
20+
1221
History
1322
~~~~~~~
1423

tests/panels/test_alerts.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from django.http import HttpResponse
2+
from django.template import Context, Template
3+
4+
from ..base import BaseTestCase
5+
6+
7+
class AlertsPanelTestCase(BaseTestCase):
8+
panel_id = "AlertsPanel"
9+
10+
def test_issue_warning_display(self):
11+
"""
12+
Test that the panel (does not) display[s] a warning when there are
13+
(no) issues.
14+
"""
15+
self.panel.issues = 0
16+
nav_subtitle = self.panel.nav_subtitle
17+
self.assertNotIn("issues found", nav_subtitle)
18+
19+
self.panel.issues = ["Issue 1", "Issue 2"]
20+
nav_subtitle = self.panel.nav_subtitle
21+
self.assertIn("2 issues found", nav_subtitle)
22+
23+
def test_file_form_without_enctype_multipart_form_data(self):
24+
"""
25+
Test that the panel displays a form invalid message when there is
26+
a file input but encoding not set to multipart/form-data.
27+
"""
28+
test_form = '<form id="test-form"><input type="file"></form>'
29+
result = self.panel.check_invalid_file_form_configuration(test_form)
30+
expected_error = (
31+
'Form with id "test-form" contains file input '
32+
"but does not have multipart/form-data encoding."
33+
)
34+
self.assertEqual(result[0]["issue"], expected_error)
35+
self.assertEqual(len(result), 1)
36+
37+
def test_file_form_with_enctype_multipart_form_data(self):
38+
test_form = """<form id="test-form" enctype="multipart/form-data">
39+
<input type="file">
40+
</form>"""
41+
result = self.panel.check_invalid_file_form_configuration(test_form)
42+
43+
self.assertEqual(len(result), 0)
44+
45+
def test_integration_file_form_without_enctype_multipart_form_data(self):
46+
t = Template('<form id="test-form"><input type="file"></form>')
47+
c = Context({})
48+
rendered_template = t.render(c)
49+
response = HttpResponse(content=rendered_template)
50+
51+
self.panel.generate_stats(self.request, response)
52+
53+
self.assertIn(
54+
"Form with id &quot;test-form&quot; contains file input "
55+
"but does not have multipart/form-data encoding.",
56+
self.panel.content,
57+
)

tests/panels/test_history.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ def test_urls(self):
6767
@override_settings(DEBUG=True)
6868
class HistoryViewsTestCase(IntegrationTestCase):
6969
PANEL_KEYS = {
70+
"AlertsPanel",
7071
"VersionsPanel",
7172
"TimerPanel",
7273
"SettingsPanel",

0 commit comments

Comments
 (0)