From f46c391f285cf3254d1c5917b6e92f5608f4d986 Mon Sep 17 00:00:00 2001 From: Dan Jacob Date: Fri, 5 Jul 2024 13:31:45 +0300 Subject: [PATCH 1/4] Check for for StreamingHttpResponse when generating stats in Alert panel. Also added unit test for this case. --- debug_toolbar/panels/alerts.py | 301 +++++++++++++++++---------------- tests/panels/test_alerts.py | 213 ++++++++++++----------- 2 files changed, 265 insertions(+), 249 deletions(-) diff --git a/debug_toolbar/panels/alerts.py b/debug_toolbar/panels/alerts.py index 27a7119ee..0ae1f9d4c 100644 --- a/debug_toolbar/panels/alerts.py +++ b/debug_toolbar/panels/alerts.py @@ -1,148 +1,153 @@ -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. - """ - - 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") - - 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): - """ - 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): - 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}) +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. + """ + + 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") + + 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): + """ + 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): + + # check if streaming response + if getattr(response, "streaming", True): + 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}) diff --git a/tests/panels/test_alerts.py b/tests/panels/test_alerts.py index e61c8da12..5c926f275 100644 --- a/tests/panels/test_alerts.py +++ b/tests/panels/test_alerts.py @@ -1,101 +1,112 @@ -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 = '
' - result = self.panel.check_invalid_file_form_configuration(test_form) - expected_error = ( - 'Form with id "test-form" contains file input, ' - 'but does not have the attribute enctype="multipart/form-data".' - ) - self.assertEqual(result[0]["alert"], expected_error) - self.assertEqual(len(result), 1) - - def test_file_form_no_id_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. - - This should use the message when the form has no id. - """ - test_form = '
' - result = self.panel.check_invalid_file_form_configuration(test_form) - expected_error = ( - "Form contains file input, but does not have " - 'the attribute enctype="multipart/form-data".' - ) - self.assertEqual(result[0]["alert"], expected_error) - self.assertEqual(len(result), 1) - - def test_file_form_with_enctype_multipart_form_data(self): - test_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 = """
- - -
""" - 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 = """
- """ - 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 the attribute enctype="multipart/form-data".' - ) - 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 = """
-
- """ - 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('
') - 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 "test-form" contains file input, " - "but does not have the attribute enctype="multipart/form-data".", - self.panel.content, - ) +from django.http import HttpResponse, StreamingHttpResponse +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 = '
' + result = self.panel.check_invalid_file_form_configuration(test_form) + expected_error = ( + 'Form with id "test-form" contains file input, ' + 'but does not have the attribute enctype="multipart/form-data".' + ) + self.assertEqual(result[0]["alert"], expected_error) + self.assertEqual(len(result), 1) + + def test_file_form_no_id_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. + + This should use the message when the form has no id. + """ + test_form = '
' + result = self.panel.check_invalid_file_form_configuration(test_form) + expected_error = ( + "Form contains file input, but does not have " + 'the attribute enctype="multipart/form-data".' + ) + self.assertEqual(result[0]["alert"], expected_error) + self.assertEqual(len(result), 1) + + def test_file_form_with_enctype_multipart_form_data(self): + test_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 = """
+ + +
""" + 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 = """
+ """ + 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 the attribute enctype="multipart/form-data".' + ) + 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 = """
+
+ """ + 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('
') + 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 "test-form" contains file input, " + "but does not have the attribute enctype="multipart/form-data".", + self.panel.content, + ) + + def test_streaming_response(self): + """Test to check for a streaming response.""" + + def _render(): + yield "ok" + + response = StreamingHttpResponse(_render()) + + self.panel.generate_stats(self.request, response) + self.assertEqual(self.panel.get_stats(), {}) From 1a4b36d5cdc15c70d5d7d720424d14fce112f067 Mon Sep 17 00:00:00 2001 From: Dan Jacob Date: Fri, 5 Jul 2024 13:40:25 +0300 Subject: [PATCH 2/4] Added entry to changes.rst --- docs/changes.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 4d26be57f..743623a24 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -4,6 +4,11 @@ Change log Pending ------- +4.4.3 (2024-07-05) +------------------ + +* Added check for StreamingHttpResponse in Alert Panel + 4.4.3 (2024-07-04) ------------------ From f43cd768c517f6d7d6e477c423088d79c0fd63ce Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 5 Jul 2024 10:41:47 +0000 Subject: [PATCH 3/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- debug_toolbar/panels/alerts.py | 1 - 1 file changed, 1 deletion(-) diff --git a/debug_toolbar/panels/alerts.py b/debug_toolbar/panels/alerts.py index 0ae1f9d4c..32c656dde 100644 --- a/debug_toolbar/panels/alerts.py +++ b/debug_toolbar/panels/alerts.py @@ -139,7 +139,6 @@ def check_invalid_file_form_configuration(self, html_content): return self.alerts def generate_stats(self, request, response): - # check if streaming response if getattr(response, "streaming", True): return From 4028159879f923cd20a7651bf494979a42e49741 Mon Sep 17 00:00:00 2001 From: Dan Jacob Date: Fri, 5 Jul 2024 13:44:05 +0300 Subject: [PATCH 4/4] Ruff format fixes --- debug_toolbar/panels/alerts.py | 1 - 1 file changed, 1 deletion(-) diff --git a/debug_toolbar/panels/alerts.py b/debug_toolbar/panels/alerts.py index 0ae1f9d4c..32c656dde 100644 --- a/debug_toolbar/panels/alerts.py +++ b/debug_toolbar/panels/alerts.py @@ -139,7 +139,6 @@ def check_invalid_file_form_configuration(self, html_content): return self.alerts def generate_stats(self, request, response): - # check if streaming response if getattr(response, "streaming", True): return