WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit 17d56bd

Browse files
committed
Generate some code for CSP support
1 parent c5671dd commit 17d56bd

File tree

6 files changed

+88
-19
lines changed

6 files changed

+88
-19
lines changed

CHANGELOG.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ Next version
88
~~~~~~~~~~~~
99

1010
- Added a ``static_lazy`` helper.
11+
- Added full CSP support for all object-based media classes:
12+
- Added ``attrs`` parameter to ``CSS``, ``JSON``, and updated ``ImportMap`` constructor to accept attributes
13+
- All classes now support adding a ``nonce`` attribute for CSP security
1114

1215

1316
3.1 (2025-02-28)

README.rst

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Usage
1414
=====
1515

1616
Use this to insert a script tag via ``forms.Media`` containing additional
17-
attributes (such as ``id`` and ``data-*`` for CSP-compatible data
17+
attributes (such as ``id``, ``nonce`` for CSP support, and ``data-*`` for CSP-compatible data
1818
injection.):
1919

2020
.. code-block:: python
@@ -25,6 +25,7 @@ injection.):
2525
JS("asset.js", {
2626
"id": "asset-script",
2727
"data-answer": "42",
28+
"nonce": "{{ request.csp_nonce }}", # For CSP support
2829
}),
2930
])
3031
@@ -34,7 +35,7 @@ now contain a script tag as follows, without line breaks:
3435
.. code-block:: html
3536

3637
<script type="text/javascript" src="/static/asset.js"
37-
data-answer="42" id="asset-script"></script>
38+
data-answer="42" id="asset-script" nonce="random-nonce-value"></script>
3839

3940
The attributes are automatically escaped. The data attributes may now be
4041
accessed inside ``asset.js``:
@@ -65,21 +66,24 @@ So, you can add everything at once:
6566
6667
from js_asset import CSS, JS, JSON
6768
69+
# Get the CSP nonce from the request context
70+
nonce = request.csp_nonce
71+
6872
forms.Media(js=[
69-
JSON({"configuration": 42}, id="widget-configuration"),
70-
CSS("widget/style.css"),
71-
CSS("p{color:red;}", inline=True),
72-
JS("widget/script.js", {"type": "module"}),
73+
JSON({"configuration": 42}, id="widget-configuration", attrs={"nonce": nonce}),
74+
CSS("widget/style.css", attrs={"nonce": nonce}),
75+
CSS("p{color:red;}", inline=True, attrs={"nonce": nonce}),
76+
JS("widget/script.js", {"type": "module", "nonce": nonce}),
7377
])
7478
7579
This produces:
7680

7781
.. code-block:: html
7882

79-
<script id="widget-configuration" type="application/json">{"configuration": 42}</script>
80-
<link href="/static/widget/style.css" media="all" rel="stylesheet">
81-
<style media="all">p{color:red;}</style>
82-
<script src="/static/widget/script.js" type="module"></script>
83+
<script id="widget-configuration" type="application/json" nonce="random-nonce-value">{"configuration": 42}</script>
84+
<link href="/static/widget/style.css" media="all" rel="stylesheet" nonce="random-nonce-value">
85+
<style media="all" nonce="random-nonce-value">p{color:red;}</style>
86+
<script src="/static/widget/script.js" type="module" nonce="random-nonce-value"></script>
8387

8488

8589

@@ -152,10 +156,15 @@ widget classes for the admin than for the rest of your site.
152156

153157
.. code-block:: python
154158
155-
# Example for adding a code.js JavaScript *module*
159+
# Example for adding a code.js JavaScript *module* with CSP support
160+
nonce = request.csp_nonce
161+
162+
# Create importmap with CSP nonce
163+
importmap_with_nonce = ImportMap(importmap._importmap, {"nonce": nonce})
164+
156165
forms.Media(js=[
157-
importmap, # See paragraph above!
158-
JS("code.js", {"type": "module"}),
166+
importmap_with_nonce, # See paragraph above!
167+
JS("code.js", {"type": "module", "nonce": nonce}),
159168
])
160169
161170
The code in ``code.js`` can now use a JavaScript import to import assets from

js_asset/js.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,32 @@ class CSS:
2323
src: str
2424
inline: bool = field(default=False, kw_only=True)
2525
media: str = "all"
26+
attrs: dict[str, Any] = field(default_factory=dict, kw_only=True)
2627

2728
def __hash__(self):
2829
return hash(self.__str__())
2930

