Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed #1682 -- alert user when using file field without proper encoding #1933

Merged
136 changes: 136 additions & 0 deletions debug_toolbar/panels/alerts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
from html.parser import HTMLParser

from django.utils.translation import gettext_lazy as _

from debug_toolbar.panels import Panel


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.
"""

title = _("Alerts")

template = "debug_toolbar/panels/alerts.html"

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

@property
def nav_subtitle(self):
alerts = self.get_stats()["alerts"]
if 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):
bkdekoning marked this conversation as resolved.
Show resolved Hide resolved
"""
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"]
)
tim-schilling marked this conversation as resolved.
Show resolved Hide resolved
):
form_id = form["form_attrs"].get("id", "no form id")
alert = (
f'Form with id "{form_id}" contains file input but '
"does not have multipart/form-data encoding."
)
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 = (
f'Input element references form with id "{form_id}" '
"but the form does not have multipart/form-data encoding."
)
self.add_alert({"alert": alert})

return self.alerts

def generate_stats(self, request, response):
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})
1 change: 1 addition & 0 deletions debug_toolbar/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def get_config():


PANELS_DEFAULTS = [
"debug_toolbar.panels.alerts.AlertsPanel",
"debug_toolbar.panels.history.HistoryPanel",
"debug_toolbar.panels.versions.VersionsPanel",
"debug_toolbar.panels.timer.TimerPanel",
Expand Down
12 changes: 12 additions & 0 deletions debug_toolbar/templates/debug_toolbar/panels/alerts.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{% load i18n %}

{% if alerts %}
<h4>{% trans "alerts found" %}</h4>
{% for alert in alerts %}
<ul>
<li>{{ alert.alert }}</li>
</ul>
{% endfor %}
{% else %}
<p>No alerts found.</p>
{% endif %}
2 changes: 2 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Change log
Pending
-------

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

Expand Down
1 change: 1 addition & 0 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ included in the toolbar. It works like Django's ``MIDDLEWARE`` setting. The
default value is::

DEBUG_TOOLBAR_PANELS = [
'debug_toolbar.panels.alerts.AlertsPanel',
'debug_toolbar.panels.history.HistoryPanel',
'debug_toolbar.panels.versions.VersionsPanel',
'debug_toolbar.panels.timer.TimerPanel',
Expand Down
11 changes: 11 additions & 0 deletions docs/panels.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ Default built-in panels

The following panels are enabled by default.

Alerts
~~~~~~~

.. class:: debug_toolbar.panels.alerts.AlertsPanel

This panel shows alerts for a set of pre-defined cases:

- Alerts when the response has a form without the
``enctype="multipart/form-data"`` attribute and the form contains
a file input

History
~~~~~~~

Expand Down
85 changes: 85 additions & 0 deletions tests/panels/test_alerts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from django.http import HttpResponse
from django.template import Context, Template

from ..base import BaseTestCase


class AlertsPanelTestCase(BaseTestCase):
panel_id = "AlertsPanel"

def test_alert_warning_display(self):
"""
Test that the panel (does not) display[s] an alert when there are
(no) problems.
"""
self.panel.record_stats({"alerts": []})
self.assertNotIn("alerts", self.panel.nav_subtitle)

self.panel.record_stats({"alerts": ["Alert 1", "Alert 2"]})
self.assertIn("2 alerts", self.panel.nav_subtitle)

def test_file_form_without_enctype_multipart_form_data(self):
"""
Test that the panel displays a form invalid message when there is
a file input but encoding not set to multipart/form-data.
"""
test_form = '<form id="test-form"><input type="file"></form>'
result = self.panel.check_invalid_file_form_configuration(test_form)
expected_error = (
'Form with id "test-form" contains file input '
"but does not have multipart/form-data encoding."
)
self.assertEqual(result[0]["alert"], expected_error)
self.assertEqual(len(result), 1)

def test_file_form_with_enctype_multipart_form_data(self):
test_form = """<form id="test-form" enctype="multipart/form-data">
<input type="file">
</form>"""
result = self.panel.check_invalid_file_form_configuration(test_form)

self.assertEqual(len(result), 0)

def test_file_form_with_enctype_multipart_form_data_in_button(self):
test_form = """<form id="test-form">
<input type="file">
<input type="submit" formenctype="multipart/form-data">
</form>"""
result = self.panel.check_invalid_file_form_configuration(test_form)

self.assertEqual(len(result), 0)

def test_referenced_file_input_without_enctype_multipart_form_data(self):
test_file_input = """<form id="test-form"></form>
<input type="file" form = "test-form">"""
result = self.panel.check_invalid_file_form_configuration(test_file_input)

expected_error = (
'Input element references form with id "test-form" '
"but the form does not have multipart/form-data encoding."
)
self.assertEqual(result[0]["alert"], expected_error)
self.assertEqual(len(result), 1)

def test_referenced_file_input_with_enctype_multipart_form_data(self):
test_file_input = """<form id="test-form" enctype="multipart/form-data">
</form>
<input type="file" form = "test-form">"""
result = self.panel.check_invalid_file_form_configuration(test_file_input)

self.assertEqual(len(result), 0)

def test_integration_file_form_without_enctype_multipart_form_data(self):
t = Template('<form id="test-form"><input type="file"></form>')
c = Context({})
rendered_template = t.render(c)
response = HttpResponse(content=rendered_template)

self.panel.generate_stats(self.request, response)

self.assertIn("1 alert", self.panel.nav_subtitle)
self.assertIn(
"Form with id &quot;test-form&quot; contains file input "
"but does not have multipart/form-data encoding.",
self.panel.content,
)
1 change: 1 addition & 0 deletions tests/panels/test_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def test_urls(self):
@override_settings(DEBUG=True)
class HistoryViewsTestCase(IntegrationTestCase):
PANEL_KEYS = {
"AlertsPanel",
"VersionsPanel",
"TimerPanel",
"SettingsPanel",
Expand Down