plain 0.6.0__py3-none-any.whl → 0.11.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 +5 -11
- plain/csrf/middleware.py +23 -40
- plain/http/request.py +5 -5
- plain/internal/handlers/base.py +13 -1
- plain/internal/middleware/headers.py +19 -0
- plain/internal/middleware/https.py +36 -0
- plain/{middleware/common.py → internal/middleware/slash.py} +2 -25
- plain/preflight/security/base.py +4 -19
- plain/preflight/security/csrf.py +1 -5
- plain/runtime/README.md +0 -3
- plain/runtime/__init__.py +11 -13
- plain/runtime/global_settings.py +33 -37
- plain/runtime/user_settings.py +1 -1
- plain/test/client.py +17 -17
- plain/views/base.py +4 -0
- {plain-0.6.0.dist-info → plain-0.11.0.dist-info}/METADATA +2 -2
- {plain-0.6.0.dist-info → plain-0.11.0.dist-info}/RECORD +21 -22
- {plain-0.6.0.dist-info → plain-0.11.0.dist-info}/WHEEL +1 -1
- plain/middleware/README.md +0 -3
- plain/middleware/gzip.py +0 -64
- plain/middleware/security.py +0 -31
- /plain/{middleware → internal/middleware}/__init__.py +0 -0
- {plain-0.6.0.dist-info → plain-0.11.0.dist-info}/LICENSE +0 -0
- {plain-0.6.0.dist-info → plain-0.11.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
|
|
@@ -250,7 +251,7 @@ def preflight_checks(package_label, deploy, fail_level, databases):
|
|
250
251
|
msg = header + body + footer
|
251
252
|
click.echo(msg, err=True)
|
252
253
|
else:
|
253
|
-
click.
|
254
|
+
click.secho("✔ Preflight check identified no issues.", err=True, fg="green")
|
254
255
|
|
255
256
|
|
256
257
|
@plain_cli.command()
|
@@ -286,17 +287,10 @@ def compile(keep_original, fingerprint, compress):
|
|
286
287
|
)
|
287
288
|
sys.exit(1)
|
288
289
|
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
click.secho("Compiling Tailwind CSS", bold=True)
|
293
|
-
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()()
|
294
293
|
print()
|
295
|
-
if result.returncode:
|
296
|
-
click.secho(
|
297
|
-
f"Error compiling Tailwind CSS (exit {result.returncode})", fg="red"
|
298
|
-
)
|
299
|
-
sys.exit(result.returncode)
|
300
294
|
|
301
295
|
# TODO also look in [tool.plain.compile.run]
|
302
296
|
|
plain/csrf/middleware.py
CHANGED
@@ -9,7 +9,7 @@ import string
|
|
9
9
|
from collections import defaultdict
|
10
10
|
from urllib.parse import urlparse
|
11
11
|
|
12
|
-
from plain.exceptions import DisallowedHost
|
12
|
+
from plain.exceptions import DisallowedHost
|
13
13
|
from plain.http import HttpHeaders, UnreadablePostError
|
14
14
|
from plain.logs import log_response
|
15
15
|
from plain.runtime import settings
|
@@ -242,44 +242,31 @@ class CsrfViewMiddleware:
|
|
242
242
|
If the CSRF_USE_SESSIONS setting is false, raises InvalidTokenFormat if
|
243
243
|
the request's secret has invalid characters or an invalid length.
|
244
244
|
"""
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
raise ImproperlyConfigured(
|
250
|
-
"CSRF_USE_SESSIONS is enabled, but request.session is not "
|
251
|
-
"set. SessionMiddleware must appear before CsrfViewMiddleware "
|
252
|
-
"in MIDDLEWARE."
|
253
|
-
)
|
245
|
+
try:
|
246
|
+
csrf_secret = request.COOKIES[settings.CSRF_COOKIE_NAME]
|
247
|
+
except KeyError:
|
248
|
+
csrf_secret = None
|
254
249
|
else:
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
csrf_secret = None
|
259
|
-
else:
|
260
|
-
# This can raise InvalidTokenFormat.
|
261
|
-
_check_token_format(csrf_secret)
|
250
|
+
# This can raise InvalidTokenFormat.
|
251
|
+
_check_token_format(csrf_secret)
|
252
|
+
|
262
253
|
if csrf_secret is None:
|
263
254
|
return None
|
264
255
|
return csrf_secret
|
265
256
|
|
266
257
|
def _set_csrf_cookie(self, request, response):
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
samesite=settings.CSRF_COOKIE_SAMESITE,
|
280
|
-
)
|
281
|
-
# Set the Vary header since content varies with the CSRF cookie.
|
282
|
-
patch_vary_headers(response, ("Cookie",))
|
258
|
+
response.set_cookie(
|
259
|
+
settings.CSRF_COOKIE_NAME,
|
260
|
+
request.META["CSRF_COOKIE"],
|
261
|
+
max_age=settings.CSRF_COOKIE_AGE,
|
262
|
+
domain=settings.CSRF_COOKIE_DOMAIN,
|
263
|
+
path=settings.CSRF_COOKIE_PATH,
|
264
|
+
secure=settings.CSRF_COOKIE_SECURE,
|
265
|
+
httponly=settings.CSRF_COOKIE_HTTPONLY,
|
266
|
+
samesite=settings.CSRF_COOKIE_SAMESITE,
|
267
|
+
)
|
268
|
+
# Set the Vary header since content varies with the CSRF cookie.
|
269
|
+
patch_vary_headers(response, ("Cookie",))
|
283
270
|
|
284
271
|
def _origin_verified(self, request):
|
285
272
|
request_origin = request.META["HTTP_ORIGIN"]
|
@@ -289,7 +276,7 @@ class CsrfViewMiddleware:
|
|
289
276
|
pass
|
290
277
|
else:
|
291
278
|
good_origin = "{}://{}".format(
|
292
|
-
"https" if request.
|
279
|
+
"https" if request.is_https() else "http",
|
293
280
|
good_host,
|
294
281
|
)
|
295
282
|
if request_origin == good_origin:
|
@@ -331,11 +318,7 @@ class CsrfViewMiddleware:
|
|
331
318
|
):
|
332
319
|
return
|
333
320
|
# Allow matching the configured cookie domain.
|
334
|
-
good_referer =
|
335
|
-
settings.SESSION_COOKIE_DOMAIN
|
336
|
-
if settings.CSRF_USE_SESSIONS
|
337
|
-
else settings.CSRF_COOKIE_DOMAIN
|
338
|
-
)
|
321
|
+
good_referer = settings.CSRF_COOKIE_DOMAIN
|
339
322
|
if good_referer is None:
|
340
323
|
# If no cookie domain is configured, allow matching the current
|
341
324
|
# host:port exactly if it's permitted by ALLOWED_HOSTS.
|
@@ -435,7 +418,7 @@ class CsrfViewMiddleware:
|
|
435
418
|
return self._reject(
|
436
419
|
request, REASON_BAD_ORIGIN % request.META["HTTP_ORIGIN"]
|
437
420
|
)
|
438
|
-
elif request.
|
421
|
+
elif request.is_https():
|
439
422
|
# If the Origin header wasn't provided, reject HTTPS requests if
|
440
423
|
# the Referer header doesn't match an allowed value.
|
441
424
|
#
|
plain/http/request.py
CHANGED
@@ -140,7 +140,7 @@ class HttpRequest:
|
|
140
140
|
# Reconstruct the host using the algorithm from PEP 333.
|
141
141
|
host = self.META["SERVER_NAME"]
|
142
142
|
server_port = self.get_port()
|
143
|
-
if server_port != ("443" if self.
|
143
|
+
if server_port != ("443" if self.is_https() else "80"):
|
144
144
|
host = f"{host}:{server_port}"
|
145
145
|
return host
|
146
146
|
|
@@ -267,12 +267,12 @@ class HttpRequest:
|
|
267
267
|
|
268
268
|
@property
|
269
269
|
def scheme(self):
|
270
|
-
if settings.
|
270
|
+
if settings.HTTPS_PROXY_HEADER:
|
271
271
|
try:
|
272
|
-
header, secure_value = settings.
|
272
|
+
header, secure_value = settings.HTTPS_PROXY_HEADER
|
273
273
|
except ValueError:
|
274
274
|
raise ImproperlyConfigured(
|
275
|
-
"The
|
275
|
+
"The HTTPS_PROXY_HEADER setting must be a tuple containing "
|
276
276
|
"two values."
|
277
277
|
)
|
278
278
|
header_value = self.META.get(header)
|
@@ -281,7 +281,7 @@ class HttpRequest:
|
|
281
281
|
return "https" if header_value.strip() == secure_value else "http"
|
282
282
|
return self._get_scheme()
|
283
283
|
|
284
|
-
def
|
284
|
+
def is_https(self):
|
285
285
|
return self.scheme == "https"
|
286
286
|
|
287
287
|
@property
|
plain/internal/handlers/base.py
CHANGED
@@ -13,6 +13,15 @@ from .exception import convert_exception_to_response
|
|
13
13
|
logger = logging.getLogger("plain.request")
|
14
14
|
|
15
15
|
|
16
|
+
# These middleware classes are always used by Plain.
|
17
|
+
BUILTIN_MIDDLEWARE = [
|
18
|
+
"plain.internal.middleware.headers.DefaultHeadersMiddleware",
|
19
|
+
"plain.internal.middleware.https.HttpsRedirectMiddleware",
|
20
|
+
"plain.internal.middleware.slash.RedirectSlashMiddleware",
|
21
|
+
"plain.csrf.middleware.CsrfViewMiddleware",
|
22
|
+
]
|
23
|
+
|
24
|
+
|
16
25
|
class BaseHandler:
|
17
26
|
_view_middleware = None
|
18
27
|
_middleware_chain = None
|
@@ -27,7 +36,10 @@ class BaseHandler:
|
|
27
36
|
|
28
37
|
get_response = self._get_response
|
29
38
|
handler = convert_exception_to_response(get_response)
|
30
|
-
|
39
|
+
|
40
|
+
middlewares = reversed(BUILTIN_MIDDLEWARE + settings.MIDDLEWARE)
|
41
|
+
|
42
|
+
for middleware_path in middlewares:
|
31
43
|
middleware = import_string(middleware_path)
|
32
44
|
mw_instance = middleware(handler)
|
33
45
|
|
@@ -0,0 +1,19 @@
|
|
1
|
+
from plain.runtime import settings
|
2
|
+
|
3
|
+
|
4
|
+
class DefaultHeadersMiddleware:
|
5
|
+
def __init__(self, get_response):
|
6
|
+
self.get_response = get_response
|
7
|
+
|
8
|
+
def __call__(self, request):
|
9
|
+
response = self.get_response(request)
|
10
|
+
|
11
|
+
for header, value in settings.DEFAULT_RESPONSE_HEADERS.items():
|
12
|
+
response.headers.setdefault(header, value)
|
13
|
+
|
14
|
+
# Add the Content-Length header to non-streaming responses if not
|
15
|
+
# already set.
|
16
|
+
if not response.streaming and not response.has_header("Content-Length"):
|
17
|
+
response.headers["Content-Length"] = str(len(response.content))
|
18
|
+
|
19
|
+
return response
|
@@ -0,0 +1,36 @@
|
|
1
|
+
import re
|
2
|
+
|
3
|
+
from plain.http import ResponsePermanentRedirect
|
4
|
+
from plain.runtime import settings
|
5
|
+
|
6
|
+
|
7
|
+
class HttpsRedirectMiddleware:
|
8
|
+
def __init__(self, get_response):
|
9
|
+
self.get_response = get_response
|
10
|
+
|
11
|
+
# Settings for https (compile regexes once)
|
12
|
+
self.https_redirect_enabled = settings.HTTPS_REDIRECT_ENABLED
|
13
|
+
self.https_redirect_host = settings.HTTPS_REDIRECT_HOST
|
14
|
+
self.https_redirect_exempt = [
|
15
|
+
re.compile(r) for r in settings.HTTPS_REDIRECT_EXEMPT
|
16
|
+
]
|
17
|
+
|
18
|
+
def __call__(self, request):
|
19
|
+
"""
|
20
|
+
Rewrite the URL based on settings.APPEND_SLASH
|
21
|
+
"""
|
22
|
+
|
23
|
+
if redirect_response := self.maybe_https_redirect(request):
|
24
|
+
return redirect_response
|
25
|
+
|
26
|
+
return self.get_response(request)
|
27
|
+
|
28
|
+
def maybe_https_redirect(self, request):
|
29
|
+
path = request.path.lstrip("/")
|
30
|
+
if (
|
31
|
+
self.https_redirect_enabled
|
32
|
+
and not request.is_https()
|
33
|
+
and not any(pattern.search(path) for pattern in self.https_redirect_exempt)
|
34
|
+
):
|
35
|
+
host = self.https_redirect_host or request.get_host()
|
36
|
+
return ResponsePermanentRedirect(f"https://{host}{request.get_full_path()}")
|
@@ -4,25 +4,7 @@ from plain.urls import is_valid_path
|
|
4
4
|
from plain.utils.http import escape_leading_slashes
|
5
5
|
|
6
6
|
|
7
|
-
class
|
8
|
-
"""
|
9
|
-
"Common" middleware for taking care of some basic operations:
|
10
|
-
|
11
|
-
- URL rewriting: Based on the APPEND_SLASH setting,
|
12
|
-
append missing slashes.
|
13
|
-
|
14
|
-
- If APPEND_SLASH is set and the initial URL doesn't end with a
|
15
|
-
slash, and it is not found in urlpatterns, form a new URL by
|
16
|
-
appending a slash at the end. If this new URL is found in
|
17
|
-
urlpatterns, return an HTTP redirect to this new URL; otherwise
|
18
|
-
process the initial URL as usual.
|
19
|
-
|
20
|
-
This behavior can be customized by subclassing CommonMiddleware and
|
21
|
-
overriding the response_redirect_class attribute.
|
22
|
-
"""
|
23
|
-
|
24
|
-
response_redirect_class = ResponsePermanentRedirect
|
25
|
-
|
7
|
+
class RedirectSlashMiddleware:
|
26
8
|
def __init__(self, get_response):
|
27
9
|
self.get_response = get_response
|
28
10
|
|
@@ -40,12 +22,7 @@ class CommonMiddleware:
|
|
40
22
|
# If the given URL is "Not Found", then check if we should redirect to
|
41
23
|
# a path with a slash appended.
|
42
24
|
if response.status_code == 404 and self.should_redirect_with_slash(request):
|
43
|
-
return
|
44
|
-
|
45
|
-
# Add the Content-Length header to non-streaming responses if not
|
46
|
-
# already set.
|
47
|
-
if not response.streaming and not response.has_header("Content-Length"):
|
48
|
-
response.headers["Content-Length"] = str(len(response.content))
|
25
|
+
return ResponsePermanentRedirect(self.get_full_path_with_slash(request))
|
49
26
|
|
50
27
|
return response
|
51
28
|
|
plain/preflight/security/base.py
CHANGED
@@ -16,17 +16,18 @@ SECRET_KEY_WARNING_MSG = (
|
|
16
16
|
f"vulnerable to attack."
|
17
17
|
)
|
18
18
|
|
19
|
+
# TODO
|
19
20
|
W001 = Warning(
|
20
|
-
"You do not have 'plain.middleware.
|
21
|
+
"You do not have 'plain.middleware.https.HttpsRedirectMiddleware' "
|
21
22
|
"in your MIDDLEWARE so the SECURE_HSTS_SECONDS, "
|
22
23
|
"SECURE_CONTENT_TYPE_NOSNIFF, SECURE_REFERRER_POLICY, "
|
23
|
-
"SECURE_CROSS_ORIGIN_OPENER_POLICY, and
|
24
|
+
"SECURE_CROSS_ORIGIN_OPENER_POLICY, and HTTPS_REDIRECT_ENABLED settings will "
|
24
25
|
"have no effect.",
|
25
26
|
id="security.W001",
|
26
27
|
)
|
27
28
|
|
28
29
|
W008 = Warning(
|
29
|
-
"Your
|
30
|
+
"Your HTTPS_REDIRECT_ENABLED setting is not set to True. "
|
30
31
|
"Unless your site should be available over both SSL and non-SSL "
|
31
32
|
"connections, you may want to either set this setting True "
|
32
33
|
"or configure a load balancer or reverse-proxy server "
|
@@ -52,22 +53,6 @@ W020 = Warning(
|
|
52
53
|
W025 = Warning(SECRET_KEY_WARNING_MSG, id="security.W025")
|
53
54
|
|
54
55
|
|
55
|
-
def _security_middleware():
|
56
|
-
return "plain.middleware.security.SecurityMiddleware" in settings.MIDDLEWARE
|
57
|
-
|
58
|
-
|
59
|
-
@register(deploy=True)
|
60
|
-
def check_security_middleware(package_configs, **kwargs):
|
61
|
-
passed_check = _security_middleware()
|
62
|
-
return [] if passed_check else [W001]
|
63
|
-
|
64
|
-
|
65
|
-
@register(deploy=True)
|
66
|
-
def check_ssl_redirect(package_configs, **kwargs):
|
67
|
-
passed_check = not _security_middleware() or settings.SECURE_SSL_REDIRECT is True
|
68
|
-
return [] if passed_check else [W008]
|
69
|
-
|
70
|
-
|
71
56
|
def _check_secret_key(secret_key):
|
72
57
|
return (
|
73
58
|
len(set(secret_key)) >= SECRET_KEY_MIN_UNIQUE_CHARACTERS
|
plain/preflight/security/csrf.py
CHANGED
@@ -32,9 +32,5 @@ def check_csrf_middleware(package_configs, **kwargs):
|
|
32
32
|
|
33
33
|
@register(deploy=True)
|
34
34
|
def check_csrf_cookie_secure(package_configs, **kwargs):
|
35
|
-
passed_check = (
|
36
|
-
settings.CSRF_USE_SESSIONS
|
37
|
-
or not _csrf_middleware()
|
38
|
-
or settings.CSRF_COOKIE_SECURE is True
|
39
|
-
)
|
35
|
+
passed_check = not _csrf_middleware() or settings.CSRF_COOKIE_SECURE is True
|
40
36
|
return [] if passed_check else [W016]
|
plain/runtime/README.md
CHANGED
@@ -54,10 +54,7 @@ SECRET_KEY = environ["SECRET_KEY"]
|
|
54
54
|
DEBUG = environ.get("DEBUG", "false").lower() in ("true", "1", "yes")
|
55
55
|
|
56
56
|
MIDDLEWARE = [
|
57
|
-
"plain.middleware.security.SecurityMiddleware",
|
58
57
|
"plain.sessions.middleware.SessionMiddleware",
|
59
|
-
"plain.middleware.common.CommonMiddleware",
|
60
|
-
"plain.csrf.middleware.CsrfViewMiddleware",
|
61
58
|
"plain.auth.middleware.AuthenticationMiddleware",
|
62
59
|
]
|
63
60
|
|
plain/runtime/__init__.py
CHANGED
@@ -1,10 +1,8 @@
|
|
1
1
|
import importlib.metadata
|
2
2
|
import sys
|
3
|
-
from
|
3
|
+
from importlib.metadata import entry_points
|
4
4
|
from pathlib import Path
|
5
5
|
|
6
|
-
from dotenv import load_dotenv
|
7
|
-
|
8
6
|
from .user_settings import Settings
|
9
7
|
|
10
8
|
try:
|
@@ -16,7 +14,6 @@ except importlib.metadata.PackageNotFoundError:
|
|
16
14
|
# Made available without setup or settings
|
17
15
|
APP_PATH = Path.cwd() / "app"
|
18
16
|
|
19
|
-
|
20
17
|
# from plain.runtime import settings
|
21
18
|
settings = Settings()
|
22
19
|
|
@@ -30,6 +27,11 @@ def setup():
|
|
30
27
|
Configure the settings (this happens as a side effect of accessing the
|
31
28
|
first setting), configure logging and populate the app registry.
|
32
29
|
"""
|
30
|
+
|
31
|
+
# Packages can hook into the setup process through an entrypoint.
|
32
|
+
for entry_point in entry_points().select(group="plain.setup"):
|
33
|
+
entry_point.load()()
|
34
|
+
|
33
35
|
from plain.logs import configure_logging
|
34
36
|
from plain.packages import packages
|
35
37
|
|
@@ -38,15 +40,11 @@ def setup():
|
|
38
40
|
"No app directory found. Are you sure you're in a Plain project?"
|
39
41
|
)
|
40
42
|
|
41
|
-
# Automatically put the
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
if app_env := environ.get("PLAIN_ENV", ""):
|
47
|
-
load_dotenv(f".env.{app_env}")
|
48
|
-
else:
|
49
|
-
load_dotenv(".env")
|
43
|
+
# Automatically put the project dir on the Python path
|
44
|
+
# which doesn't otherwise happen when you run `plain` commands.
|
45
|
+
# This makes "app.<module>" imports and relative imports work.
|
46
|
+
if APP_PATH.parent not in sys.path:
|
47
|
+
sys.path.insert(0, APP_PATH.parent.as_posix())
|
50
48
|
|
51
49
|
configure_logging(settings.LOGGING)
|
52
50
|
|
plain/runtime/global_settings.py
CHANGED
@@ -29,11 +29,41 @@ TIME_ZONE: str = "UTC"
|
|
29
29
|
DEFAULT_CHARSET = "utf-8"
|
30
30
|
|
31
31
|
# List of strings representing installed packages.
|
32
|
-
INSTALLED_PACKAGES: list = []
|
32
|
+
INSTALLED_PACKAGES: list[str] = []
|
33
33
|
|
34
34
|
# Whether to append trailing slashes to URLs.
|
35
35
|
APPEND_SLASH = True
|
36
36
|
|
37
|
+
# Default headers for all responses.
|
38
|
+
DEFAULT_RESPONSE_HEADERS = {
|
39
|
+
# "Content-Security-Policy": "default-src 'self'",
|
40
|
+
# https://hstspreload.org/
|
41
|
+
# "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
|
42
|
+
"Cross-Origin-Opener-Policy": "same-origin",
|
43
|
+
"Referrer-Policy": "same-origin",
|
44
|
+
"X-Content-Type-Options": "nosniff",
|
45
|
+
"X-Frame-Options": "DENY",
|
46
|
+
}
|
47
|
+
|
48
|
+
# Whether to redirect all non-HTTPS requests to HTTPS.
|
49
|
+
HTTPS_REDIRECT_ENABLED = True
|
50
|
+
HTTPS_REDIRECT_EXEMPT = []
|
51
|
+
HTTPS_REDIRECT_HOST = None
|
52
|
+
|
53
|
+
# If your Plain app is behind a proxy that sets a header to specify secure
|
54
|
+
# connections, AND that proxy ensures that user-submitted headers with the
|
55
|
+
# same name are ignored (so that people can't spoof it), set this value to
|
56
|
+
# a tuple of (header_name, header_value). For any requests that come in with
|
57
|
+
# that header/value, request.is_https() will return True.
|
58
|
+
# WARNING! Only set this if you fully understand what you're doing. Otherwise,
|
59
|
+
# you may be opening yourself up to a security risk.
|
60
|
+
HTTPS_PROXY_HEADER = None
|
61
|
+
|
62
|
+
# Whether to use the X-Forwarded-Host and X-Forwarded-Port headers
|
63
|
+
# when determining the host and port for the request.
|
64
|
+
USE_X_FORWARDED_HOST = False
|
65
|
+
USE_X_FORWARDED_PORT = False
|
66
|
+
|
37
67
|
# A secret key for this particular Plain installation. Used in secret-key
|
38
68
|
# hashing algorithms. Set this in your settings, or Plain will complain
|
39
69
|
# loudly.
|
@@ -43,7 +73,7 @@ SECRET_KEY: str
|
|
43
73
|
# secret key rotation.
|
44
74
|
SECRET_KEY_FALLBACKS: list[str] = []
|
45
75
|
|
46
|
-
ROOT_URLCONF = "urls"
|
76
|
+
ROOT_URLCONF = "app.urls"
|
47
77
|
|
48
78
|
# List of upload handler classes to be applied in order.
|
49
79
|
FILE_UPLOAD_HANDLERS = [
|
@@ -72,21 +102,9 @@ DATA_UPLOAD_MAX_NUMBER_FILES = 100
|
|
72
102
|
# (i.e. "/tmp" on *nix systems).
|
73
103
|
FILE_UPLOAD_TEMP_DIR = None
|
74
104
|
|
75
|
-
USE_X_FORWARDED_HOST = False
|
76
|
-
USE_X_FORWARDED_PORT = False
|
77
|
-
|
78
105
|
# User-defined overrides for error views by status code
|
79
106
|
HTTP_ERROR_VIEWS: dict[int] = {}
|
80
107
|
|
81
|
-
# If your Plain app is behind a proxy that sets a header to specify secure
|
82
|
-
# connections, AND that proxy ensures that user-submitted headers with the
|
83
|
-
# same name are ignored (so that people can't spoof it), set this value to
|
84
|
-
# a tuple of (header_name, header_value). For any requests that come in with
|
85
|
-
# that header/value, request.is_secure() will return True.
|
86
|
-
# WARNING! Only set this if you fully understand what you're doing. Otherwise,
|
87
|
-
# you may be opening yourself up to a security risk.
|
88
|
-
SECURE_PROXY_SSL_HEADER = None
|
89
|
-
|
90
108
|
##############
|
91
109
|
# MIDDLEWARE #
|
92
110
|
##############
|
@@ -94,11 +112,7 @@ SECURE_PROXY_SSL_HEADER = None
|
|
94
112
|
# List of middleware to use. Order is important; in the request phase, these
|
95
113
|
# middleware will be applied in the order given, and in the response
|
96
114
|
# phase the middleware will be applied in reverse order.
|
97
|
-
MIDDLEWARE = [
|
98
|
-
"plain.middleware.security.SecurityMiddleware",
|
99
|
-
"plain.middleware.common.CommonMiddleware",
|
100
|
-
"plain.csrf.middleware.CsrfViewMiddleware",
|
101
|
-
]
|
115
|
+
MIDDLEWARE: list[str] = []
|
102
116
|
|
103
117
|
###########
|
104
118
|
# SIGNING #
|
@@ -120,7 +134,6 @@ CSRF_COOKIE_HTTPONLY = False
|
|
120
134
|
CSRF_COOKIE_SAMESITE = "Lax"
|
121
135
|
CSRF_HEADER_NAME = "HTTP_X_CSRFTOKEN"
|
122
136
|
CSRF_TRUSTED_ORIGINS: list[str] = []
|
123
|
-
CSRF_USE_SESSIONS = False
|
124
137
|
|
125
138
|
###########
|
126
139
|
# LOGGING #
|
@@ -150,23 +163,6 @@ ASSETS_BASE_URL: str = ""
|
|
150
163
|
# message, but Plain will not stop you from e.g. running server.
|
151
164
|
SILENCED_PREFLIGHT_CHECKS = []
|
152
165
|
|
153
|
-
#######################
|
154
|
-
# SECURITY MIDDLEWARE #
|
155
|
-
#######################
|
156
|
-
SECURE_REDIRECT_EXEMPT = []
|
157
|
-
SECURE_SSL_HOST = None
|
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
|
-
}
|
169
|
-
|
170
166
|
#############
|
171
167
|
# Templates #
|
172
168
|
#############
|
plain/runtime/user_settings.py
CHANGED
@@ -41,7 +41,7 @@ class Settings:
|
|
41
41
|
|
42
42
|
# Determine the settings module
|
43
43
|
if self._settings_module is None:
|
44
|
-
self._settings_module = os.environ.get(ENVIRONMENT_VARIABLE, "settings")
|
44
|
+
self._settings_module = os.environ.get(ENVIRONMENT_VARIABLE, "app.settings")
|
45
45
|
|
46
46
|
# First load the global settings from plain
|
47
47
|
self._load_module_settings(
|
plain/test/client.py
CHANGED
@@ -379,7 +379,7 @@ class RequestFactory:
|
|
379
379
|
# Refs comment in `get_bytes_from_wsgi()`.
|
380
380
|
return path.decode("iso-8859-1")
|
381
381
|
|
382
|
-
def get(self, path, data=None, secure=
|
382
|
+
def get(self, path, data=None, secure=True, *, headers=None, **extra):
|
383
383
|
"""Construct a GET request."""
|
384
384
|
data = {} if data is None else data
|
385
385
|
return self.generic(
|
@@ -398,7 +398,7 @@ class RequestFactory:
|
|
398
398
|
path,
|
399
399
|
data=None,
|
400
400
|
content_type=MULTIPART_CONTENT,
|
401
|
-
secure=
|
401
|
+
secure=True,
|
402
402
|
*,
|
403
403
|
headers=None,
|
404
404
|
**extra,
|
@@ -417,7 +417,7 @@ class RequestFactory:
|
|
417
417
|
**extra,
|
418
418
|
)
|
419
419
|
|
420
|
-
def head(self, path, data=None, secure=
|
420
|
+
def head(self, path, data=None, secure=True, *, headers=None, **extra):
|
421
421
|
"""Construct a HEAD request."""
|
422
422
|
data = {} if data is None else data
|
423
423
|
return self.generic(
|
@@ -431,7 +431,7 @@ class RequestFactory:
|
|
431
431
|
},
|
432
432
|
)
|
433
433
|
|
434
|
-
def trace(self, path, secure=
|
434
|
+
def trace(self, path, secure=True, *, headers=None, **extra):
|
435
435
|
"""Construct a TRACE request."""
|
436
436
|
return self.generic("TRACE", path, secure=secure, headers=headers, **extra)
|
437
437
|
|
@@ -440,7 +440,7 @@ class RequestFactory:
|
|
440
440
|
path,
|
441
441
|
data="",
|
442
442
|
content_type="application/octet-stream",
|
443
|
-
secure=
|
443
|
+
secure=True,
|
444
444
|
*,
|
445
445
|
headers=None,
|
446
446
|
**extra,
|
@@ -455,7 +455,7 @@ class RequestFactory:
|
|
455
455
|
path,
|
456
456
|
data="",
|
457
457
|
content_type="application/octet-stream",
|
458
|
-
secure=
|
458
|
+
secure=True,
|
459
459
|
*,
|
460
460
|
headers=None,
|
461
461
|
**extra,
|
@@ -471,7 +471,7 @@ class RequestFactory:
|
|
471
471
|
path,
|
472
472
|
data="",
|
473
473
|
content_type="application/octet-stream",
|
474
|
-
secure=
|
474
|
+
secure=True,
|
475
475
|
*,
|
476
476
|
headers=None,
|
477
477
|
**extra,
|
@@ -487,7 +487,7 @@ class RequestFactory:
|
|
487
487
|
path,
|
488
488
|
data="",
|
489
489
|
content_type="application/octet-stream",
|
490
|
-
secure=
|
490
|
+
secure=True,
|
491
491
|
*,
|
492
492
|
headers=None,
|
493
493
|
**extra,
|
@@ -504,7 +504,7 @@ class RequestFactory:
|
|
504
504
|
path,
|
505
505
|
data="",
|
506
506
|
content_type="application/octet-stream",
|
507
|
-
secure=
|
507
|
+
secure=True,
|
508
508
|
*,
|
509
509
|
headers=None,
|
510
510
|
**extra,
|
@@ -704,7 +704,7 @@ class Client(ClientMixin, RequestFactory):
|
|
704
704
|
path,
|
705
705
|
data=None,
|
706
706
|
follow=False,
|
707
|
-
secure=
|
707
|
+
secure=True,
|
708
708
|
*,
|
709
709
|
headers=None,
|
710
710
|
**extra,
|
@@ -725,7 +725,7 @@ class Client(ClientMixin, RequestFactory):
|
|
725
725
|
data=None,
|
726
726
|
content_type=MULTIPART_CONTENT,
|
727
727
|
follow=False,
|
728
|
-
secure=
|
728
|
+
secure=True,
|
729
729
|
*,
|
730
730
|
headers=None,
|
731
731
|
**extra,
|
@@ -752,7 +752,7 @@ class Client(ClientMixin, RequestFactory):
|
|
752
752
|
path,
|
753
753
|
data=None,
|
754
754
|
follow=False,
|
755
|
-
secure=
|
755
|
+
secure=True,
|
756
756
|
*,
|
757
757
|
headers=None,
|
758
758
|
**extra,
|
@@ -775,7 +775,7 @@ class Client(ClientMixin, RequestFactory):
|
|
775
775
|
data="",
|
776
776
|
content_type="application/octet-stream",
|
777
777
|
follow=False,
|
778
|
-
secure=
|
778
|
+
secure=True,
|
779
779
|
*,
|
780
780
|
headers=None,
|
781
781
|
**extra,
|
@@ -803,7 +803,7 @@ class Client(ClientMixin, RequestFactory):
|
|
803
803
|
data="",
|
804
804
|
content_type="application/octet-stream",
|
805
805
|
follow=False,
|
806
|
-
secure=
|
806
|
+
secure=True,
|
807
807
|
*,
|
808
808
|
headers=None,
|
809
809
|
**extra,
|
@@ -831,7 +831,7 @@ class Client(ClientMixin, RequestFactory):
|
|
831
831
|
data="",
|
832
832
|
content_type="application/octet-stream",
|
833
833
|
follow=False,
|
834
|
-
secure=
|
834
|
+
secure=True,
|
835
835
|
*,
|
836
836
|
headers=None,
|
837
837
|
**extra,
|
@@ -859,7 +859,7 @@ class Client(ClientMixin, RequestFactory):
|
|
859
859
|
data="",
|
860
860
|
content_type="application/octet-stream",
|
861
861
|
follow=False,
|
862
|
-
secure=
|
862
|
+
secure=True,
|
863
863
|
*,
|
864
864
|
headers=None,
|
865
865
|
**extra,
|
@@ -886,7 +886,7 @@ class Client(ClientMixin, RequestFactory):
|
|
886
886
|
path,
|
887
887
|
data="",
|
888
888
|
follow=False,
|
889
|
-
secure=
|
889
|
+
secure=True,
|
890
890
|
*,
|
891
891
|
headers=None,
|
892
892
|
**extra,
|
plain/views/base.py
CHANGED
@@ -15,6 +15,10 @@ logger = logging.getLogger("plain.request")
|
|
15
15
|
|
16
16
|
|
17
17
|
class View:
|
18
|
+
request: HttpRequest
|
19
|
+
url_args: tuple
|
20
|
+
url_kwargs: dict
|
21
|
+
|
18
22
|
def __init__(self, *args, **kwargs) -> None:
|
19
23
|
# Views can customize their init, which receives
|
20
24
|
# the args and kwargs from as_view()
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: plain
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.11.0
|
4
4
|
Summary: A web framework for building products with Python.
|
5
5
|
Author: Dave Gaeddert
|
6
6
|
Author-email: dave.gaeddert@dropseed.dev
|
@@ -8,9 +8,9 @@ Requires-Python: >=3.11,<4.0
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
9
9
|
Classifier: Programming Language :: Python :: 3.11
|
10
10
|
Classifier: Programming Language :: Python :: 3.12
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
11
12
|
Requires-Dist: click (>=8.0.0)
|
12
13
|
Requires-Dist: jinja2 (>=3.1.2,<4.0.0)
|
13
|
-
Requires-Dist: python-dotenv (>=1.0.0,<2.0.0)
|
14
14
|
Description-Content-Type: text/markdown
|
15
15
|
|
16
16
|
<!-- This file is compiled from plain/plain/README.md. Do not edit this file directly. -->
|
@@ -9,13 +9,13 @@ 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=Ns6Yi3fhrvvBsE-HrhqtaFoLiegni7d61ypj-MrLjss,14834
|
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
|
16
16
|
plain/cli/startup.py,sha256=PJYA-tNWGia-QbTlT0e5HvC8C7yDSq8wkAkIxgfKkvw,680
|
17
17
|
plain/csrf/README.md,sha256=RXMWMtHmzf30gVVNOfj0kD4xlSqFIPgJh-n7dIciaEM,163
|
18
|
-
plain/csrf/middleware.py,sha256=
|
18
|
+
plain/csrf/middleware.py,sha256=MlDQ55B4eRXySbzauFNs8gKhgQy32yWspBfPI0a3PzA,17775
|
19
19
|
plain/csrf/views.py,sha256=YDgT451X16iUdCxpQ6rcHIy7nD0u7DAvCQl5-Mx5i9Y,219
|
20
20
|
plain/debug.py,sha256=fdrWy4RNQOuXo80_jgwthCkMZKjjaF9lDj3Kqln_gJk,604
|
21
21
|
plain/exceptions.py,sha256=tDS6l0epe_L9IlxpEdT2k2hWgEoAu8YBNIumNCtJ-WY,6333
|
@@ -29,7 +29,7 @@ plain/http/README.md,sha256=00zLFQ-FPjYXu3A8QsLhCCXxaT0ImvI5I-8xd3dp8WA,7
|
|
29
29
|
plain/http/__init__.py,sha256=DIsDRbBsCGa4qZgq-fUuQS0kkxfbTU_3KpIM9VvH04w,1067
|
30
30
|
plain/http/cookie.py,sha256=11FnSG3Plo6T3jZDbPoCw7SKh9ExdBio3pTmIO03URg,597
|
31
31
|
plain/http/multipartparser.py,sha256=Z2PFDuGucj_nFnQagwdxowJcZHqzCfDApkXl5yRlRe4,27325
|
32
|
-
plain/http/request.py,sha256=
|
32
|
+
plain/http/request.py,sha256=kOXN9uhgtgbd1IC25-oRupYlCofacE1jyoDZRlg2v5k,25990
|
33
33
|
plain/http/response.py,sha256=h43Gx4PVPGEf63EHHrABYtwYu-8Y9mgAebwiGt8qeLE,24074
|
34
34
|
plain/internal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
35
35
|
plain/internal/files/README.md,sha256=kMux-NU5qiH0o1K8IajYQT8VjrYl_jLk9LkGG_kGuSc,45
|
@@ -42,20 +42,19 @@ plain/internal/files/uploadedfile.py,sha256=JRB7T3quQjg-1y3l1ASPxywtSQZhaeMc45uF
|
|
42
42
|
plain/internal/files/uploadhandler.py,sha256=BZGQDHJMEUeBh9uJtxNVWQkFmHE7jzVTx9CLVt59Jqg,7197
|
43
43
|
plain/internal/files/utils.py,sha256=XrHAs2tMqmywURgz5C6-GSj6sr2R-MCERcWT8yzBp5k,2652
|
44
44
|
plain/internal/handlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
45
|
-
plain/internal/handlers/base.py,sha256=
|
45
|
+
plain/internal/handlers/base.py,sha256=tpTrVhC_gZKrIoTJmCWD3bIpucOCGVV1DTkF0W2HZPI,4883
|
46
46
|
plain/internal/handlers/exception.py,sha256=KUZSBzmzE6YSFxAZ336Mye_9vAPVIj9Av-w1SK5R4PA,4579
|
47
47
|
plain/internal/handlers/wsgi.py,sha256=WIZvXlEAOn8lxwDM_HpSP82-ePKVu-Tzgpe65KkXEMk,7538
|
48
|
+
plain/internal/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
49
|
+
plain/internal/middleware/headers.py,sha256=UnJWnlVVLD-10h7PB_QSYeREBzLo-TS3C-_ahmZ6w0I,636
|
50
|
+
plain/internal/middleware/https.py,sha256=XpuQK8HicYX1jNanQHqNgyQ9rqe4NLUOZO3ZzKdsP8k,1203
|
51
|
+
plain/internal/middleware/slash.py,sha256=LhQi5aUztE4kJnvRn75u8zaFvAVPPEl_Whu1gYWGs7g,2656
|
48
52
|
plain/json.py,sha256=McJdsbMT1sYwkGRG--f2NSZz0hVXPMix9x3nKaaak2o,1262
|
49
53
|
plain/logs/README.md,sha256=H6uVXdInYlasq0Z1WnhWnPmNwYQoZ1MSLPDQ4ZE7u4A,492
|
50
54
|
plain/logs/__init__.py,sha256=rASvo4qFBDIHfkACmGLNGa6lRGbG9PbNjW6FmBt95ys,168
|
51
55
|
plain/logs/configure.py,sha256=6mV7d1IxkDYT3VBz61qhIj0Esuy5l5QdQfsHaGCfI6w,1063
|
52
56
|
plain/logs/loggers.py,sha256=iz9SYcwP9w5QAuwpULl48SFkVyJuuMoQ_fdLgdCHpNg,2121
|
53
57
|
plain/logs/utils.py,sha256=9UzdCCQXJinGDs71Ngw297mlWkhgZStSd67ya4NOW98,1257
|
54
|
-
plain/middleware/README.md,sha256=MgiLHwAfP8ooBSlDi1JhTwIHMlwphOqAkeWglYRbe8s,52
|
55
|
-
plain/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
56
|
-
plain/middleware/common.py,sha256=-YySkYUyaRujYA5Yg7GRD3xFjlQOZpeJP1Stpt6pias,3631
|
57
|
-
plain/middleware/gzip.py,sha256=2NogLO6hPxVc3otxkhMDl7-r2Zw3vcIkAP29fx4j2eU,2383
|
58
|
-
plain/middleware/security.py,sha256=WZRn5F9qx33wFTqh4CkBEtHrTuyr7RCt4Gwq4W2mBgE,1043
|
59
58
|
plain/packages/README.md,sha256=Vq1Nw3mmEmZ2IriQavuVi4BjcQC2nb8k7YIbnm8QjIg,799
|
60
59
|
plain/packages/__init__.py,sha256=DnHN1wwHXiXib4Y9BV__x9WrbUaTovoTIxW-tVyScTU,106
|
61
60
|
plain/packages/config.py,sha256=6Vdf1TEQllZkkEvK0WK__zHJYT9nxmS3EyYrbuq0GkM,11201
|
@@ -67,13 +66,13 @@ plain/preflight/files.py,sha256=wbHCNgps7o1c1zQNBd8FDCaVaqX90UwuvLgEQ_DbUpY,510
|
|
67
66
|
plain/preflight/messages.py,sha256=u0oc7q7YmBlKYJRcF5SQpzncfOkEzDhZTcpyclQDfHg,2427
|
68
67
|
plain/preflight/registry.py,sha256=ZpxnZPIklXuT8xZVTxCUp_IER3zhd7DdfsmqIpAbLj4,2306
|
69
68
|
plain/preflight/security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
70
|
-
plain/preflight/security/base.py,sha256=
|
71
|
-
plain/preflight/security/csrf.py,sha256=
|
69
|
+
plain/preflight/security/base.py,sha256=nsv-g-bFr_188mkOQwC1ZDnyS0rE6eZED8xZT-FEM8M,3074
|
70
|
+
plain/preflight/security/csrf.py,sha256=8dKzs5kQwTTKeyfHbkrzdPk3OEoUN8mc-0xhSBo1KmM,1175
|
72
71
|
plain/preflight/urls.py,sha256=O4PQ_v205VA2872fQlhPfxaihDDRCsVp0ZVKQ92aX4k,3019
|
73
|
-
plain/runtime/README.md,sha256=
|
74
|
-
plain/runtime/__init__.py,sha256=
|
75
|
-
plain/runtime/global_settings.py,sha256=
|
76
|
-
plain/runtime/user_settings.py,sha256
|
72
|
+
plain/runtime/README.md,sha256=Q8VVO7JRGuYrDxzuYL6ptoilhclbecxKzpRXKgbWGkU,2061
|
73
|
+
plain/runtime/__init__.py,sha256=DH8TwKTGJhjviOy4yh_d051v8YGaAWMlFBPhK8ZuC9g,1499
|
74
|
+
plain/runtime/global_settings.py,sha256=_FaHDQjtLDoRCTv1-2EEA8GZWCiCVPJHIm_O7OxwrsU,5554
|
75
|
+
plain/runtime/user_settings.py,sha256=-1xXUggueuOF3YlgnLfeyG55CUvR3azOGWr2UkTOmfs,11259
|
77
76
|
plain/signals/README.md,sha256=cd3tKEgH-xc88CUWyDxl4-qv-HBXx8VT32BXVwA5azA,230
|
78
77
|
plain/signals/__init__.py,sha256=eAs0kLqptuP6I31dWXeAqRNji3svplpAV4Ez6ktjwXM,131
|
79
78
|
plain/signals/dispatch/__init__.py,sha256=FzEygqV9HsM6gopio7O2Oh_X230nA4d5Q9s0sUjMq0E,292
|
@@ -91,7 +90,7 @@ plain/templates/jinja/filters.py,sha256=3KJKKbxcv9dLzUDWPcaa88k3NU2m1GG3iMIgFhzX
|
|
91
90
|
plain/templates/jinja/globals.py,sha256=qhvQuikkRkOTpHSW5FwdsvoViJNlRgHq3-O7ZyeajsE,669
|
92
91
|
plain/test/README.md,sha256=Zso3Ir7a8vQerzKB6egjROQWkpveLAbscn7VTROPAiU,37
|
93
92
|
plain/test/__init__.py,sha256=rXe88Y602NP8DBnReSyXb7dUzKoWweLuT43j-qwOUl4,138
|
94
|
-
plain/test/client.py,sha256=
|
93
|
+
plain/test/client.py,sha256=470yny2wfLEebdVjQckBqC9pqyDkHy8e0EH-rlVjsAQ,31368
|
95
94
|
plain/urls/README.md,sha256=pWnCvgYkWN7rG7hSyBOtX4ZUP3iO7FhqM6lvwwYll6c,33
|
96
95
|
plain/urls/__init__.py,sha256=3UzwIufXjIks2K_X_Vms2MV19IqvyPLrXUeHU3WP47c,753
|
97
96
|
plain/urls/base.py,sha256=ECaOCEXs1ygKn4k1mt5XxSNPNlg5raJvx0aPaj7DFfE,3719
|
@@ -133,7 +132,7 @@ plain/utils/tree.py,sha256=wdWzmfsgc26YDF2wxhAY3yVxXTixQYqYDKE9mL3L3ZY,4383
|
|
133
132
|
plain/validators.py,sha256=L9v9KtTe4iZhZVramZdKGf33R5Tt95FCdg2AJD2-2n0,19963
|
134
133
|
plain/views/README.md,sha256=qndsXKyNMnipPlLaAvgQeGxqXknNQwlFh31Yxk8rHp8,5994
|
135
134
|
plain/views/__init__.py,sha256=a-N1nkklVohJTtz0yD1MMaS0g66HviEjsKydNVVjvVc,392
|
136
|
-
plain/views/base.py,sha256=
|
135
|
+
plain/views/base.py,sha256=wMkCAbr3XqXyP8dJr-O9atA1-N6K4-cTFflLhSYGOpY,3227
|
137
136
|
plain/views/csrf.py,sha256=gO9npd_Ut_LoYF_u7Qb_ZsPRfSeE3aTPG97XlMp4oEo,724
|
138
137
|
plain/views/errors.py,sha256=Y4oGX4Z6D2COKcDEfINvXE1acE8Ad15KwNNWPs5BCfc,967
|
139
138
|
plain/views/exceptions.py,sha256=b4euI49ZUKS9O8AGAcFfiDpstzkRAuuj_uYQXzWNHME,138
|
@@ -142,8 +141,8 @@ plain/views/objects.py,sha256=9QBYyb8PgkRirXCQ8-Pms4_yMzP37dfeL30hWRYmtZg,7909
|
|
142
141
|
plain/views/redirect.py,sha256=KLnlktzK6ZNMTlaEiZpMKQMEP5zeTgGLJ9BIkIJfwBo,1733
|
143
142
|
plain/views/templates.py,sha256=nF9CcdhhjAyp3LB0RrSYnBaHpHzMfPSw719RCdcXk7o,2007
|
144
143
|
plain/wsgi.py,sha256=R6k5FiAElvGDApEbMPTT0MPqSD7n2e2Az5chQqJZU0I,236
|
145
|
-
plain-0.
|
146
|
-
plain-0.
|
147
|
-
plain-0.
|
148
|
-
plain-0.
|
149
|
-
plain-0.
|
144
|
+
plain-0.11.0.dist-info/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
|
145
|
+
plain-0.11.0.dist-info/METADATA,sha256=tRnc7WP5pznuQMZQYEQXwyhi6cLywdRkcSikq-Vu9QI,2722
|
146
|
+
plain-0.11.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
147
|
+
plain-0.11.0.dist-info/entry_points.txt,sha256=7O1RZTmMasKYB73bfqQcTwIhsXo7RjEIKv2WbtTtOIM,39
|
148
|
+
plain-0.11.0.dist-info/RECORD,,
|
plain/middleware/README.md
DELETED
plain/middleware/gzip.py
DELETED
@@ -1,64 +0,0 @@
|
|
1
|
-
from plain.utils.cache import patch_vary_headers
|
2
|
-
from plain.utils.regex_helper import _lazy_re_compile
|
3
|
-
from plain.utils.text import compress_sequence, compress_string
|
4
|
-
|
5
|
-
re_accepts_gzip = _lazy_re_compile(r"\bgzip\b")
|
6
|
-
|
7
|
-
|
8
|
-
class GZipMiddleware:
|
9
|
-
"""
|
10
|
-
Compress content if the browser allows gzip compression.
|
11
|
-
Set the Vary header accordingly, so that caches will base their storage
|
12
|
-
on the Accept-Encoding header.
|
13
|
-
"""
|
14
|
-
|
15
|
-
max_random_bytes = 100
|
16
|
-
|
17
|
-
def __init__(self, get_response):
|
18
|
-
self.get_response = get_response
|
19
|
-
|
20
|
-
def __call__(self, request):
|
21
|
-
response = self.get_response(request)
|
22
|
-
|
23
|
-
# It's not worth attempting to compress really short responses.
|
24
|
-
if not response.streaming and len(response.content) < 200:
|
25
|
-
return response
|
26
|
-
|
27
|
-
# Avoid gzipping if we've already got a content-encoding.
|
28
|
-
if response.has_header("Content-Encoding"):
|
29
|
-
return response
|
30
|
-
|
31
|
-
patch_vary_headers(response, ("Accept-Encoding",))
|
32
|
-
|
33
|
-
ae = request.META.get("HTTP_ACCEPT_ENCODING", "")
|
34
|
-
if not re_accepts_gzip.search(ae):
|
35
|
-
return response
|
36
|
-
|
37
|
-
if response.streaming:
|
38
|
-
response.streaming_content = compress_sequence(
|
39
|
-
response.streaming_content,
|
40
|
-
max_random_bytes=self.max_random_bytes,
|
41
|
-
)
|
42
|
-
# Delete the `Content-Length` header for streaming content, because
|
43
|
-
# we won't know the compressed size until we stream it.
|
44
|
-
del response.headers["Content-Length"]
|
45
|
-
else:
|
46
|
-
# Return the compressed content only if it's actually shorter.
|
47
|
-
compressed_content = compress_string(
|
48
|
-
response.content,
|
49
|
-
max_random_bytes=self.max_random_bytes,
|
50
|
-
)
|
51
|
-
if len(compressed_content) >= len(response.content):
|
52
|
-
return response
|
53
|
-
response.content = compressed_content
|
54
|
-
response.headers["Content-Length"] = str(len(response.content))
|
55
|
-
|
56
|
-
# If there is a strong ETag, make it weak to fulfill the requirements
|
57
|
-
# of RFC 9110 Section 8.8.1 while also allowing conditional request
|
58
|
-
# matches on ETags.
|
59
|
-
etag = response.get("ETag")
|
60
|
-
if etag and etag.startswith('"'):
|
61
|
-
response.headers["ETag"] = "W/" + etag
|
62
|
-
response.headers["Content-Encoding"] = "gzip"
|
63
|
-
|
64
|
-
return response
|
plain/middleware/security.py
DELETED
@@ -1,31 +0,0 @@
|
|
1
|
-
import re
|
2
|
-
|
3
|
-
from plain.http import ResponsePermanentRedirect
|
4
|
-
from plain.runtime import settings
|
5
|
-
|
6
|
-
|
7
|
-
class SecurityMiddleware:
|
8
|
-
def __init__(self, get_response):
|
9
|
-
self.get_response = get_response
|
10
|
-
self.redirect = settings.SECURE_SSL_REDIRECT
|
11
|
-
self.redirect_host = settings.SECURE_SSL_HOST
|
12
|
-
self.redirect_exempt = [re.compile(r) for r in settings.SECURE_REDIRECT_EXEMPT]
|
13
|
-
|
14
|
-
self.default_headers = settings.SECURE_DEFAULT_HEADERS
|
15
|
-
|
16
|
-
def __call__(self, request):
|
17
|
-
path = request.path.lstrip("/")
|
18
|
-
if (
|
19
|
-
self.redirect
|
20
|
-
and not request.is_secure()
|
21
|
-
and not any(pattern.search(path) for pattern in self.redirect_exempt)
|
22
|
-
):
|
23
|
-
host = self.redirect_host or request.get_host()
|
24
|
-
return ResponsePermanentRedirect(f"https://{host}{request.get_full_path()}")
|
25
|
-
|
26
|
-
response = self.get_response(request)
|
27
|
-
|
28
|
-
for header, value in self.default_headers.items():
|
29
|
-
response.headers.setdefault(header, value)
|
30
|
-
|
31
|
-
return response
|
File without changes
|
File without changes
|
File without changes
|