plain 0.16.1__tar.gz → 0.18.0__tar.gz
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-0.16.1 → plain-0.18.0}/PKG-INFO +1 -1
- {plain-0.16.1 → plain-0.18.0}/plain/csrf/middleware.py +5 -9
- plain-0.18.0/plain/csrf/views.py +31 -0
- {plain-0.16.1 → plain-0.18.0}/plain/http/response.py +5 -30
- {plain-0.16.1 → plain-0.18.0}/plain/internal/handlers/base.py +1 -20
- {plain-0.16.1 → plain-0.18.0}/plain/internal/handlers/wsgi.py +1 -1
- {plain-0.16.1 → plain-0.18.0}/plain/internal/middleware/headers.py +1 -1
- {plain-0.16.1 → plain-0.18.0}/plain/test/client.py +3 -3
- {plain-0.16.1 → plain-0.18.0}/plain/utils/cache.py +5 -5
- plain-0.18.0/plain/utils/decorators.py +10 -0
- {plain-0.16.1 → plain-0.18.0}/plain/views/base.py +14 -15
- plain-0.18.0/plain/views/csrf.py +4 -0
- {plain-0.16.1 → plain-0.18.0}/pyproject.toml +1 -1
- {plain-0.16.1 → plain-0.18.0}/uv.lock +1 -1
- plain-0.16.1/plain/csrf/views.py +0 -10
- plain-0.16.1/plain/utils/decorators.py +0 -90
- plain-0.16.1/plain/views/csrf.py +0 -24
- {plain-0.16.1 → plain-0.18.0}/.gitignore +0 -0
- {plain-0.16.1 → plain-0.18.0}/LICENSE +0 -0
- {plain-0.16.1 → plain-0.18.0}/README.md +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/README.md +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/__main__.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/assets/README.md +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/assets/__init__.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/assets/compile.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/assets/finders.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/assets/fingerprints.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/assets/urls.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/assets/views.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/cli/README.md +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/cli/__init__.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/cli/cli.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/cli/formatting.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/cli/packages.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/cli/print.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/cli/startup.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/csrf/README.md +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/debug.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/exceptions.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/forms/README.md +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/forms/__init__.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/forms/boundfield.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/forms/exceptions.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/forms/fields.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/forms/forms.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/http/README.md +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/http/__init__.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/http/cookie.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/http/multipartparser.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/http/request.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/internal/__init__.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/internal/files/README.md +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/internal/files/__init__.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/internal/files/base.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/internal/files/locks.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/internal/files/move.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/internal/files/temp.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/internal/files/uploadedfile.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/internal/files/uploadhandler.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/internal/files/utils.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/internal/handlers/__init__.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/internal/handlers/exception.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/internal/middleware/__init__.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/internal/middleware/https.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/internal/middleware/slash.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/json.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/logs/README.md +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/logs/__init__.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/logs/configure.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/logs/loggers.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/logs/utils.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/packages/README.md +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/packages/__init__.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/packages/config.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/packages/registry.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/paginator.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/preflight/README.md +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/preflight/__init__.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/preflight/files.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/preflight/messages.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/preflight/registry.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/preflight/security.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/preflight/urls.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/runtime/README.md +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/runtime/__init__.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/runtime/global_settings.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/runtime/user_settings.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/signals/README.md +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/signals/__init__.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/signals/dispatch/__init__.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/signals/dispatch/dispatcher.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/signals/dispatch/license.txt +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/signing.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/templates/README.md +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/templates/__init__.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/templates/core.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/templates/jinja/README.md +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/templates/jinja/__init__.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/templates/jinja/environments.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/templates/jinja/extensions.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/templates/jinja/filters.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/templates/jinja/globals.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/test/README.md +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/test/__init__.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/urls/README.md +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/urls/__init__.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/urls/base.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/urls/conf.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/urls/converters.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/urls/exceptions.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/urls/resolvers.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/utils/README.md +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/utils/__init__.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/utils/_os.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/utils/connection.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/utils/crypto.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/utils/datastructures.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/utils/dateformat.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/utils/dateparse.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/utils/dates.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/utils/deconstruct.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/utils/deprecation.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/utils/duration.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/utils/email.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/utils/encoding.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/utils/functional.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/utils/hashable.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/utils/html.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/utils/http.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/utils/inspect.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/utils/ipv6.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/utils/itercompat.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/utils/module_loading.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/utils/regex_helper.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/utils/safestring.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/utils/text.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/utils/timesince.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/utils/timezone.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/utils/tree.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/validators.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/views/README.md +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/views/__init__.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/views/errors.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/views/exceptions.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/views/forms.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/views/objects.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/views/redirect.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/views/templates.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/plain/wsgi.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/tests/.bolt/assets_collected/assets.json +0 -0
- {plain-0.16.1 → plain-0.18.0}/tests/.gitignore +0 -0
- {plain-0.16.1 → plain-0.18.0}/tests/app/.gitignore +0 -0
- {plain-0.16.1 → plain-0.18.0}/tests/app/settings.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/tests/app/test/__init__.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/tests/app/test/default_settings.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/tests/app/urls.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/tests/conftest.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/tests/test_cli.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/tests/test_runtime.py +0 -0
- {plain-0.16.1 → plain-0.18.0}/tests/test_wsgi.py +0 -0
@@ -180,6 +180,9 @@ class CsrfViewMiddleware:
|
|
180
180
|
# saved anyways.
|
181
181
|
request.META["CSRF_COOKIE"] = csrf_secret
|
182
182
|
|
183
|
+
if csrf_response := self._get_csrf_response(request):
|
184
|
+
return csrf_response
|
185
|
+
|
183
186
|
response = self.get_response(request)
|
184
187
|
|
185
188
|
if request.META.get("CSRF_COOKIE_NEEDS_UPDATE"):
|
@@ -395,23 +398,16 @@ class CsrfViewMiddleware:
|
|
395
398
|
reason = self._bad_token_message("incorrect", token_source)
|
396
399
|
raise RejectRequest(reason)
|
397
400
|
|
398
|
-
def
|
401
|
+
def _get_csrf_response(self, request):
|
399
402
|
# Wait until request.META["CSRF_COOKIE"] has been manipulated before
|
400
403
|
# bailing out, so that get_token still works
|
401
|
-
if getattr(
|
404
|
+
if getattr(request, "csrf_exempt", True):
|
402
405
|
return None
|
403
406
|
|
404
407
|
# Assume that anything not defined as 'safe' by RFC 9110 needs protection
|
405
408
|
if request.method in ("GET", "HEAD", "OPTIONS", "TRACE"):
|
406
409
|
return None
|
407
410
|
|
408
|
-
if getattr(request, "_dont_enforce_csrf_checks", False):
|
409
|
-
# Mechanism to turn off CSRF checks for test suite. It comes after
|
410
|
-
# the creation of CSRF cookies, so that everything else continues
|
411
|
-
# to work exactly the same (e.g. cookies are sent, etc.), but
|
412
|
-
# before any branches that call the _reject method.
|
413
|
-
return None
|
414
|
-
|
415
411
|
# Reject the request if the Origin header doesn't match an allowed
|
416
412
|
# value.
|
417
413
|
if "HTTP_ORIGIN" in request.META:
|
@@ -0,0 +1,31 @@
|
|
1
|
+
from plain.views import TemplateView
|
2
|
+
|
3
|
+
|
4
|
+
class CsrfFailureView(TemplateView):
|
5
|
+
template_name = "403.html"
|
6
|
+
|
7
|
+
def get_response(self):
|
8
|
+
response = super().get_response()
|
9
|
+
response.status_code = 403
|
10
|
+
return response
|
11
|
+
|
12
|
+
def post(self):
|
13
|
+
return self.get()
|
14
|
+
|
15
|
+
def put(self):
|
16
|
+
return self.get()
|
17
|
+
|
18
|
+
def patch(self):
|
19
|
+
return self.get()
|
20
|
+
|
21
|
+
def delete(self):
|
22
|
+
return self.get()
|
23
|
+
|
24
|
+
def head(self):
|
25
|
+
return self.get()
|
26
|
+
|
27
|
+
def options(self):
|
28
|
+
return self.get()
|
29
|
+
|
30
|
+
def trace(self):
|
31
|
+
return self.get()
|
@@ -187,27 +187,6 @@ class ResponseBase:
|
|
187
187
|
else ""
|
188
188
|
)
|
189
189
|
|
190
|
-
def __setitem__(self, header, value):
|
191
|
-
self.headers[header] = value
|
192
|
-
|
193
|
-
def __delitem__(self, header):
|
194
|
-
del self.headers[header]
|
195
|
-
|
196
|
-
def __getitem__(self, header):
|
197
|
-
return self.headers[header]
|
198
|
-
|
199
|
-
def has_header(self, header):
|
200
|
-
"""Case-insensitive check for a header."""
|
201
|
-
return header in self.headers
|
202
|
-
|
203
|
-
__contains__ = has_header
|
204
|
-
|
205
|
-
def items(self):
|
206
|
-
return self.headers.items()
|
207
|
-
|
208
|
-
def get(self, header, alternate=None):
|
209
|
-
return self.headers.get(header, alternate)
|
210
|
-
|
211
190
|
def set_cookie(
|
212
191
|
self,
|
213
192
|
key,
|
@@ -272,10 +251,6 @@ class ResponseBase:
|
|
272
251
|
raise ValueError('samesite must be "lax", "none", or "strict".')
|
273
252
|
self.cookies[key]["samesite"] = samesite
|
274
253
|
|
275
|
-
def setdefault(self, key, value):
|
276
|
-
"""Set a header unless it has already been set."""
|
277
|
-
self.headers.setdefault(key, value)
|
278
|
-
|
279
254
|
def set_signed_cookie(self, key, value, salt="", **kwargs):
|
280
255
|
value = signing.get_cookie_signer(salt=key + salt).sign(value)
|
281
256
|
return self.set_cookie(key, value, **kwargs)
|
@@ -587,14 +562,14 @@ class ResponseRedirectBase(Response):
|
|
587
562
|
|
588
563
|
def __init__(self, redirect_to, *args, **kwargs):
|
589
564
|
super().__init__(*args, **kwargs)
|
590
|
-
self["Location"] = iri_to_uri(redirect_to)
|
565
|
+
self.headers["Location"] = iri_to_uri(redirect_to)
|
591
566
|
parsed = urlparse(str(redirect_to))
|
592
567
|
if parsed.scheme and parsed.scheme not in self.allowed_schemes:
|
593
568
|
raise DisallowedRedirect(
|
594
569
|
f"Unsafe redirect to URL with protocol '{parsed.scheme}'"
|
595
570
|
)
|
596
571
|
|
597
|
-
url = property(lambda self: self["Location"])
|
572
|
+
url = property(lambda self: self.headers["Location"])
|
598
573
|
|
599
574
|
def __repr__(self):
|
600
575
|
return (
|
@@ -627,7 +602,7 @@ class ResponseNotModified(Response):
|
|
627
602
|
|
628
603
|
def __init__(self, *args, **kwargs):
|
629
604
|
super().__init__(*args, **kwargs)
|
630
|
-
del self["content-type"]
|
605
|
+
del self.headers["content-type"]
|
631
606
|
|
632
607
|
@Response.content.setter
|
633
608
|
def content(self, value):
|
@@ -663,14 +638,14 @@ class ResponseNotAllowed(Response):
|
|
663
638
|
|
664
639
|
def __init__(self, permitted_methods, *args, **kwargs):
|
665
640
|
super().__init__(*args, **kwargs)
|
666
|
-
self["Allow"] = ", ".join(permitted_methods)
|
641
|
+
self.headers["Allow"] = ", ".join(permitted_methods)
|
667
642
|
|
668
643
|
def __repr__(self):
|
669
644
|
return "<%(cls)s [%(methods)s] status_code=%(status_code)d%(content_type)s>" % { # noqa: UP031
|
670
645
|
"cls": self.__class__.__name__,
|
671
646
|
"status_code": self.status_code,
|
672
647
|
"content_type": self._content_type_for_repr,
|
673
|
-
"methods": self["Allow"],
|
648
|
+
"methods": self.headers["Allow"],
|
674
649
|
}
|
675
650
|
|
676
651
|
|
@@ -23,7 +23,6 @@ BUILTIN_MIDDLEWARE = [
|
|
23
23
|
|
24
24
|
|
25
25
|
class BaseHandler:
|
26
|
-
_view_middleware = None
|
27
26
|
_middleware_chain = None
|
28
27
|
|
29
28
|
def load_middleware(self):
|
@@ -32,8 +31,6 @@ class BaseHandler:
|
|
32
31
|
|
33
32
|
Must be called after the environment is fixed (see __call__ in subclasses).
|
34
33
|
"""
|
35
|
-
self._view_middleware = []
|
36
|
-
|
37
34
|
get_response = self._get_response
|
38
35
|
handler = convert_exception_to_response(get_response)
|
39
36
|
|
@@ -48,12 +45,6 @@ class BaseHandler:
|
|
48
45
|
f"Middleware factory {middleware_path} returned None."
|
49
46
|
)
|
50
47
|
|
51
|
-
if hasattr(mw_instance, "process_view"):
|
52
|
-
self._view_middleware.insert(
|
53
|
-
0,
|
54
|
-
mw_instance.process_view,
|
55
|
-
)
|
56
|
-
|
57
48
|
handler = convert_exception_to_response(mw_instance)
|
58
49
|
|
59
50
|
# We only assign to this when initialization is complete as it is used
|
@@ -82,19 +73,9 @@ class BaseHandler:
|
|
82
73
|
template_response middleware. This method is everything that happens
|
83
74
|
inside the request/response middleware.
|
84
75
|
"""
|
85
|
-
response = None
|
86
76
|
callback, callback_args, callback_kwargs = self.resolve_request(request)
|
87
77
|
|
88
|
-
|
89
|
-
for middleware_method in self._view_middleware:
|
90
|
-
response = middleware_method(
|
91
|
-
request, callback, callback_args, callback_kwargs
|
92
|
-
)
|
93
|
-
if response:
|
94
|
-
break
|
95
|
-
|
96
|
-
if response is None:
|
97
|
-
response = callback(request, *callback_args, **callback_kwargs)
|
78
|
+
response = callback(request, *callback_args, **callback_kwargs)
|
98
79
|
|
99
80
|
# Complain if the view returned None (a common error).
|
100
81
|
self.check_response(response, callback)
|
@@ -140,7 +140,7 @@ class WSGIHandler(base.BaseHandler):
|
|
140
140
|
|
141
141
|
status = "%d %s" % (response.status_code, response.reason_phrase) # noqa: UP031
|
142
142
|
response_headers = [
|
143
|
-
*response.items(),
|
143
|
+
*response.headers.items(),
|
144
144
|
*(("Set-Cookie", c.output(header="")) for c in response.cookies.values()),
|
145
145
|
]
|
146
146
|
start_response(status, response_headers)
|
@@ -19,7 +19,7 @@ class DefaultHeadersMiddleware:
|
|
19
19
|
|
20
20
|
# Add the Content-Length header to non-streaming responses if not
|
21
21
|
# already set.
|
22
|
-
if not response.streaming and
|
22
|
+
if not response.streaming and "Content-Length" not in response.headers:
|
23
23
|
response.headers["Content-Length"] = str(len(response.content))
|
24
24
|
|
25
25
|
return response
|
@@ -177,7 +177,7 @@ class ClientHandler(BaseHandler):
|
|
177
177
|
# CsrfViewMiddleware. This makes life easier, and is probably
|
178
178
|
# required for backwards compatibility with external tests against
|
179
179
|
# admin views.
|
180
|
-
request.
|
180
|
+
request.csrf_exempt = not self.enforce_csrf_checks
|
181
181
|
|
182
182
|
# Request goes through middleware.
|
183
183
|
response = self.get_response(request)
|
@@ -615,10 +615,10 @@ class ClientMixin:
|
|
615
615
|
|
616
616
|
def _parse_json(self, response, **extra):
|
617
617
|
if not hasattr(response, "_json"):
|
618
|
-
if not JSON_CONTENT_TYPE_RE.match(response.get("Content-Type")):
|
618
|
+
if not JSON_CONTENT_TYPE_RE.match(response.headers.get("Content-Type")):
|
619
619
|
raise ValueError(
|
620
620
|
'Content-Type header is "{}", not "application/json"'.format(
|
621
|
-
response.get("Content-Type")
|
621
|
+
response.headers.get("Content-Type")
|
622
622
|
)
|
623
623
|
)
|
624
624
|
response._json = json.loads(
|
@@ -55,7 +55,7 @@ def patch_cache_control(response, **kwargs):
|
|
55
55
|
return f"{t[0]}={t[1]}"
|
56
56
|
|
57
57
|
cc = defaultdict(set)
|
58
|
-
if response.get("Cache-Control"):
|
58
|
+
if response.headers.get("Cache-Control"):
|
59
59
|
for field in cc_delim_re.split(response.headers["Cache-Control"]):
|
60
60
|
directive, value = dictitem(field)
|
61
61
|
if directive == "no-cache":
|
@@ -102,7 +102,7 @@ def get_max_age(response):
|
|
102
102
|
Return the max-age from the response Cache-Control header as an integer,
|
103
103
|
or None if it wasn't found or wasn't an integer.
|
104
104
|
"""
|
105
|
-
if
|
105
|
+
if "Cache-Control" not in response.headers:
|
106
106
|
return
|
107
107
|
cc = dict(
|
108
108
|
_to_tuple(el) for el in cc_delim_re.split(response.headers["Cache-Control"])
|
@@ -146,7 +146,7 @@ def _not_modified(request, response=None):
|
|
146
146
|
"Last-Modified",
|
147
147
|
"Vary",
|
148
148
|
):
|
149
|
-
if header in response:
|
149
|
+
if header in response.headers:
|
150
150
|
new_response.headers[header] = response.headers[header]
|
151
151
|
|
152
152
|
# Preserve cookies as per the cookie specification: "If a proxy server
|
@@ -278,7 +278,7 @@ def patch_response_headers(response, cache_timeout=None):
|
|
278
278
|
cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS
|
279
279
|
if cache_timeout < 0:
|
280
280
|
cache_timeout = 0 # Can't have max-age negative
|
281
|
-
if not response.
|
281
|
+
if "Expires" not in response.headers:
|
282
282
|
response.headers["Expires"] = http_date(time.time() + cache_timeout)
|
283
283
|
patch_cache_control(response, max_age=cache_timeout)
|
284
284
|
|
@@ -303,7 +303,7 @@ def patch_vary_headers(response, newheaders):
|
|
303
303
|
# Note that we need to keep the original order intact, because cache
|
304
304
|
# implementations may rely on the order of the Vary contents in, say,
|
305
305
|
# computing an MD5 hash.
|
306
|
-
if
|
306
|
+
if "Vary" in response.headers:
|
307
307
|
vary_headers = cc_delim_re.split(response.headers["Vary"])
|
308
308
|
else:
|
309
309
|
vary_headers = []
|
@@ -0,0 +1,10 @@
|
|
1
|
+
"Functions that help with dynamically creating decorators for views."
|
2
|
+
|
3
|
+
|
4
|
+
class classonlymethod(classmethod):
|
5
|
+
def __get__(self, instance, cls=None):
|
6
|
+
if instance is not None:
|
7
|
+
raise AttributeError(
|
8
|
+
"This method is available only on the class, not on instances."
|
9
|
+
)
|
10
|
+
return super().__get__(instance, cls)
|
@@ -19,6 +19,18 @@ class View:
|
|
19
19
|
url_args: tuple
|
20
20
|
url_kwargs: dict
|
21
21
|
|
22
|
+
# By default, any of these are allowed if a method is defined for it.
|
23
|
+
allowed_http_methods = [
|
24
|
+
"get",
|
25
|
+
"post",
|
26
|
+
"put",
|
27
|
+
"patch",
|
28
|
+
"delete",
|
29
|
+
"head",
|
30
|
+
"options",
|
31
|
+
"trace",
|
32
|
+
]
|
33
|
+
|
22
34
|
def __init__(self, *args, **kwargs) -> None:
|
23
35
|
# Views can customize their init, which receives
|
24
36
|
# the args and kwargs from as_view()
|
@@ -42,9 +54,6 @@ class View:
|
|
42
54
|
except ResponseException as e:
|
43
55
|
return e.response
|
44
56
|
|
45
|
-
# Copy possible attributes set by decorators, e.g. @csrf_exempt, from
|
46
|
-
# the dispatch method.
|
47
|
-
view.__dict__.update(cls.get_response.__dict__)
|
48
57
|
view.view_class = cls
|
49
58
|
|
50
59
|
return view
|
@@ -57,7 +66,7 @@ class View:
|
|
57
66
|
|
58
67
|
handler = getattr(self, self.request.method.lower(), None)
|
59
68
|
|
60
|
-
if not handler:
|
69
|
+
if not handler or self.request.method.lower() not in self.allowed_http_methods:
|
61
70
|
logger.warning(
|
62
71
|
"Method Not Allowed (%s): %s",
|
63
72
|
self.request.method,
|
@@ -100,14 +109,4 @@ class View:
|
|
100
109
|
return response
|
101
110
|
|
102
111
|
def _allowed_methods(self) -> list[str]:
|
103
|
-
|
104
|
-
"get",
|
105
|
-
"post",
|
106
|
-
"put",
|
107
|
-
"patch",
|
108
|
-
"delete",
|
109
|
-
"head",
|
110
|
-
"options",
|
111
|
-
"trace",
|
112
|
-
]
|
113
|
-
return [m.upper() for m in known_http_method_names if hasattr(self, m)]
|
112
|
+
return [m.upper() for m in self.allowed_http_methods if hasattr(self, m)]
|
plain-0.16.1/plain/csrf/views.py
DELETED
@@ -1,90 +0,0 @@
|
|
1
|
-
"Functions that help with dynamically creating decorators for views."
|
2
|
-
|
3
|
-
from functools import partial, update_wrapper, wraps
|
4
|
-
|
5
|
-
|
6
|
-
class classonlymethod(classmethod):
|
7
|
-
def __get__(self, instance, cls=None):
|
8
|
-
if instance is not None:
|
9
|
-
raise AttributeError(
|
10
|
-
"This method is available only on the class, not on instances."
|
11
|
-
)
|
12
|
-
return super().__get__(instance, cls)
|
13
|
-
|
14
|
-
|
15
|
-
def _update_method_wrapper(_wrapper, decorator):
|
16
|
-
# _multi_decorate()'s bound_method isn't available in this scope. Cheat by
|
17
|
-
# using it on a dummy function.
|
18
|
-
@decorator
|
19
|
-
def dummy(*args, **kwargs):
|
20
|
-
pass
|
21
|
-
|
22
|
-
update_wrapper(_wrapper, dummy)
|
23
|
-
|
24
|
-
|
25
|
-
def _multi_decorate(decorators, method):
|
26
|
-
"""
|
27
|
-
Decorate `method` with one or more function decorators. `decorators` can be
|
28
|
-
a single decorator or an iterable of decorators.
|
29
|
-
"""
|
30
|
-
if hasattr(decorators, "__iter__"):
|
31
|
-
# Apply a list/tuple of decorators if 'decorators' is one. Decorator
|
32
|
-
# functions are applied so that the call order is the same as the
|
33
|
-
# order in which they appear in the iterable.
|
34
|
-
decorators = decorators[::-1]
|
35
|
-
else:
|
36
|
-
decorators = [decorators]
|
37
|
-
|
38
|
-
def _wrapper(self, *args, **kwargs):
|
39
|
-
# bound_method has the signature that 'decorator' expects i.e. no
|
40
|
-
# 'self' argument, but it's a closure over self so it can call
|
41
|
-
# 'func'. Also, wrap method.__get__() in a function because new
|
42
|
-
# attributes can't be set on bound method objects, only on functions.
|
43
|
-
bound_method = wraps(method)(partial(method.__get__(self, type(self))))
|
44
|
-
for dec in decorators:
|
45
|
-
bound_method = dec(bound_method)
|
46
|
-
return bound_method(*args, **kwargs)
|
47
|
-
|
48
|
-
# Copy any attributes that a decorator adds to the function it decorates.
|
49
|
-
for dec in decorators:
|
50
|
-
_update_method_wrapper(_wrapper, dec)
|
51
|
-
# Preserve any existing attributes of 'method', including the name.
|
52
|
-
update_wrapper(_wrapper, method)
|
53
|
-
return _wrapper
|
54
|
-
|
55
|
-
|
56
|
-
def method_decorator(decorator, name=""):
|
57
|
-
"""
|
58
|
-
Convert a function decorator into a method decorator
|
59
|
-
"""
|
60
|
-
|
61
|
-
# 'obj' can be a class or a function. If 'obj' is a function at the time it
|
62
|
-
# is passed to _dec, it will eventually be a method of the class it is
|
63
|
-
# defined on. If 'obj' is a class, the 'name' is required to be the name
|
64
|
-
# of the method that will be decorated.
|
65
|
-
def _dec(obj):
|
66
|
-
if not isinstance(obj, type):
|
67
|
-
return _multi_decorate(decorator, obj)
|
68
|
-
if not (name and hasattr(obj, name)):
|
69
|
-
raise ValueError(
|
70
|
-
"The keyword argument `name` must be the name of a method "
|
71
|
-
f"of the decorated class: {obj}. Got '{name}' instead."
|
72
|
-
)
|
73
|
-
method = getattr(obj, name)
|
74
|
-
if not callable(method):
|
75
|
-
raise TypeError(
|
76
|
-
f"Cannot decorate '{name}' as it isn't a callable attribute of "
|
77
|
-
f"{obj} ({method})."
|
78
|
-
)
|
79
|
-
_wrapper = _multi_decorate(decorator, method)
|
80
|
-
setattr(obj, name, _wrapper)
|
81
|
-
return obj
|
82
|
-
|
83
|
-
# Don't worry about making _dec look similar to a list/tuple as it's rather
|
84
|
-
# meaningless.
|
85
|
-
if not hasattr(decorator, "__iter__"):
|
86
|
-
update_wrapper(_dec, decorator)
|
87
|
-
# Change the name to aid debugging.
|
88
|
-
obj = decorator if hasattr(decorator, "__name__") else decorator.__class__
|
89
|
-
_dec.__name__ = f"method_decorator({obj.__name__})"
|
90
|
-
return _dec
|
plain-0.16.1/plain/views/csrf.py
DELETED
@@ -1,24 +0,0 @@
|
|
1
|
-
from functools import wraps
|
2
|
-
|
3
|
-
from plain.utils.decorators import method_decorator
|
4
|
-
|
5
|
-
|
6
|
-
def csrf_exempt(view_func):
|
7
|
-
"""Mark a view function as being exempt from the CSRF view protection."""
|
8
|
-
|
9
|
-
# view_func.csrf_exempt = True would also work, but decorators are nicer
|
10
|
-
# if they don't have side effects, so return a new function.
|
11
|
-
@wraps(view_func)
|
12
|
-
def wrapper_view(*args, **kwargs):
|
13
|
-
return view_func(*args, **kwargs)
|
14
|
-
|
15
|
-
wrapper_view.csrf_exempt = True
|
16
|
-
return wrapper_view
|
17
|
-
|
18
|
-
|
19
|
-
@method_decorator(csrf_exempt, name="get_response")
|
20
|
-
class CsrfExemptViewMixin:
|
21
|
-
"""CsrfExemptViewMixin needs to come before View in the class definition"""
|
22
|
-
|
23
|
-
def get_response(self):
|
24
|
-
return super().get_response()
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|