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 - ``