plain 0.82.0__py3-none-any.whl → 0.83.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of plain might be problematic. Click here for more details.

plain/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # plain changelog
2
2
 
3
+ ## [0.83.0](https://github.com/dropseed/plain/releases/plain@0.83.0) (2025-10-29)
4
+
5
+ ### What's changed
6
+
7
+ - Added comprehensive Content Security Policy (CSP) documentation explaining how to use nonces with inline scripts and styles ([784f3dd972](https://github.com/dropseed/plain/commit/784f3dd972))
8
+ - The `json_script` utility function now accepts an optional `nonce` parameter for CSP-compliant inline JSON scripts ([784f3dd972](https://github.com/dropseed/plain/commit/784f3dd972))
9
+
10
+ ### Upgrade instructions
11
+
12
+ - Any `|json_script` usages need to make sure the second argument is a nonce, not a custom encoder (which is now third)
13
+
3
14
  ## [0.82.0](https://github.com/dropseed/plain/releases/plain@0.82.0) (2025-10-29)
4
15
 
5
16
  ### What's changed
plain/http/README.md CHANGED
@@ -3,6 +3,7 @@
3
3
  **HTTP request and response handling.**
4
4
 
5
5
  - [Overview](#overview)
6
+ - [Content Security Policy (CSP)](#content-security-policy-csp)
6
7
 
7
8
  ## Overview
8
9
 
@@ -28,3 +29,61 @@ class ExampleView(View):
28
29
 
29
30
  return response
30
31
  ```
32
+
33
+ ## Content Security Policy (CSP)
34
+
35
+ Plain includes built-in support for Content Security Policy (CSP) through nonces, allowing you to use strict CSP policies without `'unsafe-inline'`.
36
+
37
+ Each request generates a unique cryptographically secure nonce available via [`request.csp_nonce`](request.py#Request.csp_nonce):
38
+
39
+ ### Configuring CSP Headers
40
+
41
+ Set `DEFAULT_RESPONSE_HEADERS` as a callable function to generate dynamic CSP headers with nonces:
42
+
43
+ ```python
44
+ # app/settings.py
45
+ def DEFAULT_RESPONSE_HEADERS(request):
46
+ """
47
+ Dynamic response headers with CSP nonces.
48
+ """
49
+ nonce = request.csp_nonce
50
+ return {
51
+ "Content-Security-Policy": (
52
+ f"default-src 'self'; "
53
+ f"script-src 'self' 'nonce-{nonce}'; "
54
+ f"style-src 'self' 'nonce-{nonce}'; "
55
+ f"img-src 'self' data:; "
56
+ f"font-src 'self'; "
57
+ f"connect-src 'self'; "
58
+ f"frame-ancestors 'self'; "
59
+ f"base-uri 'self'; "
60
+ f"form-action 'self'"
61
+ ),
62
+ }
63
+ ```
64
+
65
+ Use tools like [Google's CSP Evaluator](https://csp-evaluator.withgoogle.com/) to analyze your CSP policy and identify potential security issues or misconfigurations.
66
+
67
+ ### Using Nonces in Templates
68
+
69
+ Add the nonce attribute to inline scripts and styles in your templates:
70
+
71
+ ```html
72
+ <!-- Inline script with nonce -->
73
+ <script nonce="{{ request.csp_nonce }}">
74
+ console.log("This script is allowed by CSP");
75
+ </script>
76
+
77
+ <!-- Inline style with nonce -->
78
+ <style nonce="{{ request.csp_nonce }}">
79
+ .example { color: red; }
80
+ </style>
81
+ ```
82
+
83
+ External scripts and stylesheets loaded from `'self'` don't need nonces:
84
+
85
+ ```html
86
+ <!-- External scripts/styles work with 'self' directive -->
87
+ <script src="/assets/app.js"></script>
88
+ <link rel="stylesheet" href="/assets/app.css">
89
+ ```
plain/utils/html.py CHANGED
@@ -34,25 +34,30 @@ _json_script_escapes = {
34
34
  def json_script(
35
35
  value: Any,
36
36
  element_id: str | None = None,
37
+ nonce: str = "",
37
38
  encoder: type[json.JSONEncoder] | None = None,
38
39
  ) -> SafeString:
39
40
  """
40
41
  Escape all the HTML/XML special characters with their unicode escapes, so
41
42
  value is safe to be output anywhere except for inside a tag attribute. Wrap
42
43
  the escaped JSON in a script tag.
44
+
45
+ Args:
46
+ value: The data to encode as JSON
47
+ element_id: Optional ID attribute for the script tag
48
+ nonce: Optional CSP nonce for inline script tags
49
+ encoder: Optional custom JSON encoder class
43
50
  """
44
51
  from plain.json import PlainJSONEncoder
45
52
 
46
53
  json_str = json.dumps(value, cls=encoder or PlainJSONEncoder).translate(
47
54
  _json_script_escapes
48
55
  )
49
- if element_id:
50
- template = '<script id="{}" type="application/json">{}</script>'
51
- args = (element_id, mark_safe(json_str))
52
- else:
53
- template = '<script type="application/json">{}</script>'
54
- args = (mark_safe(json_str),)
55
- return format_html(template, *args)
56
+ id_attr = f' id="{element_id}"' if element_id else ""
57
+ nonce_attr = f' nonce="{nonce}"' if nonce else ""
58
+ return mark_safe(
59
+ f'<script{id_attr}{nonce_attr} type="application/json">{json_str}</script>'
60
+ )
56
61
 
57
62
 
58
63
  def conditional_escape(text: Any) -> SafeString | str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain
3
- Version: 0.82.0
3
+ Version: 0.83.0
4
4
  Summary: A web framework for building products with Python.
5
5
  Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
6
  License-File: LICENSE
@@ -1,5 +1,5 @@
1
1
  plain/AGENTS.md,sha256=As6EFSWWHJ9lYIxb2LMRqNRteH45SRs7a_VFslzF53M,1046
2
- plain/CHANGELOG.md,sha256=LafG-4y2ZZGMobjIsHK5Iob3e47rA_umHo__6KUA1rI,33085
2
+ plain/CHANGELOG.md,sha256=xY-6RVqtfACU0z3588U3ALK_B3tuaNuBbp1d2J8qFf8,33715
3
3
  plain/README.md,sha256=VvzhXNvf4I6ddmjBP9AExxxFXxs7RwyoxdgFm-W5dsg,4072
4
4
  plain/__main__.py,sha256=GK39854Lc_LO_JP8DzY9Y2MIQ4cQEl7SXFJy244-lC8,110
5
5
  plain/debug.py,sha256=C2OnFHtRGMrpCiHSt-P2r58JypgQZ62qzDBpV4mhtFM,855
@@ -56,7 +56,7 @@ plain/forms/boundfield.py,sha256=PEquPRn1BoVd4ZkIin8tjkBzRDMv-41wO8qHV0S1shg,196
56
56
  plain/forms/exceptions.py,sha256=MuMUF-33Qsc_HWk5zI3rcWzdvXzQbEOKAZvJKkbrL58,320
57
57
  plain/forms/fields.py,sha256=GQSTI6-eaHivIXj3TQSw2LpyWMbN0NZ-SHjUO_oxqeA,37132
58
58
  plain/forms/forms.py,sha256=GnJWzjIXOPByfrdiqjjo4xdKdkBw1gBD6Hq4mg73gHQ,11259
59
- plain/http/README.md,sha256=32uuWbarAcG_qmP-Fltk-_26HFKfY3dBdlrO2FDb7D0,756
59
+ plain/http/README.md,sha256=8FLsZ12Gx6lLa0mFMDcqWYetM6ysCvfHMZLLh4dAAVI,2611
60
60
  plain/http/__init__.py,sha256=gUTIGh-GbSIlh3SP-Db-XAOX4P_j2hh2445w8Cm-zKQ,1032
61
61
  plain/http/cookie.py,sha256=x13G3LIr0jxnPK1NQRptmi0DrAq9PsivQnQTm4LKaW0,2191
62
62
  plain/http/middleware.py,sha256=TPs585IIFjgp-5uUAJtIoigH6uwTS3FJqwFSsQdayd4,960
@@ -167,7 +167,7 @@ plain/utils/duration.py,sha256=QBFh-iLvRXuw0N0UUdFqabeJvZVqkgFME_c2gRSLwhI,1340
167
167
  plain/utils/encoding.py,sha256=40siECldXUFyZ_IkyQCeOkyQuXb7i4Rpys3nFWFHnuc,4482
168
168
  plain/utils/functional.py,sha256=HqRXyM6R3Sl9IbhGl6t-DdFwtfr05IYmq7rgFCWV_2U,14650
169
169
  plain/utils/hashable.py,sha256=1Kh_SFxsaR2d3xQMNEtHb4eKSHugtNt66iZ3ZvKjt50,811
170
- plain/utils/html.py,sha256=x9kruRAzs9mIJ3XlSmVcxIXOy-lSupZ4J3J0KlWr1Rc,4028
170
+ plain/utils/html.py,sha256=ctgzowSi_NkytPOw3FvyqEIXgOE-52trkTFPjiGvGVg,4202
171
171
  plain/utils/http.py,sha256=P7dkgWpC28Bm-_VNuj57VN8j8AdbaDu1CKxoggwBpKo,5847
172
172
  plain/utils/inspect.py,sha256=wbWDAiVa4MW4lwJZz-mvU8Db-I0Nq5DhHh9ew5w81EE,1480
173
173
  plain/utils/ipv6.py,sha256=TLOQVN0aqKGM4eS_HwarTgO-bGIql-k5AipDPgtQdCA,1352
@@ -188,8 +188,8 @@ plain/views/forms.py,sha256=ESZOXuo6IeYixp1RZvPb94KplkowRiwO2eGJCM6zJI0,2400
188
188
  plain/views/objects.py,sha256=5y0PoPPo07dQTTcJ_9kZcx0iI1O7regsooAIK4VqXQ0,5579
189
189
  plain/views/redirect.py,sha256=mIpSAFcaEyeLDyv4Fr6g_ektduG4Wfa6s6L-rkdazmM,2154
190
190
  plain/views/templates.py,sha256=9LgDMCv4C7JzLiyw8jHF-i4350ukwgixC_9y4faEGu0,1885
191
- plain-0.82.0.dist-info/METADATA,sha256=Az4gbY6JEIaJb3xC12DeYjEl2fezGU6GXHx64Ku1WLs,4516
192
- plain-0.82.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
193
- plain-0.82.0.dist-info/entry_points.txt,sha256=1Ys2lsSeMepD1vz8RSrJopna0RQfUd951vYvCRsvl6A,45
194
- plain-0.82.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
195
- plain-0.82.0.dist-info/RECORD,,
191
+ plain-0.83.0.dist-info/METADATA,sha256=hiux2R0QdqLHhIroLLNQdEyMh4ZnKnfzJod0bGsGNw0,4516
192
+ plain-0.83.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
193
+ plain-0.83.0.dist-info/entry_points.txt,sha256=1Ys2lsSeMepD1vz8RSrJopna0RQfUd951vYvCRsvl6A,45
194
+ plain-0.83.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
195
+ plain-0.83.0.dist-info/RECORD,,
File without changes