Skip to content

Commit

Permalink
Fixed django-commons#1682 -- alert user when using file field without…
Browse files Browse the repository at this point in the history
… proper encoding
  • Loading branch information
bkdekoning committed Jun 3, 2024
1 parent 9c4eb67 commit d19bf7b
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 0 deletions.
53 changes: 53 additions & 0 deletions debug_toolbar/panels/templates/panel.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from contextlib import contextmanager
from html.parser import HTMLParser
from os.path import normpath
from pprint import pformat, saferepr

Expand Down Expand Up @@ -58,6 +59,31 @@ def _request_context_bind_template(self, template):
RequestContext.bind_template = _request_context_bind_template


class FormParser(HTMLParser):
"""
HTML form parser, used to check for invalid configurations
"""

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

def handle_starttag(self, tag, attrs):
attrs = dict(attrs)
if tag == "form":
self.in_form = True
self.current_form = {"attrs": attrs, "file_inputs": []}
elif self.in_form and tag == "input" and attrs.get("type") == "file":
self.current_form["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 TemplatesPanel(Panel):
"""
A panel that lists all templates used during processing of a response.
Expand Down Expand Up @@ -177,6 +203,25 @@ def process_context_list(self, context_layers):

return context_list

def check_invalid_file_form_configuration(self, html_content):
parser = FormParser()
parser.feed(html_content)

invalid_forms = []
for form in parser.forms:
if (
form["file_inputs"]
and form["attrs"].get("enctype") != "multipart/form-data"
):
form_id = form["attrs"].get("id", "no form id")
error_message = (
f'Form with id "{form_id}" contains file input but '
f'missing enctype="multipart/form-data".'
)
invalid_forms.append({"form": form, "error_message": error_message})

return invalid_forms

def generate_stats(self, request, response):
template_context = []
for template_data in self.templates:
Expand Down Expand Up @@ -211,10 +256,18 @@ def generate_stats(self, request, response):
context_processors = None
template_dirs = []

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

self.record_stats(
{
"templates": template_context,
"template_dirs": [normpath(x) for x in template_dirs],
"context_processors": context_processors,
"invalid_file_form_configs": [
issue["error_message"] for issue in invalid_file_form_configs
],
}
)
9 changes: 9 additions & 0 deletions debug_toolbar/templates/debug_toolbar/panels/templates.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,12 @@ <h4>{% blocktrans count context_processors|length as context_processors_count %}
{% else %}
<p>{% trans "None" %}</p>
{% endif %}

{% if invalid_file_form_configs %}
<h4>{% blocktrans count invalid_file_form_configs|length as invalid_file_form_configs_count %}Invalid file form configuration{% plural %}Invalid file form configurations{% endblocktrans %}</h4>
<dl>
{% for invalid_file_form_config in invalid_file_form_configs %}
<dt>{{ invalid_file_form_config | escape }}</dt>
{% endfor %}
</dl>
{% endif %}
22 changes: 22 additions & 0 deletions tests/panels/test_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,28 @@ def test_object_with_non_ascii_repr_in_context(self):
self.panel.generate_stats(self.request, response)
self.assertIn("nôt åscíì", self.panel.content)

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 missing enctype="multipart/form-data".'
)
self.assertEqual(result[0]["error_message"], expected_error)

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_insert_content(self):
"""
Test that the panel only inserts content after generate_stats and
Expand Down

0 comments on commit d19bf7b

Please sign in to comment.