3031
def __str__(self):
3132
if self.inline:
32-
return format_html('<style media="{}">{}</style>', self.media, self.src)
33+
if not self.attrs:
34+
return format_html('<style media="{}">{}</style>', self.media, self.src)
35+
return format_html(
36+
'<style media="{}"{}>{}</style>',
37+
self.media,
38+
mark_safe(flatatt(self.attrs)),
39+
self.src,
40+
)
41+
if not self.attrs:
42+
return format_html(
43+
'<link href="{}" media="{}" rel="stylesheet">',
44+
static_if_relative(self.src),
45+
self.media,
46+
)
3347
return format_html(
34-
'<link href="{}" media="{}" rel="stylesheet">',
48+
'<link href="{}" media="{}" rel="stylesheet"{}>',
3549
static_if_relative(self.src),
3650
self.media,
51+
mark_safe(flatatt(self.attrs)),
3752
)
3853

3954

@@ -59,25 +74,36 @@ def __str__(self):
5974
class JSON:
6075
data: dict[str, Any]
6176
id: str | None = field(default="", kw_only=True)
77+
attrs: dict[str, Any] = field(default_factory=dict, kw_only=True)
6278

6379
def __hash__(self):
6480
return hash(self.__str__())
6581

6682
def __str__(self):
67-
return json_script(self.data, self.id)
83+
if not self.attrs:
84+
return json_script(self.data, self.id)
85+
86+
script = json_script(self.data, self.id)
87+
# Insert attributes before the closing tag
88+
if self.attrs:
89+
attrs_str = flatatt(self.attrs)
90+
script = script.replace(">", f"{attrs_str}>", 1)
91+
return mark_safe(script)
6892

6993

7094
@html_safe
7195
class ImportMap:
72-
def __init__(self, importmap):
96+
def __init__(self, importmap, attrs=None):
7397
self._importmap = importmap
98+
self._attrs = attrs or {}
7499

75100
def __str__(self):
76101
if self._importmap:
77102
html = json_script(self._importmap).removeprefix(
78103
'<script type="application/json">'
79104
)
80-
return mark_safe(f'<script type="importmap">{html}')
105+
attrs_str = flatatt(self._attrs) if self._attrs else ""
106+
return mark_safe(f'<script type="importmap"{attrs_str}>{html}')
81107
return ""
82108

83109
def update(self, other):

tests/testapp/test_importmap.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,17 @@ def test_merging(self):
4040
"""\
4141
<script type="importmap">{"imports": {"a": "/static/a.js", "b": "/static/b.js", "/app/": "./original-app/", "/app/helper": "./helper/index.mjs"}, "integrity": {"/static/a.js": "sha384-blub-a", "/static/b.js": "sha384-blub-b"}, "scopes": {"/js": {"/app/": "./js-app/"}}}</script>""",
4242
)
43+
44+
def test_csp_nonce(self):
45+
# Test with CSP nonce attribute
46+
importmap = ImportMap(
47+
{
48+
"imports": {"a": "/static/a.js"},
49+
},
50+
attrs={"nonce": "random-nonce"},
51+
)
52+
53+
self.assertEqual(
54+
str(importmap),
55+
'<script type="importmap" nonce="random-nonce">{"imports": {"a": "/static/a.js"}}</script>',
56+
)

tests/testapp/test_js_asset.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,17 @@ def test_css(self):
8383
'<style media="all">p{color:red}</style>',
8484
)
8585

86+
# Test with CSP nonce attribute
87+
self.assertEqual(
88+
str(CSS("app/style.css", attrs={"nonce": "random-nonce"})),
89+
'<link href="/static/app/style.css" media="all" rel="stylesheet" nonce="random-nonce">',
90+
)
91+
92+
self.assertEqual(
93+
str(CSS("p{color:red}", inline=True, attrs={"nonce": "random-nonce"})),
94+
'<style media="all" nonce="random-nonce">p{color:red}</style>',
95+
)
96+
8697
def test_json(self):
8798
self.assertEqual(
8899
str(JSON({"hello": "world"}, id="hello")),
@@ -93,3 +104,9 @@ def test_json(self):
93104
str(JSON({"hello": "world"})),
94105
'<script type="application/json">{"hello": "world"}</script>',
95106
)
107+
108+
# Test with CSP nonce attribute
109+
self.assertEqual(
110+
str(JSON({"hello": "world"}, id="hello", attrs={"nonce": "random-nonce"})),
111+
'<script id="hello" type="application/json" nonce="random-nonce">{"hello": "world"}</script>',
112+
)

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,5 @@ deps =
1414
dj42: Django>=4.2,<5.0
1515
dj50: Django>=5.0,<5.1
1616
dj51: Django>=5.1,<5.2
17-
dj52: Django>=5.2a1,<6.0
17+
dj52: Django>=5.2,<6.0
1818
djmain: https://github.com/django/django/archive/main.tar.gz

0 commit comments

Comments
 (0)