plain 0.5.0__py3-none-any.whl → 0.7.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.
- plain/cli/cli.py +35 -12
- plain/forms/fields.py +2 -3
- plain/middleware/security.py +4 -37
- plain/preflight/security/base.py +1 -155
- plain/runtime/README.md +0 -1
- plain/runtime/__init__.py +2 -2
- plain/runtime/global_settings.py +16 -29
- plain/runtime/user_settings.py +226 -217
- plain/signing.py +1 -1
- plain/utils/timezone.py +2 -23
- {plain-0.5.0.dist-info → plain-0.7.0.dist-info}/METADATA +1 -1
- {plain-0.5.0.dist-info → plain-0.7.0.dist-info}/RECORD +15 -16
- plain/middleware/clickjacking.py +0 -52
- {plain-0.5.0.dist-info → plain-0.7.0.dist-info}/LICENSE +0 -0
- {plain-0.5.0.dist-info → plain-0.7.0.dist-info}/WHEEL +0 -0
- {plain-0.5.0.dist-info → plain-0.7.0.dist-info}/entry_points.txt +0 -0
plain/cli/cli.py
CHANGED
@@ -5,6 +5,7 @@ import shutil
|
|
5
5
|
import subprocess
|
6
6
|
import sys
|
7
7
|
import traceback
|
8
|
+
from importlib.metadata import entry_points
|
8
9
|
from importlib.util import find_spec
|
9
10
|
from pathlib import Path
|
10
11
|
|
@@ -14,7 +15,9 @@ from click.core import Command, Context
|
|
14
15
|
import plain.runtime
|
15
16
|
from plain import preflight
|
16
17
|
from plain.assets.compile import compile_assets, get_compiled_path
|
18
|
+
from plain.exceptions import ImproperlyConfigured
|
17
19
|
from plain.packages import packages
|
20
|
+
from plain.utils.crypto import get_random_string
|
18
21
|
|
19
22
|
from .formatting import PlainContext
|
20
23
|
from .packages import EntryPointGroup, InstalledPackagesGroup
|
@@ -284,17 +287,10 @@ def compile(keep_original, fingerprint, compress):
|
|
284
287
|
)
|
285
288
|
sys.exit(1)
|
286
289
|
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
click.secho("Compiling Tailwind CSS", bold=True)
|
291
|
-
result = subprocess.run(["plain", "tailwind", "compile", "--minify"])
|
290
|
+
for entry_point in entry_points(group="plain.assets.compile"):
|
291
|
+
click.secho(f"Running {entry_point.name}", bold=True)
|
292
|
+
result = entry_point.load()()
|
292
293
|
print()
|
293
|
-
if result.returncode:
|
294
|
-
click.secho(
|
295
|
-
f"Error compiling Tailwind CSS (exit {result.returncode})", fg="red"
|
296
|
-
)
|
297
|
-
sys.exit(result.returncode)
|
298
294
|
|
299
295
|
# TODO also look in [tool.plain.compile.run]
|
300
296
|
|
@@ -406,6 +402,18 @@ def setting(setting_name):
|
|
406
402
|
click.secho(f'Setting "{setting_name}" not found', fg="red")
|
407
403
|
|
408
404
|
|
405
|
+
@plain_cli.group()
|
406
|
+
def utils():
|
407
|
+
pass
|
408
|
+
|
409
|
+
|
410
|
+
@utils.command()
|
411
|
+
def generate_secret_key():
|
412
|
+
"""Generate a new secret key"""
|
413
|
+
new_secret_key = get_random_string(50)
|
414
|
+
click.echo(new_secret_key)
|
415
|
+
|
416
|
+
|
409
417
|
class AppCLIGroup(click.Group):
|
410
418
|
"""
|
411
419
|
Loads app.cli if it exists as `plain app`
|
@@ -459,16 +467,31 @@ class PlainCommandCollection(click.CommandCollection):
|
|
459
467
|
EntryPointGroup(),
|
460
468
|
plain_cli,
|
461
469
|
]
|
462
|
-
except
|
470
|
+
except ImproperlyConfigured as e:
|
463
471
|
click.secho(
|
464
|
-
|
472
|
+
str(e),
|
465
473
|
fg="red",
|
466
474
|
err=True,
|
467
475
|
)
|
476
|
+
|
477
|
+
# None of these require the app to be setup
|
478
|
+
sources = [
|
479
|
+
EntryPointGroup(),
|
480
|
+
AppCLIGroup(),
|
481
|
+
plain_cli,
|
482
|
+
]
|
483
|
+
except Exception as e:
|
468
484
|
print("---")
|
469
485
|
print(traceback.format_exc())
|
470
486
|
print("---")
|
471
487
|
|
488
|
+
click.secho(
|
489
|
+
f"Error: {e}",
|
490
|
+
fg="red",
|
491
|
+
err=True,
|
492
|
+
)
|
493
|
+
|
494
|
+
# None of these require the app to be setup
|
472
495
|
sources = [
|
473
496
|
EntryPointGroup(),
|
474
497
|
AppCLIGroup(),
|
plain/forms/fields.py
CHANGED
@@ -14,7 +14,6 @@ from urllib.parse import urlsplit, urlunsplit
|
|
14
14
|
|
15
15
|
from plain import validators
|
16
16
|
from plain.exceptions import ValidationError
|
17
|
-
from plain.runtime import settings
|
18
17
|
from plain.utils import timezone
|
19
18
|
from plain.utils.dateparse import parse_datetime, parse_duration
|
20
19
|
from plain.utils.duration import duration_string
|
@@ -1001,7 +1000,7 @@ def from_current_timezone(value):
|
|
1001
1000
|
When time zone support is enabled, convert naive datetimes
|
1002
1001
|
entered in the current time zone to aware datetimes.
|
1003
1002
|
"""
|
1004
|
-
if
|
1003
|
+
if value is not None and timezone.is_naive(value):
|
1005
1004
|
current_timezone = timezone.get_current_timezone()
|
1006
1005
|
try:
|
1007
1006
|
if timezone._datetime_ambiguous_or_imaginary(value, current_timezone):
|
@@ -1025,6 +1024,6 @@ def to_current_timezone(value):
|
|
1025
1024
|
When time zone support is enabled, convert aware datetimes
|
1026
1025
|
to naive datetimes in the current time zone for display.
|
1027
1026
|
"""
|
1028
|
-
if
|
1027
|
+
if value is not None and timezone.is_aware(value):
|
1029
1028
|
return timezone.make_naive(value)
|
1030
1029
|
return value
|
plain/middleware/security.py
CHANGED
@@ -7,15 +7,11 @@ from plain.runtime import settings
|
|
7
7
|
class SecurityMiddleware:
|
8
8
|
def __init__(self, get_response):
|
9
9
|
self.get_response = get_response
|
10
|
-
self.sts_seconds = settings.SECURE_HSTS_SECONDS
|
11
|
-
self.sts_include_subdomains = settings.SECURE_HSTS_INCLUDE_SUBDOMAINS
|
12
|
-
self.sts_preload = settings.SECURE_HSTS_PRELOAD
|
13
|
-
self.content_type_nosniff = settings.SECURE_CONTENT_TYPE_NOSNIFF
|
14
10
|
self.redirect = settings.SECURE_SSL_REDIRECT
|
15
11
|
self.redirect_host = settings.SECURE_SSL_HOST
|
16
12
|
self.redirect_exempt = [re.compile(r) for r in settings.SECURE_REDIRECT_EXEMPT]
|
17
|
-
|
18
|
-
self.
|
13
|
+
|
14
|
+
self.default_headers = settings.SECURE_DEFAULT_HEADERS
|
19
15
|
|
20
16
|
def __call__(self, request):
|
21
17
|
path = request.path.lstrip("/")
|
@@ -29,36 +25,7 @@ class SecurityMiddleware:
|
|
29
25
|
|
30
26
|
response = self.get_response(request)
|
31
27
|
|
32
|
-
|
33
|
-
|
34
|
-
and request.is_secure()
|
35
|
-
and "Strict-Transport-Security" not in response
|
36
|
-
):
|
37
|
-
sts_header = "max-age=%s" % self.sts_seconds
|
38
|
-
if self.sts_include_subdomains:
|
39
|
-
sts_header += "; includeSubDomains"
|
40
|
-
if self.sts_preload:
|
41
|
-
sts_header += "; preload"
|
42
|
-
response.headers["Strict-Transport-Security"] = sts_header
|
43
|
-
|
44
|
-
if self.content_type_nosniff:
|
45
|
-
response.headers.setdefault("X-Content-Type-Options", "nosniff")
|
46
|
-
|
47
|
-
if self.referrer_policy:
|
48
|
-
# Support a comma-separated string or iterable of values to allow
|
49
|
-
# fallback.
|
50
|
-
response.headers.setdefault(
|
51
|
-
"Referrer-Policy",
|
52
|
-
",".join(
|
53
|
-
[v.strip() for v in self.referrer_policy.split(",")]
|
54
|
-
if isinstance(self.referrer_policy, str)
|
55
|
-
else self.referrer_policy
|
56
|
-
),
|
57
|
-
)
|
28
|
+
for header, value in self.default_headers.items():
|
29
|
+
response.headers.setdefault(header, value)
|
58
30
|
|
59
|
-
if self.cross_origin_opener_policy:
|
60
|
-
response.setdefault(
|
61
|
-
"Cross-Origin-Opener-Policy",
|
62
|
-
self.cross_origin_opener_policy,
|
63
|
-
)
|
64
31
|
return response
|
plain/preflight/security/base.py
CHANGED
@@ -1,23 +1,7 @@
|
|
1
1
|
from plain.exceptions import ImproperlyConfigured
|
2
2
|
from plain.runtime import settings
|
3
3
|
|
4
|
-
from .. import
|
5
|
-
|
6
|
-
CROSS_ORIGIN_OPENER_POLICY_VALUES = {
|
7
|
-
"same-origin",
|
8
|
-
"same-origin-allow-popups",
|
9
|
-
"unsafe-none",
|
10
|
-
}
|
11
|
-
REFERRER_POLICY_VALUES = {
|
12
|
-
"no-referrer",
|
13
|
-
"no-referrer-when-downgrade",
|
14
|
-
"origin",
|
15
|
-
"origin-when-cross-origin",
|
16
|
-
"same-origin",
|
17
|
-
"strict-origin",
|
18
|
-
"strict-origin-when-cross-origin",
|
19
|
-
"unsafe-url",
|
20
|
-
}
|
4
|
+
from .. import Warning, register
|
21
5
|
|
22
6
|
SECRET_KEY_INSECURE_PREFIX = "plain-insecure-"
|
23
7
|
SECRET_KEY_MIN_LENGTH = 50
|
@@ -41,43 +25,6 @@ W001 = Warning(
|
|
41
25
|
id="security.W001",
|
42
26
|
)
|
43
27
|
|
44
|
-
W002 = Warning(
|
45
|
-
"You do not have "
|
46
|
-
"'plain.middleware.clickjacking.XFrameOptionsMiddleware' in your "
|
47
|
-
"MIDDLEWARE, so your pages will not be served with an "
|
48
|
-
"'x-frame-options' header. Unless there is a good reason for your "
|
49
|
-
"site to be served in a frame, you should consider enabling this "
|
50
|
-
"header to help prevent clickjacking attacks.",
|
51
|
-
id="security.W002",
|
52
|
-
)
|
53
|
-
|
54
|
-
W004 = Warning(
|
55
|
-
"You have not set a value for the SECURE_HSTS_SECONDS setting. "
|
56
|
-
"If your entire site is served only over SSL, you may want to consider "
|
57
|
-
"setting a value and enabling HTTP Strict Transport Security. "
|
58
|
-
"Be sure to read the documentation first; enabling HSTS carelessly "
|
59
|
-
"can cause serious, irreversible problems.",
|
60
|
-
id="security.W004",
|
61
|
-
)
|
62
|
-
|
63
|
-
W005 = Warning(
|
64
|
-
"You have not set the SECURE_HSTS_INCLUDE_SUBDOMAINS setting to True. "
|
65
|
-
"Without this, your site is potentially vulnerable to attack "
|
66
|
-
"via an insecure connection to a subdomain. Only set this to True if "
|
67
|
-
"you are certain that all subdomains of your domain should be served "
|
68
|
-
"exclusively via SSL.",
|
69
|
-
id="security.W005",
|
70
|
-
)
|
71
|
-
|
72
|
-
W006 = Warning(
|
73
|
-
"Your SECURE_CONTENT_TYPE_NOSNIFF setting is not set to True, "
|
74
|
-
"so your pages will not be served with an "
|
75
|
-
"'X-Content-Type-Options: nosniff' header. "
|
76
|
-
"You should consider enabling this header to prevent the "
|
77
|
-
"browser from identifying content types incorrectly.",
|
78
|
-
id="security.W006",
|
79
|
-
)
|
80
|
-
|
81
28
|
W008 = Warning(
|
82
29
|
"Your SECURE_SSL_REDIRECT setting is not set to True. "
|
83
30
|
"Unless your site should be available over both SSL and non-SSL "
|
@@ -102,34 +49,6 @@ W020 = Warning(
|
|
102
49
|
id="security.W020",
|
103
50
|
)
|
104
51
|
|
105
|
-
W021 = Warning(
|
106
|
-
"You have not set the SECURE_HSTS_PRELOAD setting to True. Without this, "
|
107
|
-
"your site cannot be submitted to the browser preload list.",
|
108
|
-
id="security.W021",
|
109
|
-
)
|
110
|
-
|
111
|
-
W022 = Warning(
|
112
|
-
"You have not set the SECURE_REFERRER_POLICY setting. Without this, your "
|
113
|
-
"site will not send a Referrer-Policy header. You should consider "
|
114
|
-
"enabling this header to protect user privacy.",
|
115
|
-
id="security.W022",
|
116
|
-
)
|
117
|
-
|
118
|
-
E023 = Error(
|
119
|
-
"You have set the SECURE_REFERRER_POLICY setting to an invalid value.",
|
120
|
-
hint="Valid values are: {}.".format(", ".join(sorted(REFERRER_POLICY_VALUES))),
|
121
|
-
id="security.E023",
|
122
|
-
)
|
123
|
-
|
124
|
-
E024 = Error(
|
125
|
-
"You have set the SECURE_CROSS_ORIGIN_OPENER_POLICY setting to an invalid "
|
126
|
-
"value.",
|
127
|
-
hint="Valid values are: {}.".format(
|
128
|
-
", ".join(sorted(CROSS_ORIGIN_OPENER_POLICY_VALUES)),
|
129
|
-
),
|
130
|
-
id="security.E024",
|
131
|
-
)
|
132
|
-
|
133
52
|
W025 = Warning(SECRET_KEY_WARNING_MSG, id="security.W025")
|
134
53
|
|
135
54
|
|
@@ -137,58 +56,12 @@ def _security_middleware():
|
|
137
56
|
return "plain.middleware.security.SecurityMiddleware" in settings.MIDDLEWARE
|
138
57
|
|
139
58
|
|
140
|
-
def _xframe_middleware():
|
141
|
-
return (
|
142
|
-
"plain.middleware.clickjacking.XFrameOptionsMiddleware" in settings.MIDDLEWARE
|
143
|
-
)
|
144
|
-
|
145
|
-
|
146
59
|
@register(deploy=True)
|
147
60
|
def check_security_middleware(package_configs, **kwargs):
|
148
61
|
passed_check = _security_middleware()
|
149
62
|
return [] if passed_check else [W001]
|
150
63
|
|
151
64
|
|
152
|
-
@register(deploy=True)
|
153
|
-
def check_xframe_options_middleware(package_configs, **kwargs):
|
154
|
-
passed_check = _xframe_middleware()
|
155
|
-
return [] if passed_check else [W002]
|
156
|
-
|
157
|
-
|
158
|
-
@register(deploy=True)
|
159
|
-
def check_sts(package_configs, **kwargs):
|
160
|
-
passed_check = not _security_middleware() or settings.SECURE_HSTS_SECONDS
|
161
|
-
return [] if passed_check else [W004]
|
162
|
-
|
163
|
-
|
164
|
-
@register(deploy=True)
|
165
|
-
def check_sts_include_subdomains(package_configs, **kwargs):
|
166
|
-
passed_check = (
|
167
|
-
not _security_middleware()
|
168
|
-
or not settings.SECURE_HSTS_SECONDS
|
169
|
-
or settings.SECURE_HSTS_INCLUDE_SUBDOMAINS is True
|
170
|
-
)
|
171
|
-
return [] if passed_check else [W005]
|
172
|
-
|
173
|
-
|
174
|
-
@register(deploy=True)
|
175
|
-
def check_sts_preload(package_configs, **kwargs):
|
176
|
-
passed_check = (
|
177
|
-
not _security_middleware()
|
178
|
-
or not settings.SECURE_HSTS_SECONDS
|
179
|
-
or settings.SECURE_HSTS_PRELOAD is True
|
180
|
-
)
|
181
|
-
return [] if passed_check else [W021]
|
182
|
-
|
183
|
-
|
184
|
-
@register(deploy=True)
|
185
|
-
def check_content_type_nosniff(package_configs, **kwargs):
|
186
|
-
passed_check = (
|
187
|
-
not _security_middleware() or settings.SECURE_CONTENT_TYPE_NOSNIFF is True
|
188
|
-
)
|
189
|
-
return [] if passed_check else [W006]
|
190
|
-
|
191
|
-
|
192
65
|
@register(deploy=True)
|
193
66
|
def check_ssl_redirect(package_configs, **kwargs):
|
194
67
|
passed_check = not _security_middleware() or settings.SECURE_SSL_REDIRECT is True
|
@@ -239,30 +112,3 @@ def check_debug(package_configs, **kwargs):
|
|
239
112
|
@register(deploy=True)
|
240
113
|
def check_allowed_hosts(package_configs, **kwargs):
|
241
114
|
return [] if settings.ALLOWED_HOSTS else [W020]
|
242
|
-
|
243
|
-
|
244
|
-
@register(deploy=True)
|
245
|
-
def check_referrer_policy(package_configs, **kwargs):
|
246
|
-
if _security_middleware():
|
247
|
-
if settings.SECURE_REFERRER_POLICY is None:
|
248
|
-
return [W022]
|
249
|
-
# Support a comma-separated string or iterable of values to allow fallback.
|
250
|
-
if isinstance(settings.SECURE_REFERRER_POLICY, str):
|
251
|
-
values = {v.strip() for v in settings.SECURE_REFERRER_POLICY.split(",")}
|
252
|
-
else:
|
253
|
-
values = set(settings.SECURE_REFERRER_POLICY)
|
254
|
-
if not values <= REFERRER_POLICY_VALUES:
|
255
|
-
return [E023]
|
256
|
-
return []
|
257
|
-
|
258
|
-
|
259
|
-
@register(deploy=True)
|
260
|
-
def check_cross_origin_opener_policy(package_configs, **kwargs):
|
261
|
-
if (
|
262
|
-
_security_middleware()
|
263
|
-
and settings.SECURE_CROSS_ORIGIN_OPENER_POLICY is not None
|
264
|
-
and settings.SECURE_CROSS_ORIGIN_OPENER_POLICY
|
265
|
-
not in CROSS_ORIGIN_OPENER_POLICY_VALUES
|
266
|
-
):
|
267
|
-
return [E024]
|
268
|
-
return []
|
plain/runtime/README.md
CHANGED
plain/runtime/__init__.py
CHANGED
@@ -5,7 +5,7 @@ from pathlib import Path
|
|
5
5
|
|
6
6
|
from dotenv import load_dotenv
|
7
7
|
|
8
|
-
from .user_settings import
|
8
|
+
from .user_settings import Settings
|
9
9
|
|
10
10
|
try:
|
11
11
|
__version__ = importlib.metadata.version("plain")
|
@@ -18,7 +18,7 @@ APP_PATH = Path.cwd() / "app"
|
|
18
18
|
|
19
19
|
|
20
20
|
# from plain.runtime import settings
|
21
|
-
settings =
|
21
|
+
settings = Settings()
|
22
22
|
|
23
23
|
|
24
24
|
class AppPathNotFound(RuntimeError):
|
plain/runtime/global_settings.py
CHANGED
@@ -20,12 +20,9 @@ ALLOWED_HOSTS: list[str] = []
|
|
20
20
|
|
21
21
|
# Local time zone for this installation. All choices can be found here:
|
22
22
|
# https://en.wikipedia.org/wiki/List_of_tz_zones_by_name (although not all
|
23
|
-
# systems may support all possibilities).
|
24
|
-
#
|
25
|
-
TIME_ZONE = "
|
26
|
-
|
27
|
-
# If you set this to True, Plain will use timezone-aware datetimes.
|
28
|
-
USE_TZ = True
|
23
|
+
# systems may support all possibilities). This is interpreted as the default
|
24
|
+
# user time zone.
|
25
|
+
TIME_ZONE: str = "UTC"
|
29
26
|
|
30
27
|
# Default charset to use for all Response objects, if a MIME type isn't
|
31
28
|
# manually specified. It's used to construct the Content-Type header.
|
@@ -75,19 +72,6 @@ DATA_UPLOAD_MAX_NUMBER_FILES = 100
|
|
75
72
|
# (i.e. "/tmp" on *nix systems).
|
76
73
|
FILE_UPLOAD_TEMP_DIR = None
|
77
74
|
|
78
|
-
# The numeric mode to set newly-uploaded files to. The value should be a mode
|
79
|
-
# you'd pass directly to os.chmod; see
|
80
|
-
# https://docs.python.org/library/os.html#files-and-directories.
|
81
|
-
FILE_UPLOAD_PERMISSIONS = 0o644
|
82
|
-
|
83
|
-
# The numeric mode to assign to newly-created directories, when uploading files.
|
84
|
-
# The value should be a mode as you'd pass to os.chmod;
|
85
|
-
# see https://docs.python.org/library/os.html#files-and-directories.
|
86
|
-
FILE_UPLOAD_DIRECTORY_PERMISSIONS = None
|
87
|
-
|
88
|
-
# Default X-Frame-Options header value
|
89
|
-
X_FRAME_OPTIONS = "DENY"
|
90
|
-
|
91
75
|
USE_X_FORWARDED_HOST = False
|
92
76
|
USE_X_FORWARDED_PORT = False
|
93
77
|
|
@@ -114,14 +98,13 @@ MIDDLEWARE = [
|
|
114
98
|
"plain.middleware.security.SecurityMiddleware",
|
115
99
|
"plain.middleware.common.CommonMiddleware",
|
116
100
|
"plain.csrf.middleware.CsrfViewMiddleware",
|
117
|
-
"plain.middleware.clickjacking.XFrameOptionsMiddleware",
|
118
101
|
]
|
119
102
|
|
120
103
|
###########
|
121
104
|
# SIGNING #
|
122
105
|
###########
|
123
106
|
|
124
|
-
|
107
|
+
COOKIE_SIGNING_BACKEND = "plain.signing.TimestampSigner"
|
125
108
|
|
126
109
|
########
|
127
110
|
# CSRF #
|
@@ -132,7 +115,7 @@ CSRF_COOKIE_NAME = "csrftoken"
|
|
132
115
|
CSRF_COOKIE_AGE = 60 * 60 * 24 * 7 * 52
|
133
116
|
CSRF_COOKIE_DOMAIN = None
|
134
117
|
CSRF_COOKIE_PATH = "/"
|
135
|
-
CSRF_COOKIE_SECURE =
|
118
|
+
CSRF_COOKIE_SECURE = True
|
136
119
|
CSRF_COOKIE_HTTPONLY = False
|
137
120
|
CSRF_COOKIE_SAMESITE = "Lax"
|
138
121
|
CSRF_HEADER_NAME = "HTTP_X_CSRFTOKEN"
|
@@ -170,15 +153,19 @@ SILENCED_PREFLIGHT_CHECKS = []
|
|
170
153
|
#######################
|
171
154
|
# SECURITY MIDDLEWARE #
|
172
155
|
#######################
|
173
|
-
SECURE_CONTENT_TYPE_NOSNIFF = True
|
174
|
-
SECURE_CROSS_ORIGIN_OPENER_POLICY = "same-origin"
|
175
|
-
SECURE_HSTS_INCLUDE_SUBDOMAINS = False
|
176
|
-
SECURE_HSTS_PRELOAD = False
|
177
|
-
SECURE_HSTS_SECONDS = 0
|
178
156
|
SECURE_REDIRECT_EXEMPT = []
|
179
|
-
SECURE_REFERRER_POLICY = "same-origin"
|
180
157
|
SECURE_SSL_HOST = None
|
181
|
-
SECURE_SSL_REDIRECT =
|
158
|
+
SECURE_SSL_REDIRECT = True
|
159
|
+
|
160
|
+
SECURE_DEFAULT_HEADERS = {
|
161
|
+
# "Content-Security-Policy": "default-src 'self'",
|
162
|
+
# https://hstspreload.org/
|
163
|
+
# "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
|
164
|
+
"Cross-Origin-Opener-Policy": "same-origin",
|
165
|
+
"Referrer-Policy": "same-origin",
|
166
|
+
"X-Content-Type-Options": "nosniff",
|
167
|
+
"X-Frame-Options": "DENY",
|
168
|
+
}
|
182
169
|
|
183
170
|
#############
|
184
171
|
# Templates #
|
plain/runtime/user_settings.py
CHANGED
@@ -1,13 +1,5 @@
|
|
1
|
-
"""
|
2
|
-
Settings and configuration for Plain.
|
3
|
-
|
4
|
-
Read values from the module specified by the PLAIN_SETTINGS_MODULE environment
|
5
|
-
variable, and then from plain.global_settings; see the global_settings.py
|
6
|
-
for a list of all possible variables.
|
7
|
-
"""
|
8
1
|
import importlib
|
9
2
|
import json
|
10
|
-
import logging
|
11
3
|
import os
|
12
4
|
import time
|
13
5
|
import types
|
@@ -16,106 +8,256 @@ from pathlib import Path
|
|
16
8
|
|
17
9
|
from plain.exceptions import ImproperlyConfigured
|
18
10
|
from plain.packages import PackageConfig
|
19
|
-
from plain.utils.functional import LazyObject, empty
|
20
11
|
|
21
12
|
ENVIRONMENT_VARIABLE = "PLAIN_SETTINGS_MODULE"
|
22
13
|
ENV_SETTINGS_PREFIX = "PLAIN_"
|
23
|
-
|
24
|
-
logger = logging.getLogger("plain.runtime")
|
14
|
+
CUSTOM_SETTINGS_PREFIX = "APP_"
|
25
15
|
|
26
16
|
|
27
|
-
class
|
17
|
+
class Settings:
|
28
18
|
"""
|
29
|
-
|
30
|
-
|
19
|
+
Settings and configuration for Plain.
|
20
|
+
|
21
|
+
This class handles loading settings from the module specified by the
|
22
|
+
PLAIN_SETTINGS_MODULE environment variable, as well as from default settings,
|
23
|
+
environment variables, and explicit settings in the settings module.
|
24
|
+
|
25
|
+
Lazy initialization is implemented to defer loading until settings are first accessed.
|
31
26
|
"""
|
32
27
|
|
33
|
-
def
|
34
|
-
|
28
|
+
def __init__(self, settings_module=None):
|
29
|
+
self._settings_module = settings_module
|
30
|
+
self._settings = {}
|
31
|
+
self._errors = [] # Collect configuration errors
|
32
|
+
self.configured = False
|
35
33
|
|
36
|
-
def
|
37
|
-
self.
|
34
|
+
def _setup(self):
|
35
|
+
if self.configured:
|
36
|
+
return
|
37
|
+
else:
|
38
|
+
self.configured = True
|
38
39
|
|
40
|
+
self._settings = {} # Maps setting names to SettingDefinition instances
|
39
41
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
42
|
+
# Determine the settings module
|
43
|
+
if self._settings_module is None:
|
44
|
+
self._settings_module = os.environ.get(ENVIRONMENT_VARIABLE, "settings")
|
45
|
+
|
46
|
+
# First load the global settings from plain
|
47
|
+
self._load_module_settings(
|
48
|
+
importlib.import_module("plain.runtime.global_settings")
|
49
|
+
)
|
46
50
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
self._wrapped = Settings(settings_module)
|
51
|
+
# Import the user's settings module
|
52
|
+
try:
|
53
|
+
mod = importlib.import_module(self._settings_module)
|
54
|
+
except ImportError as e:
|
55
|
+
raise ImproperlyConfigured(
|
56
|
+
f"Could not import settings '{self._settings_module}': {e}"
|
57
|
+
)
|
55
58
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
59
|
+
# Keep a reference to the settings.py module path
|
60
|
+
self.path = Path(mod.__file__).resolve()
|
61
|
+
|
62
|
+
# Load default settings from installed packages
|
63
|
+
self._load_default_settings(mod)
|
64
|
+
# Load environment settings
|
65
|
+
self._load_env_settings()
|
66
|
+
# Load explicit settings from the settings module
|
67
|
+
self._load_explicit_settings(mod)
|
68
|
+
# Check for any required settings that are missing
|
69
|
+
self._check_required_settings()
|
70
|
+
# Check for any collected errors
|
71
|
+
self._raise_errors_if_any()
|
72
|
+
|
73
|
+
def _load_module_settings(self, module):
|
74
|
+
annotations = getattr(module, "__annotations__", {})
|
75
|
+
settings = dir(module)
|
76
|
+
|
77
|
+
for setting in settings:
|
78
|
+
if setting.isupper():
|
79
|
+
if setting in self._settings:
|
80
|
+
self._errors.append(f"Duplicate setting '{setting}'.")
|
81
|
+
continue
|
82
|
+
|
83
|
+
setting_value = getattr(module, setting)
|
84
|
+
self._settings[setting] = SettingDefinition(
|
85
|
+
name=setting,
|
86
|
+
default_value=setting_value,
|
87
|
+
annotation=annotations.get(setting, None),
|
88
|
+
module=module,
|
89
|
+
)
|
90
|
+
|
91
|
+
# Store any annotations that didn't have a value (these are required settings)
|
92
|
+
for setting, annotation in annotations.items():
|
93
|
+
if setting not in self._settings:
|
94
|
+
self._settings[setting] = SettingDefinition(
|
95
|
+
name=setting,
|
96
|
+
default_value=None,
|
97
|
+
annotation=annotation,
|
98
|
+
module=module,
|
99
|
+
required=True,
|
100
|
+
)
|
101
|
+
|
102
|
+
def _load_default_settings(self, settings_module):
|
103
|
+
for entry in getattr(settings_module, "INSTALLED_PACKAGES", []):
|
104
|
+
try:
|
105
|
+
if isinstance(entry, PackageConfig):
|
106
|
+
app_settings = entry.module.default_settings
|
107
|
+
else:
|
108
|
+
app_settings = importlib.import_module(f"{entry}.default_settings")
|
109
|
+
except ModuleNotFoundError:
|
110
|
+
continue
|
111
|
+
|
112
|
+
self._load_module_settings(app_settings)
|
113
|
+
|
114
|
+
def _load_env_settings(self):
|
115
|
+
env_settings = {
|
116
|
+
k[len(ENV_SETTINGS_PREFIX) :]: v
|
117
|
+
for k, v in os.environ.items()
|
118
|
+
if k.startswith(ENV_SETTINGS_PREFIX) and k.isupper()
|
119
|
+
}
|
120
|
+
for setting, value in env_settings.items():
|
121
|
+
if setting in self._settings:
|
122
|
+
setting_def = self._settings[setting]
|
123
|
+
try:
|
124
|
+
parsed_value = _parse_env_value(value, setting_def.annotation)
|
125
|
+
setting_def.set_value(parsed_value, "env")
|
126
|
+
except ImproperlyConfigured as e:
|
127
|
+
self._errors.append(str(e))
|
128
|
+
|
129
|
+
def _load_explicit_settings(self, settings_module):
|
130
|
+
for setting in dir(settings_module):
|
131
|
+
if setting.isupper():
|
132
|
+
setting_value = getattr(settings_module, setting)
|
133
|
+
|
134
|
+
if setting in self._settings:
|
135
|
+
setting_def = self._settings[setting]
|
136
|
+
try:
|
137
|
+
setting_def.set_value(setting_value, "explicit")
|
138
|
+
except ImproperlyConfigured as e:
|
139
|
+
self._errors.append(str(e))
|
140
|
+
continue
|
141
|
+
|
142
|
+
elif setting.startswith(CUSTOM_SETTINGS_PREFIX):
|
143
|
+
# Accept custom settings prefixed with '{CUSTOM_SETTINGS_PREFIX}'
|
144
|
+
setting_def = SettingDefinition(
|
145
|
+
name=setting,
|
146
|
+
default_value=None,
|
147
|
+
annotation=None,
|
148
|
+
required=False,
|
149
|
+
)
|
150
|
+
try:
|
151
|
+
setting_def.set_value(setting_value, "explicit")
|
152
|
+
except ImproperlyConfigured as e:
|
153
|
+
self._errors.append(str(e))
|
154
|
+
continue
|
155
|
+
self._settings[setting] = setting_def
|
156
|
+
else:
|
157
|
+
# Collect unrecognized settings individually
|
158
|
+
self._errors.append(
|
159
|
+
f"Unknown setting '{setting}'. Custom settings must start with '{CUSTOM_SETTINGS_PREFIX}'."
|
160
|
+
)
|
161
|
+
|
162
|
+
if hasattr(time, "tzset") and self.TIME_ZONE:
|
163
|
+
zoneinfo_root = Path("/usr/share/zoneinfo")
|
164
|
+
zone_info_file = zoneinfo_root.joinpath(*self.TIME_ZONE.split("/"))
|
165
|
+
if zoneinfo_root.exists() and not zone_info_file.exists():
|
166
|
+
self._errors.append(
|
167
|
+
f"Invalid TIME_ZONE setting '{self.TIME_ZONE}'. Timezone file not found."
|
168
|
+
)
|
169
|
+
else:
|
170
|
+
os.environ["TZ"] = self.TIME_ZONE
|
171
|
+
time.tzset()
|
172
|
+
|
173
|
+
def _check_required_settings(self):
|
174
|
+
missing = [k for k, v in self._settings.items() if v.required and not v.is_set]
|
175
|
+
if missing:
|
176
|
+
self._errors.append(f"Missing required setting(s): {', '.join(missing)}.")
|
177
|
+
|
178
|
+
def _raise_errors_if_any(self):
|
179
|
+
if self._errors:
|
180
|
+
errors = ["- " + e for e in self._errors]
|
181
|
+
raise ImproperlyConfigured(
|
182
|
+
"Settings configuration errors:\n" + "\n".join(errors)
|
183
|
+
)
|
61
184
|
|
62
185
|
def __getattr__(self, name):
|
63
|
-
|
64
|
-
if
|
65
|
-
|
66
|
-
_wrapped = self._wrapped
|
67
|
-
val = getattr(_wrapped, name)
|
186
|
+
# Avoid recursion by directly returning internal attributes
|
187
|
+
if not name.isupper():
|
188
|
+
return object.__getattribute__(self, name)
|
68
189
|
|
69
|
-
|
70
|
-
# This is done here for performance reasons so the modified value is cached.
|
71
|
-
if name == "SECRET_KEY" and not val:
|
72
|
-
raise ImproperlyConfigured("The SECRET_KEY setting must not be empty.")
|
190
|
+
self._setup()
|
73
191
|
|
74
|
-
|
75
|
-
|
192
|
+
if name in self._settings:
|
193
|
+
return self._settings[name].value
|
194
|
+
else:
|
195
|
+
raise AttributeError(f"'Settings' object has no attribute '{name}'")
|
76
196
|
|
77
197
|
def __setattr__(self, name, value):
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
"""
|
82
|
-
if name == "_wrapped":
|
83
|
-
self.__dict__.clear()
|
198
|
+
# Handle internal attributes without recursion
|
199
|
+
if not name.isupper():
|
200
|
+
object.__setattr__(self, name, value)
|
84
201
|
else:
|
85
|
-
self.
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
super().__delattr__(name)
|
91
|
-
self.__dict__.pop(name, None)
|
202
|
+
if name in self._settings:
|
203
|
+
self._settings[name].set_value(value, "runtime")
|
204
|
+
self._raise_errors_if_any()
|
205
|
+
else:
|
206
|
+
object.__setattr__(self, name, value)
|
92
207
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
return self.
|
208
|
+
def __repr__(self):
|
209
|
+
if not self.configured:
|
210
|
+
return "<Settings [Unevaluated]>"
|
211
|
+
return f'<Settings "{self._settings_module}">'
|
212
|
+
|
213
|
+
|
214
|
+
def _parse_env_value(value, annotation):
|
215
|
+
if not annotation:
|
216
|
+
raise ImproperlyConfigured("Type hint required to set from environment.")
|
217
|
+
|
218
|
+
if annotation is bool:
|
219
|
+
# Special case for bools
|
220
|
+
return value.lower() in ("true", "1", "yes")
|
221
|
+
elif annotation is str:
|
222
|
+
return value
|
223
|
+
else:
|
224
|
+
# Parse other types using JSON
|
225
|
+
try:
|
226
|
+
return json.loads(value)
|
227
|
+
except json.JSONDecodeError as e:
|
228
|
+
raise ImproperlyConfigured(
|
229
|
+
f"Invalid JSON value for setting: {e.msg}"
|
230
|
+
) from e
|
97
231
|
|
98
232
|
|
99
233
|
class SettingDefinition:
|
100
|
-
"""Store
|
234
|
+
"""Store detailed information about settings."""
|
101
235
|
|
102
|
-
def __init__(
|
236
|
+
def __init__(
|
237
|
+
self, name, default_value=None, annotation=None, module=None, required=False
|
238
|
+
):
|
103
239
|
self.name = name
|
104
|
-
self.
|
240
|
+
self.default_value = default_value
|
105
241
|
self.annotation = annotation
|
106
242
|
self.module = module
|
107
243
|
self.required = required
|
244
|
+
self.value = default_value
|
245
|
+
self.source = "default" # 'default', 'env', 'explicit', or 'runtime'
|
246
|
+
self.is_set = False # Indicates if the value was set explicitly
|
108
247
|
|
109
|
-
def
|
110
|
-
|
248
|
+
def set_value(self, value, source):
|
249
|
+
self.check_type(value)
|
250
|
+
self.value = value
|
251
|
+
self.source = source
|
252
|
+
self.is_set = True
|
111
253
|
|
112
254
|
def check_type(self, obj):
|
113
255
|
if not self.annotation:
|
114
256
|
return
|
115
257
|
|
116
258
|
if not SettingDefinition._is_instance_of_type(obj, self.annotation):
|
117
|
-
raise
|
118
|
-
f"
|
259
|
+
raise ImproperlyConfigured(
|
260
|
+
f"'{self.name}': Expected type {self.annotation}, but got {type(obj)}."
|
119
261
|
)
|
120
262
|
|
121
263
|
@staticmethod
|
@@ -152,153 +294,20 @@ class SettingDefinition:
|
|
152
294
|
for i, item in enumerate(value)
|
153
295
|
)
|
154
296
|
|
155
|
-
raise ValueError("Unsupported type hint:
|
156
|
-
|
157
|
-
|
158
|
-
class Settings:
|
159
|
-
def __init__(self, settings_module):
|
160
|
-
self._default_settings = {}
|
161
|
-
self._explicit_settings = set()
|
162
|
-
|
163
|
-
# First load the global settings from plain
|
164
|
-
self._load_module_settings(
|
165
|
-
importlib.import_module("plain.runtime.global_settings")
|
166
|
-
)
|
167
|
-
|
168
|
-
# store the settings module in case someone later cares
|
169
|
-
self.SETTINGS_MODULE = settings_module
|
170
|
-
|
171
|
-
mod = importlib.import_module(self.SETTINGS_MODULE)
|
172
|
-
|
173
|
-
# Keep a reference to the settings.py module path
|
174
|
-
# so we can find files next to it (assume it's at the app root)
|
175
|
-
self.path = Path(mod.__file__).resolve()
|
176
|
-
|
177
|
-
# First, get all the default_settings from the INSTALLED_PACKAGES and set those values
|
178
|
-
self._load_default_settings(mod)
|
179
|
-
# Second, look at the environment variables and overwrite with those
|
180
|
-
self._load_env_settings()
|
181
|
-
# Finally, load the explicit settings from the settings module
|
182
|
-
self._load_explicit_settings(mod)
|
183
|
-
# Check for any required settings that are missing
|
184
|
-
self._check_required_settings()
|
185
|
-
|
186
|
-
def _load_default_settings(self, settings_module):
|
187
|
-
# Get INSTALLED_PACKAGES from mod,
|
188
|
-
# then (without populating packages) do a check for default_settings in each
|
189
|
-
# app and load those now too.
|
190
|
-
for entry in getattr(settings_module, "INSTALLED_PACKAGES", []):
|
191
|
-
try:
|
192
|
-
if isinstance(entry, PackageConfig):
|
193
|
-
app_settings = entry.module.default_settings
|
194
|
-
else:
|
195
|
-
app_settings = importlib.import_module(f"{entry}.default_settings")
|
196
|
-
except ModuleNotFoundError:
|
197
|
-
continue
|
198
|
-
|
199
|
-
self._load_module_settings(app_settings)
|
200
|
-
|
201
|
-
def _load_module_settings(self, module):
|
202
|
-
annotations = getattr(module, "__annotations__", {})
|
203
|
-
settings = dir(module)
|
204
|
-
|
205
|
-
for setting in settings:
|
206
|
-
if setting.isupper():
|
207
|
-
if hasattr(self, setting):
|
208
|
-
raise ImproperlyConfigured("The %s setting is duplicated" % setting)
|
297
|
+
raise ValueError(f"Unsupported type hint: {type_hint}")
|
209
298
|
|
210
|
-
|
211
|
-
|
212
|
-
# Set a simple attr on the settings object
|
213
|
-
setattr(self, setting, setting_value)
|
214
|
-
|
215
|
-
# Store a more complex setting reference for more detail
|
216
|
-
self._default_settings[setting] = SettingDefinition(
|
217
|
-
name=setting,
|
218
|
-
value=setting_value,
|
219
|
-
annotation=annotations.get(setting, ""),
|
220
|
-
module=module,
|
221
|
-
)
|
222
|
-
|
223
|
-
# Store any annotations that didn't have a value (these are required settings)
|
224
|
-
for setting, annotation in annotations.items():
|
225
|
-
if setting not in self._default_settings:
|
226
|
-
self._default_settings[setting] = SettingDefinition(
|
227
|
-
name=setting,
|
228
|
-
value=None,
|
229
|
-
annotation=annotation,
|
230
|
-
module=module,
|
231
|
-
required=True,
|
232
|
-
)
|
233
|
-
|
234
|
-
def _load_env_settings(self):
|
235
|
-
env_settings = {
|
236
|
-
k[len(ENV_SETTINGS_PREFIX) :]: v
|
237
|
-
for k, v in os.environ.items()
|
238
|
-
if k.startswith(ENV_SETTINGS_PREFIX)
|
239
|
-
}
|
240
|
-
logger.debug("Loading environment settings: %s", env_settings)
|
241
|
-
for setting, value in env_settings.items():
|
242
|
-
if setting not in self._default_settings:
|
243
|
-
# Ignore anything not defined in the default settings
|
244
|
-
continue
|
245
|
-
|
246
|
-
default_setting = self._default_settings[setting]
|
247
|
-
if not default_setting.annotation:
|
248
|
-
raise ValueError(
|
249
|
-
f"Setting {setting} needs a type hint to be set from the environment"
|
250
|
-
)
|
251
|
-
|
252
|
-
if default_setting.annotation is bool:
|
253
|
-
# Special case for bools
|
254
|
-
parsed_value = value.lower() in ("true", "1", "yes")
|
255
|
-
elif default_setting.annotation is str:
|
256
|
-
parsed_value = value
|
257
|
-
else:
|
258
|
-
# Anything besides a string will be parsed as JSON
|
259
|
-
# (works for ints, lists, etc.)
|
260
|
-
parsed_value = json.loads(value)
|
261
|
-
|
262
|
-
default_setting.check_type(parsed_value)
|
263
|
-
|
264
|
-
setattr(self, setting, parsed_value)
|
265
|
-
self._explicit_settings.add(setting)
|
266
|
-
|
267
|
-
def _load_explicit_settings(self, settings_module):
|
268
|
-
for setting in dir(settings_module):
|
269
|
-
if setting.isupper():
|
270
|
-
setting_value = getattr(settings_module, setting)
|
271
|
-
|
272
|
-
if setting in self._default_settings:
|
273
|
-
self._default_settings[setting].check_type(setting_value)
|
274
|
-
|
275
|
-
setattr(self, setting, setting_value)
|
276
|
-
self._explicit_settings.add(setting)
|
299
|
+
def __str__(self):
|
300
|
+
return f"SettingDefinition(name={self.name}, value={self.value}, source={self.source})"
|
277
301
|
|
278
|
-
if hasattr(time, "tzset") and self.TIME_ZONE:
|
279
|
-
# When we can, attempt to validate the timezone. If we can't find
|
280
|
-
# this file, no check happens and it's harmless.
|
281
|
-
zoneinfo_root = Path("/usr/share/zoneinfo")
|
282
|
-
zone_info_file = zoneinfo_root.joinpath(*self.TIME_ZONE.split("/"))
|
283
|
-
if zoneinfo_root.exists() and not zone_info_file.exists():
|
284
|
-
raise ValueError("Incorrect timezone setting: %s" % self.TIME_ZONE)
|
285
|
-
# Move the time zone info into os.environ. See ticket #2315 for why
|
286
|
-
# we don't do this unconditionally (breaks Windows).
|
287
|
-
os.environ["TZ"] = self.TIME_ZONE
|
288
|
-
time.tzset()
|
289
302
|
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
raise ImproperlyConfigured(
|
296
|
-
"The following settings are required: %s" % ", ".join(missing)
|
297
|
-
)
|
303
|
+
class SettingsReference(str):
|
304
|
+
"""
|
305
|
+
String subclass which references a current settings value. It's treated as
|
306
|
+
the value in memory but serializes to a settings.NAME attribute reference.
|
307
|
+
"""
|
298
308
|
|
299
|
-
|
300
|
-
|
301
|
-
return setting in self._explicit_settings
|
309
|
+
def __new__(self, value, setting_name):
|
310
|
+
return str.__new__(self, value)
|
302
311
|
|
303
|
-
def
|
304
|
-
|
312
|
+
def __init__(self, value, setting_name):
|
313
|
+
self.setting_name = setting_name
|
plain/signing.py
CHANGED
@@ -107,7 +107,7 @@ def _cookie_signer_key(key):
|
|
107
107
|
|
108
108
|
|
109
109
|
def get_cookie_signer(salt="plain.signing.get_cookie_signer"):
|
110
|
-
Signer = import_string(settings.
|
110
|
+
Signer = import_string(settings.COOKIE_SIGNING_BACKEND)
|
111
111
|
return Signer(
|
112
112
|
key=_cookie_signer_key(settings.SECRET_KEY),
|
113
113
|
fallback_keys=map(_cookie_signer_key, settings.SECRET_KEY_FALLBACKS),
|
plain/utils/timezone.py
CHANGED
@@ -138,27 +138,6 @@ class override(ContextDecorator):
|
|
138
138
|
_active.value = self.old_timezone
|
139
139
|
|
140
140
|
|
141
|
-
# Templates
|
142
|
-
|
143
|
-
|
144
|
-
def template_localtime(value, use_tz=None):
|
145
|
-
"""
|
146
|
-
Check if value is a datetime and converts it to local time if necessary.
|
147
|
-
|
148
|
-
If use_tz is provided and is not None, that will force the value to
|
149
|
-
be converted (or not), overriding the value of settings.USE_TZ.
|
150
|
-
|
151
|
-
This function is designed for use by the template engine.
|
152
|
-
"""
|
153
|
-
should_convert = (
|
154
|
-
isinstance(value, datetime)
|
155
|
-
and (settings.USE_TZ if use_tz is None else use_tz)
|
156
|
-
and not is_naive(value)
|
157
|
-
and getattr(value, "convert_to_local_time", True)
|
158
|
-
)
|
159
|
-
return localtime(value) if should_convert else value
|
160
|
-
|
161
|
-
|
162
141
|
# Utilities
|
163
142
|
|
164
143
|
|
@@ -184,9 +163,9 @@ def localtime(value=None, timezone=None):
|
|
184
163
|
|
185
164
|
def now():
|
186
165
|
"""
|
187
|
-
Return
|
166
|
+
Return a timezone aware datetime.
|
188
167
|
"""
|
189
|
-
return datetime.now(tz=timezone.utc
|
168
|
+
return datetime.now(tz=timezone.utc)
|
190
169
|
|
191
170
|
|
192
171
|
# By design, these four functions don't perform any checks on their arguments.
|
@@ -9,7 +9,7 @@ plain/assets/urls.py,sha256=ZTIoM1Zq35JaXZ3wFhXhfGa7VoITDNlH9i5RS0R5xow,933
|
|
9
9
|
plain/assets/views.py,sha256=dhjIpMu0GDR_VGbXM90_6RnC84C2C4bFv1RxDVklGBk,9173
|
10
10
|
plain/cli/README.md,sha256=xjr1K-sIMTi5OWxdxL--O7aoo16Pd1xdawIZtz6BL7Q,2464
|
11
11
|
plain/cli/__init__.py,sha256=9ByBOIdM8DebChjNz-RH2atdz4vWe8somlwNEsbhwh4,40
|
12
|
-
plain/cli/cli.py,sha256=
|
12
|
+
plain/cli/cli.py,sha256=TS9sxJfryyVSH4DlHf3FCpIvn6vF0s56aZ-lGG1MpcE,14817
|
13
13
|
plain/cli/formatting.py,sha256=1hZH13y1qwHcU2K2_Na388nw9uvoeQH8LrWL-O9h8Yc,2207
|
14
14
|
plain/cli/packages.py,sha256=69VH1bIi1-5N5l2jlBcR5EP0pt-v16sPar9arO3gCSE,2052
|
15
15
|
plain/cli/print.py,sha256=XraUYrgODOJquIiEv78wSCYGRBplHXtXSS9QtFG5hqY,217
|
@@ -23,7 +23,7 @@ plain/forms/README.md,sha256=fglB9MmHiEgfGGdZmcRstNl6eYaFljrElu2mzapK52M,377
|
|
23
23
|
plain/forms/__init__.py,sha256=UxqPwB8CiYPCQdHmUc59jadqaXqDmXBH8y4bt9vTPms,226
|
24
24
|
plain/forms/boundfield.py,sha256=LhydhCVR0okrli0-QBMjGjAJ8-06gTCXVEaBZhBouQk,1741
|
25
25
|
plain/forms/exceptions.py,sha256=XCLDRl5snIEDu5-8mLB0NnU_tegcBfyIHMiJxqvbxnc,164
|
26
|
-
plain/forms/fields.py,sha256=
|
26
|
+
plain/forms/fields.py,sha256=86ZE9jac6Zyg5vKsYGgyOUOIQLKxO--UomGXwA65tk4,35103
|
27
27
|
plain/forms/forms.py,sha256=-EcS2QVpAy4H95Y-RL108LnWnHLSyCGgEnUCdIIXnjg,10451
|
28
28
|
plain/http/README.md,sha256=00zLFQ-FPjYXu3A8QsLhCCXxaT0ImvI5I-8xd3dp8WA,7
|
29
29
|
plain/http/__init__.py,sha256=DIsDRbBsCGa4qZgq-fUuQS0kkxfbTU_3KpIM9VvH04w,1067
|
@@ -53,10 +53,9 @@ plain/logs/loggers.py,sha256=iz9SYcwP9w5QAuwpULl48SFkVyJuuMoQ_fdLgdCHpNg,2121
|
|
53
53
|
plain/logs/utils.py,sha256=9UzdCCQXJinGDs71Ngw297mlWkhgZStSd67ya4NOW98,1257
|
54
54
|
plain/middleware/README.md,sha256=MgiLHwAfP8ooBSlDi1JhTwIHMlwphOqAkeWglYRbe8s,52
|
55
55
|
plain/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
56
|
-
plain/middleware/clickjacking.py,sha256=MJOHWSDqJB8K6YE6XTh34uyr2LNKuE9XsywZRRsljFk,1764
|
57
56
|
plain/middleware/common.py,sha256=-YySkYUyaRujYA5Yg7GRD3xFjlQOZpeJP1Stpt6pias,3631
|
58
57
|
plain/middleware/gzip.py,sha256=2NogLO6hPxVc3otxkhMDl7-r2Zw3vcIkAP29fx4j2eU,2383
|
59
|
-
plain/middleware/security.py,sha256=
|
58
|
+
plain/middleware/security.py,sha256=WZRn5F9qx33wFTqh4CkBEtHrTuyr7RCt4Gwq4W2mBgE,1043
|
60
59
|
plain/packages/README.md,sha256=Vq1Nw3mmEmZ2IriQavuVi4BjcQC2nb8k7YIbnm8QjIg,799
|
61
60
|
plain/packages/__init__.py,sha256=DnHN1wwHXiXib4Y9BV__x9WrbUaTovoTIxW-tVyScTU,106
|
62
61
|
plain/packages/config.py,sha256=6Vdf1TEQllZkkEvK0WK__zHJYT9nxmS3EyYrbuq0GkM,11201
|
@@ -68,19 +67,19 @@ plain/preflight/files.py,sha256=wbHCNgps7o1c1zQNBd8FDCaVaqX90UwuvLgEQ_DbUpY,510
|
|
68
67
|
plain/preflight/messages.py,sha256=u0oc7q7YmBlKYJRcF5SQpzncfOkEzDhZTcpyclQDfHg,2427
|
69
68
|
plain/preflight/registry.py,sha256=ZpxnZPIklXuT8xZVTxCUp_IER3zhd7DdfsmqIpAbLj4,2306
|
70
69
|
plain/preflight/security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
71
|
-
plain/preflight/security/base.py,sha256=
|
70
|
+
plain/preflight/security/base.py,sha256=EY9rgXi8qdzLY1mMq9lMqYJmIV2OhN66Vt96meblxoE,3541
|
72
71
|
plain/preflight/security/csrf.py,sha256=EZy_DkVqc1kUmBA-UbNmhVsKhRINfmqgWSRlatKy5AA,1237
|
73
72
|
plain/preflight/urls.py,sha256=O4PQ_v205VA2872fQlhPfxaihDDRCsVp0ZVKQ92aX4k,3019
|
74
|
-
plain/runtime/README.md,sha256=
|
75
|
-
plain/runtime/__init__.py,sha256=
|
76
|
-
plain/runtime/global_settings.py,sha256=
|
77
|
-
plain/runtime/user_settings.py,sha256=
|
73
|
+
plain/runtime/README.md,sha256=wR0XrWAb4FGnOON2-O9ySSZCSMZmPkKGx-4DQXd_2h4,2209
|
74
|
+
plain/runtime/__init__.py,sha256=vIh77lL4e5CoQZz-HxvXeJL5329s8VfPFpNxx9rHgxs,1384
|
75
|
+
plain/runtime/global_settings.py,sha256=cVwGcdpDFf2OoYeGD3Lfv-mxGF48aKSGzZlLKwp8UKI,5566
|
76
|
+
plain/runtime/user_settings.py,sha256=JhxmCCOmEMk0QHh82l5iTpEie-UdZh13aXGsLhE5PBw,11255
|
78
77
|
plain/signals/README.md,sha256=cd3tKEgH-xc88CUWyDxl4-qv-HBXx8VT32BXVwA5azA,230
|
79
78
|
plain/signals/__init__.py,sha256=eAs0kLqptuP6I31dWXeAqRNji3svplpAV4Ez6ktjwXM,131
|
80
79
|
plain/signals/dispatch/__init__.py,sha256=FzEygqV9HsM6gopio7O2Oh_X230nA4d5Q9s0sUjMq0E,292
|
81
80
|
plain/signals/dispatch/dispatcher.py,sha256=VxSlqn9PCOTghPPJLOqZPs6FNQZfV2BJpMfFMSg6Dtc,11531
|
82
81
|
plain/signals/dispatch/license.txt,sha256=o9EhDhsC4Q5HbmD-IfNGVTEkXtNE33r5rIt3lleJ8gc,1727
|
83
|
-
plain/signing.py,sha256=
|
82
|
+
plain/signing.py,sha256=V6A6PTDYWekuwtQRI1iFD8dud5OHPZTv4EkeoZEHoXo,8737
|
84
83
|
plain/templates/README.md,sha256=VfA2HmrklG5weE1md85q9g84cWnMBEiXAynKzM7S1Sk,464
|
85
84
|
plain/templates/__init__.py,sha256=Jh1jit55UR4dRpklQ6qAN2ixzYZBVoDi0AOdfD4Nh4E,106
|
86
85
|
plain/templates/core.py,sha256=iw58EAmyyv8N5HDA-Sq4-fLgz_qx8v8WJfurgR116jw,625
|
@@ -129,7 +128,7 @@ plain/utils/safestring.py,sha256=SHGhpbX6FFDKSYOY9zYAgAQX0g0exzRba7dM2bJalWs,187
|
|
129
128
|
plain/utils/termcolors.py,sha256=78MimQMp4Etoh1X1lokOJ6ucxErHtg8z9rxeTtV5nhk,7394
|
130
129
|
plain/utils/text.py,sha256=QxhJsk_4VrNVUtwwo0DXGTMHJ1x_hrKOqJOxlPB33qc,16596
|
131
130
|
plain/utils/timesince.py,sha256=essdb0XWBKWmKtIprs-4rO0qKTtsFqZ0Fwn-RTDyhOc,4758
|
132
|
-
plain/utils/timezone.py,sha256=
|
131
|
+
plain/utils/timezone.py,sha256=AZ7lcmUjofUTfQUb08pHXu0u7TDuPJpMRB5lgvE4E0w,6212
|
133
132
|
plain/utils/tree.py,sha256=wdWzmfsgc26YDF2wxhAY3yVxXTixQYqYDKE9mL3L3ZY,4383
|
134
133
|
plain/validators.py,sha256=L9v9KtTe4iZhZVramZdKGf33R5Tt95FCdg2AJD2-2n0,19963
|
135
134
|
plain/views/README.md,sha256=qndsXKyNMnipPlLaAvgQeGxqXknNQwlFh31Yxk8rHp8,5994
|
@@ -143,8 +142,8 @@ plain/views/objects.py,sha256=9QBYyb8PgkRirXCQ8-Pms4_yMzP37dfeL30hWRYmtZg,7909
|
|
143
142
|
plain/views/redirect.py,sha256=KLnlktzK6ZNMTlaEiZpMKQMEP5zeTgGLJ9BIkIJfwBo,1733
|
144
143
|
plain/views/templates.py,sha256=nF9CcdhhjAyp3LB0RrSYnBaHpHzMfPSw719RCdcXk7o,2007
|
145
144
|
plain/wsgi.py,sha256=R6k5FiAElvGDApEbMPTT0MPqSD7n2e2Az5chQqJZU0I,236
|
146
|
-
plain-0.
|
147
|
-
plain-0.
|
148
|
-
plain-0.
|
149
|
-
plain-0.
|
150
|
-
plain-0.
|
145
|
+
plain-0.7.0.dist-info/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
|
146
|
+
plain-0.7.0.dist-info/METADATA,sha256=ILizFFak8iFUNe3VI8xf3UtKZsz2XB8jmg3zWm2QGS0,2716
|
147
|
+
plain-0.7.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
148
|
+
plain-0.7.0.dist-info/entry_points.txt,sha256=7O1RZTmMasKYB73bfqQcTwIhsXo7RjEIKv2WbtTtOIM,39
|
149
|
+
plain-0.7.0.dist-info/RECORD,,
|
plain/middleware/clickjacking.py
DELETED
@@ -1,52 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Clickjacking Protection Middleware.
|
3
|
-
|
4
|
-
This module provides a middleware that implements protection against a
|
5
|
-
malicious site loading resources from your site in a hidden frame.
|
6
|
-
"""
|
7
|
-
|
8
|
-
from plain.runtime import settings
|
9
|
-
|
10
|
-
|
11
|
-
class XFrameOptionsMiddleware:
|
12
|
-
"""
|
13
|
-
Set the X-Frame-Options HTTP header in HTTP responses.
|
14
|
-
|
15
|
-
Do not set the header if it's already set or if the response contains
|
16
|
-
a xframe_options_exempt value set to True.
|
17
|
-
|
18
|
-
By default, set the X-Frame-Options header to 'DENY', meaning the response
|
19
|
-
cannot be displayed in a frame, regardless of the site attempting to do so.
|
20
|
-
To enable the response to be loaded on a frame within the same site, set
|
21
|
-
X_FRAME_OPTIONS in your project's Plain settings to 'SAMEORIGIN'.
|
22
|
-
"""
|
23
|
-
|
24
|
-
def __init__(self, get_response):
|
25
|
-
self.get_response = get_response
|
26
|
-
|
27
|
-
def __call__(self, request):
|
28
|
-
response = self.get_response(request)
|
29
|
-
|
30
|
-
# Don't set it if it's already in the response
|
31
|
-
if response.get("X-Frame-Options") is not None:
|
32
|
-
return response
|
33
|
-
|
34
|
-
# Don't set it if they used @xframe_options_exempt
|
35
|
-
if getattr(response, "xframe_options_exempt", False):
|
36
|
-
return response
|
37
|
-
|
38
|
-
response.headers["X-Frame-Options"] = self.get_xframe_options_value(
|
39
|
-
request,
|
40
|
-
response,
|
41
|
-
)
|
42
|
-
return response
|
43
|
-
|
44
|
-
def get_xframe_options_value(self, request, response):
|
45
|
-
"""
|
46
|
-
Get the value to set for the X_FRAME_OPTIONS header. Use the value from
|
47
|
-
the X_FRAME_OPTIONS setting, or 'DENY' if not set.
|
48
|
-
|
49
|
-
This method can be overridden if needed, allowing it to vary based on
|
50
|
-
the request or response.
|
51
|
-
"""
|
52
|
-
return getattr(settings, "X_FRAME_OPTIONS", "DENY").upper()
|
File without changes
|
File without changes
|
File without changes
|