diff --git a/.gitignore b/.gitignore
index ee3559cc4..988922d50 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,5 @@ htmlcov
.tox
geckodriver.log
coverage.xml
+.direnv/
+.envrc
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index af8ab8b6f..54a49e4d6 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -14,7 +14,7 @@ repos:
hooks:
- id: doc8
- repo: https://github.com/adamchainz/django-upgrade
- rev: 1.17.0
+ rev: 1.19.0
hooks:
- id: django-upgrade
args: [--target-version, "4.2"]
@@ -32,7 +32,7 @@ repos:
args:
- --trailing-comma=es5
- repo: https://github.com/pre-commit/mirrors-eslint
- rev: v9.3.0
+ rev: v9.6.0
hooks:
- id: eslint
additional_dependencies:
@@ -44,7 +44,7 @@ repos:
args:
- --fix
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: 'v0.4.5'
+ rev: 'v0.5.0'
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
diff --git a/README.rst b/README.rst
index 31c8a6f59..2ce1db4b7 100644
--- a/README.rst
+++ b/README.rst
@@ -44,7 +44,7 @@ Here's a screenshot of the toolbar in action:
In addition to the built-in panels, a number of third-party panels are
contributed by the community.
-The current stable version of the Debug Toolbar is 4.4.2. It works on
+The current stable version of the Debug Toolbar is 4.4.3. It works on
Django ≥ 4.2.0.
The Debug Toolbar does not currently support `Django's asynchronous views
diff --git a/debug_toolbar/__init__.py b/debug_toolbar/__init__.py
index 003f26cda..5ddb15d15 100644
--- a/debug_toolbar/__init__.py
+++ b/debug_toolbar/__init__.py
@@ -4,7 +4,7 @@
# Do not use pkg_resources to find the version but set it here directly!
# see issue #1446
-VERSION = "4.4.2"
+VERSION = "4.4.3"
# Code that discovers files or modules in INSTALLED_APPS imports this module.
urls = "debug_toolbar.urls", APP_NAME
diff --git a/debug_toolbar/apps.py b/debug_toolbar/apps.py
index a2e977d84..a49875bac 100644
--- a/debug_toolbar/apps.py
+++ b/debug_toolbar/apps.py
@@ -5,10 +5,12 @@
from django.conf import settings
from django.core.checks import Error, Warning, register
from django.middleware.gzip import GZipMiddleware
+from django.urls import NoReverseMatch, reverse
from django.utils.module_loading import import_string
from django.utils.translation import gettext_lazy as _
-from debug_toolbar import settings as dt_settings
+from debug_toolbar import APP_NAME, settings as dt_settings
+from debug_toolbar.settings import CONFIG_DEFAULTS
class DebugToolbarConfig(AppConfig):
@@ -213,7 +215,33 @@ def debug_toolbar_installed_when_running_tests_check(app_configs, **kwargs):
"""
Check that the toolbar is not being used when tests are running
"""
- if not settings.DEBUG and dt_settings.get_config()["IS_RUNNING_TESTS"]:
+ # Check if show toolbar callback has changed
+ show_toolbar_changed = (
+ dt_settings.get_config()["SHOW_TOOLBAR_CALLBACK"]
+ != CONFIG_DEFAULTS["SHOW_TOOLBAR_CALLBACK"]
+ )
+ try:
+ # Check if the toolbar's urls are installed
+ reverse(f"{APP_NAME}:render_panel")
+ toolbar_urls_installed = True
+ except NoReverseMatch:
+ toolbar_urls_installed = False
+
+ # If the user is using the default SHOW_TOOLBAR_CALLBACK,
+ # then the middleware will respect the change to settings.DEBUG.
+ # However, if the user has changed the callback to:
+ # DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": lambda request: DEBUG}
+ # where DEBUG is not settings.DEBUG, then it won't pick up that Django'
+ # test runner has changed the value for settings.DEBUG, and the middleware
+ # will inject the toolbar, while the URLs aren't configured leading to a
+ # NoReverseMatch error.
+ likely_error_setup = show_toolbar_changed and not toolbar_urls_installed
+
+ if (
+ not settings.DEBUG
+ and dt_settings.get_config()["IS_RUNNING_TESTS"]
+ and likely_error_setup
+ ):
return [
Error(
"The Django Debug Toolbar can't be used with tests",
diff --git a/debug_toolbar/middleware.py b/debug_toolbar/middleware.py
index 0513e2379..65b5282c5 100644
--- a/debug_toolbar/middleware.py
+++ b/debug_toolbar/middleware.py
@@ -20,8 +20,14 @@ def show_toolbar(request):
"""
Default function to determine whether to show the toolbar on a given page.
"""
- internal_ips = list(settings.INTERNAL_IPS)
+ if not settings.DEBUG:
+ return False
+ # Test: settings
+ if request.META.get("REMOTE_ADDR") in settings.INTERNAL_IPS:
+ return True
+
+ # Test: Docker
try:
# This is a hack for docker installations. It attempts to look
# up the IP address of the docker host.
@@ -31,11 +37,14 @@ def show_toolbar(request):
".".join(socket.gethostbyname("host.docker.internal").rsplit(".")[:-1])
+ ".1"
)
- internal_ips.append(docker_ip)
+ if request.META.get("REMOTE_ADDR") == docker_ip:
+ return True
except socket.gaierror:
# It's fine if the lookup errored since they may not be using docker
pass
- return settings.DEBUG and request.META.get("REMOTE_ADDR") in internal_ips
+
+ # No test passed
+ return False
@lru_cache(maxsize=None)
diff --git a/debug_toolbar/panels/alerts.py b/debug_toolbar/panels/alerts.py
new file mode 100644
index 000000000..27a7119ee
--- /dev/null
+++ b/debug_toolbar/panels/alerts.py
@@ -0,0 +1,148 @@
+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})
diff --git a/debug_toolbar/settings.py b/debug_toolbar/settings.py
index ca7036c34..48483cf40 100644
--- a/debug_toolbar/settings.py
+++ b/debug_toolbar/settings.py
@@ -67,6 +67,7 @@ def get_config():
"debug_toolbar.panels.sql.SQLPanel",
"debug_toolbar.panels.staticfiles.StaticFilesPanel",
"debug_toolbar.panels.templates.TemplatesPanel",
+ "debug_toolbar.panels.alerts.AlertsPanel",
"debug_toolbar.panels.cache.CachePanel",
"debug_toolbar.panels.signals.SignalsPanel",
"debug_toolbar.panels.redirects.RedirectsPanel",
diff --git a/debug_toolbar/static/debug_toolbar/css/toolbar.css b/debug_toolbar/static/debug_toolbar/css/toolbar.css
index e028a67b7..e495eeb0c 100644
--- a/debug_toolbar/static/debug_toolbar/css/toolbar.css
+++ b/debug_toolbar/static/debug_toolbar/css/toolbar.css
@@ -1,6 +1,5 @@
/* Variable definitions */
-:root,
-#djDebug[data-theme="light"] {
+:root {
/* Font families are the same as in Django admin/css/base.css */
--djdt-font-family-primary: "Segoe UI", system-ui, Roboto, "Helvetica Neue",
Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
@@ -10,7 +9,10 @@
"Source Code Pro", "Fira Mono", "Droid Sans Mono", "Courier New",
monospace, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
"Noto Color Emoji";
+}
+:root,
+#djDebug[data-theme="light"] {
--djdt-font-color: black;
--djdt-background-color: white;
--djdt-panel-content-background-color: #eee;
@@ -732,4 +734,6 @@
#djDebug[data-theme="dark"] #djToggleThemeButton svg.theme-dark,
#djDebug[data-theme="auto"] #djToggleThemeButton svg.theme-auto {
display: block;
+ height: 1rem;
+ width: 1rem;
}
diff --git a/debug_toolbar/templates/debug_toolbar/includes/theme_selector.html b/debug_toolbar/templates/debug_toolbar/includes/theme_selector.html
index 372727900..926ff250b 100644
--- a/debug_toolbar/templates/debug_toolbar/includes/theme_selector.html
+++ b/debug_toolbar/templates/debug_toolbar/includes/theme_selector.html
@@ -2,8 +2,6 @@
aria-hidden="true"
class="djdt-hidden theme-auto"
fill="currentColor"
- width="1rem"
- height="1rem"
viewBox="0 0 32 32"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
@@ -15,8 +13,6 @@