diff --git a/django/forms/widgets.py b/django/forms/widgets.py
index 1bcfeba2886e..697215540285 100644
--- a/django/forms/widgets.py
+++ b/django/forms/widgets.py
@@ -82,15 +82,28 @@ def __hash__(self):
return hash(self._path)
def __str__(self):
+ return self.render()
+
+ def __repr__(self):
+ return f"{type(self).__qualname__}({self._path!r})"
+
+ def render(self, *, attrs=None):
+ if (
+ attrs
+ and self.attributes
+ and (conflicts := attrs.keys() & self.attributes.keys())
+ ):
+ conflicts = ", ".join(sorted(conflicts))
+ raise ValueError(
+ f"{self.__class__.__qualname__} has conflicting attributes: "
+ f"{conflicts}"
+ )
return format_html(
self.element_template,
path=self.path,
- attributes=flatatt(self.attributes),
+ attributes=flatatt({**(attrs or {}), **self.attributes}),
)
- def __repr__(self):
- return f"{type(self).__qualname__}({self._path!r})"
-
@property
def path(self):
"""
@@ -142,38 +155,47 @@ def _css(self):
def _js(self):
return self.merge(*self._js_lists)
- def render(self):
+ def render(self, *, attrs=None):
return mark_safe(
"\n".join(
chain.from_iterable(
- getattr(self, "render_" + name)() for name in MEDIA_TYPES
+ getattr(self, "render_" + name)(attrs=attrs) for name in MEDIA_TYPES
)
)
)
- def render_js(self):
+ def render_js(self, *, attrs=None):
return [
(
- path.__html__()
- if hasattr(path, "__html__")
- else format_html('', self.absolute_path(path))
+ path.render(attrs=attrs)
+ if isinstance(path, MediaAsset)
+ else (
+ path.__html__()
+ if hasattr(path, "__html__")
+ else Script(path).render(attrs=attrs)
+ )
)
for path in self._js
]
- def render_css(self):
+ def render_css(self, *, attrs=None):
# To keep rendering order consistent, we can't just iterate over
# items(). We need to sort the keys, and iterate over the sorted list.
media = sorted(self._css)
return chain.from_iterable(
[
(
- path.__html__()
- if hasattr(path, "__html__")
- else format_html(
- '',
- self.absolute_path(path),
- medium,
+ path.render(attrs=attrs)
+ if isinstance(path, MediaAsset)
+ else (
+ path.__html__()
+ if hasattr(path, "__html__")
+ else format_html(
+ '',
+ self.absolute_path(path),
+ medium,
+ flatatt(attrs) if attrs else "",
+ )
)
)
for path in self._css[medium]
diff --git a/django/template/context_processors.py b/django/template/context_processors.py
index 214972de530e..1ff894ecfe96 100644
--- a/django/template/context_processors.py
+++ b/django/template/context_processors.py
@@ -12,6 +12,7 @@
from django.conf import settings
from django.middleware.csp import get_nonce
from django.middleware.csrf import get_token
+from django.utils.csp import CONTEXT_KEY as CSP_CONTEXT_KEY
from django.utils.functional import SimpleLazyObject, lazy
@@ -94,4 +95,4 @@ def csp(request):
"""
Add the CSP nonce to the context.
"""
- return {"csp_nonce": get_nonce(request)}
+ return {CSP_CONTEXT_KEY: get_nonce(request)}
diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py
index 3348e6426312..4f43b0242f0d 100644
--- a/django/template/defaulttags.py
+++ b/django/template/defaulttags.py
@@ -11,7 +11,7 @@
from django.conf import settings
from django.http import QueryDict
-from django.utils import timezone
+from django.utils import csp, timezone
from django.utils.datastructures import DeferredSubDict
from django.utils.html import conditional_escape, escape, format_html
from django.utils.lorem_ipsum import paragraphs, words
@@ -1685,3 +1685,8 @@ def do_with(parser, token):
nodelist = parser.parse(("endwith",))
parser.delete_first_token()
return WithNode(None, None, nodelist, extra_context=extra_context)
+
+
+@register.simple_tag(takes_context=True)
+def csp_nonce_attr(context, media=None):
+ return csp.nonce_attr(context, media)
diff --git a/django/utils/csp.py b/django/utils/csp.py
index 08a9d0752e38..08aaed685af7 100644
--- a/django/utils/csp.py
+++ b/django/utils/csp.py
@@ -2,6 +2,10 @@
from enum import StrEnum
from django.utils.functional import SimpleLazyObject, empty
+from django.utils.html import format_html
+
+# Template context key for the CSP nonce.
+CONTEXT_KEY = "csp_nonce"
class CSP(StrEnum):
@@ -63,10 +67,10 @@ class LazyNonce(SimpleLazyObject):
Example Django template usage with context processors enabled:
-
- The `{% if %}` block will only render if the nonce has been evaluated
- elsewhere.
+ ``{% csp_nonce_attr %}`` will only render the nonce attribute if the nonce
+ has been evaluated (i.e. accessed) elsewhere in the request/response cycle.
"""
@@ -77,6 +81,15 @@ def __bool__(self):
return self._wrapped is not empty
+def nonce_attr(context, media=None):
+ nonce = context.get(CONTEXT_KEY)
+ if media:
+ return media.render(attrs={"nonce": nonce} if nonce is not None else None)
+ if nonce is None:
+ return ""
+ return format_html('nonce="{}"', nonce)
+
+
def generate_nonce():
return secrets.token_urlsafe(16)
diff --git a/docs/howto/csp.txt b/docs/howto/csp.txt
index 756f815bf255..f37bb2a82ad9 100644
--- a/docs/howto/csp.txt
+++ b/docs/howto/csp.txt
@@ -73,8 +73,10 @@ To use nonces in your CSP policy, beside the basic config, you need to:
},
]
-3. In your templates, add the ``nonce`` attribute to the relevant inline
- ``