plain 0.1.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/README.md +33 -0
- plain/__main__.py +5 -0
- plain/assets/README.md +56 -0
- plain/assets/__init__.py +6 -0
- plain/assets/finders.py +233 -0
- plain/assets/preflight.py +14 -0
- plain/assets/storage.py +916 -0
- plain/assets/utils.py +52 -0
- plain/assets/whitenoise/__init__.py +5 -0
- plain/assets/whitenoise/base.py +259 -0
- plain/assets/whitenoise/compress.py +189 -0
- plain/assets/whitenoise/media_types.py +137 -0
- plain/assets/whitenoise/middleware.py +197 -0
- plain/assets/whitenoise/responders.py +286 -0
- plain/assets/whitenoise/storage.py +178 -0
- plain/assets/whitenoise/string_utils.py +13 -0
- plain/cli/README.md +123 -0
- plain/cli/__init__.py +3 -0
- plain/cli/cli.py +439 -0
- plain/cli/formatting.py +61 -0
- plain/cli/packages.py +73 -0
- plain/cli/print.py +9 -0
- plain/cli/startup.py +33 -0
- plain/csrf/README.md +3 -0
- plain/csrf/middleware.py +466 -0
- plain/csrf/views.py +10 -0
- plain/debug.py +23 -0
- plain/exceptions.py +242 -0
- plain/forms/README.md +14 -0
- plain/forms/__init__.py +8 -0
- plain/forms/boundfield.py +58 -0
- plain/forms/exceptions.py +11 -0
- plain/forms/fields.py +1030 -0
- plain/forms/forms.py +297 -0
- plain/http/README.md +1 -0
- plain/http/__init__.py +51 -0
- plain/http/cookie.py +20 -0
- plain/http/multipartparser.py +743 -0
- plain/http/request.py +754 -0
- plain/http/response.py +719 -0
- plain/internal/__init__.py +0 -0
- plain/internal/files/README.md +3 -0
- plain/internal/files/__init__.py +3 -0
- plain/internal/files/base.py +161 -0
- plain/internal/files/locks.py +127 -0
- plain/internal/files/move.py +102 -0
- plain/internal/files/temp.py +79 -0
- plain/internal/files/uploadedfile.py +150 -0
- plain/internal/files/uploadhandler.py +254 -0
- plain/internal/files/utils.py +78 -0
- plain/internal/handlers/__init__.py +0 -0
- plain/internal/handlers/base.py +133 -0
- plain/internal/handlers/exception.py +145 -0
- plain/internal/handlers/wsgi.py +216 -0
- plain/internal/legacy/__init__.py +0 -0
- plain/internal/legacy/__main__.py +12 -0
- plain/internal/legacy/management/__init__.py +414 -0
- plain/internal/legacy/management/base.py +692 -0
- plain/internal/legacy/management/color.py +113 -0
- plain/internal/legacy/management/commands/__init__.py +0 -0
- plain/internal/legacy/management/commands/collectstatic.py +297 -0
- plain/internal/legacy/management/sql.py +67 -0
- plain/internal/legacy/management/utils.py +175 -0
- plain/json.py +40 -0
- plain/logs/README.md +24 -0
- plain/logs/__init__.py +5 -0
- plain/logs/configure.py +39 -0
- plain/logs/loggers.py +74 -0
- plain/logs/utils.py +46 -0
- plain/middleware/README.md +3 -0
- plain/middleware/__init__.py +0 -0
- plain/middleware/clickjacking.py +52 -0
- plain/middleware/common.py +87 -0
- plain/middleware/gzip.py +64 -0
- plain/middleware/security.py +64 -0
- plain/packages/README.md +41 -0
- plain/packages/__init__.py +4 -0
- plain/packages/config.py +259 -0
- plain/packages/registry.py +438 -0
- plain/paginator.py +187 -0
- plain/preflight/README.md +3 -0
- plain/preflight/__init__.py +38 -0
- plain/preflight/compatibility/__init__.py +0 -0
- plain/preflight/compatibility/django_4_0.py +20 -0
- plain/preflight/files.py +19 -0
- plain/preflight/messages.py +88 -0
- plain/preflight/registry.py +72 -0
- plain/preflight/security/__init__.py +0 -0
- plain/preflight/security/base.py +268 -0
- plain/preflight/security/csrf.py +40 -0
- plain/preflight/urls.py +117 -0
- plain/runtime/README.md +75 -0
- plain/runtime/__init__.py +61 -0
- plain/runtime/global_settings.py +199 -0
- plain/runtime/user_settings.py +353 -0
- plain/signals/README.md +14 -0
- plain/signals/__init__.py +5 -0
- plain/signals/dispatch/__init__.py +9 -0
- plain/signals/dispatch/dispatcher.py +320 -0
- plain/signals/dispatch/license.txt +35 -0
- plain/signing.py +299 -0
- plain/templates/README.md +20 -0
- plain/templates/__init__.py +6 -0
- plain/templates/core.py +24 -0
- plain/templates/jinja/README.md +227 -0
- plain/templates/jinja/__init__.py +22 -0
- plain/templates/jinja/defaults.py +119 -0
- plain/templates/jinja/extensions.py +39 -0
- plain/templates/jinja/filters.py +28 -0
- plain/templates/jinja/globals.py +19 -0
- plain/test/README.md +3 -0
- plain/test/__init__.py +16 -0
- plain/test/client.py +985 -0
- plain/test/utils.py +255 -0
- plain/urls/README.md +3 -0
- plain/urls/__init__.py +40 -0
- plain/urls/base.py +118 -0
- plain/urls/conf.py +94 -0
- plain/urls/converters.py +66 -0
- plain/urls/exceptions.py +9 -0
- plain/urls/resolvers.py +731 -0
- plain/utils/README.md +3 -0
- plain/utils/__init__.py +0 -0
- plain/utils/_os.py +52 -0
- plain/utils/cache.py +327 -0
- plain/utils/connection.py +84 -0
- plain/utils/crypto.py +76 -0
- plain/utils/datastructures.py +345 -0
- plain/utils/dateformat.py +329 -0
- plain/utils/dateparse.py +154 -0
- plain/utils/dates.py +76 -0
- plain/utils/deconstruct.py +54 -0
- plain/utils/decorators.py +90 -0
- plain/utils/deprecation.py +6 -0
- plain/utils/duration.py +44 -0
- plain/utils/email.py +12 -0
- plain/utils/encoding.py +235 -0
- plain/utils/functional.py +456 -0
- plain/utils/hashable.py +26 -0
- plain/utils/html.py +401 -0
- plain/utils/http.py +374 -0
- plain/utils/inspect.py +73 -0
- plain/utils/ipv6.py +46 -0
- plain/utils/itercompat.py +8 -0
- plain/utils/module_loading.py +69 -0
- plain/utils/regex_helper.py +353 -0
- plain/utils/safestring.py +72 -0
- plain/utils/termcolors.py +221 -0
- plain/utils/text.py +518 -0
- plain/utils/timesince.py +138 -0
- plain/utils/timezone.py +244 -0
- plain/utils/tree.py +126 -0
- plain/validators.py +603 -0
- plain/views/README.md +268 -0
- plain/views/__init__.py +18 -0
- plain/views/base.py +107 -0
- plain/views/csrf.py +24 -0
- plain/views/errors.py +25 -0
- plain/views/exceptions.py +4 -0
- plain/views/forms.py +76 -0
- plain/views/objects.py +229 -0
- plain/views/redirect.py +72 -0
- plain/views/templates.py +66 -0
- plain/wsgi.py +11 -0
- plain-0.1.0.dist-info/LICENSE +85 -0
- plain-0.1.0.dist-info/METADATA +51 -0
- plain-0.1.0.dist-info/RECORD +169 -0
- plain-0.1.0.dist-info/WHEEL +4 -0
- plain-0.1.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base file upload handler classes, and the built-in concrete subclasses
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
from io import BytesIO
|
|
6
|
+
|
|
7
|
+
from plain.internal.files.uploadedfile import (
|
|
8
|
+
InMemoryUploadedFile,
|
|
9
|
+
TemporaryUploadedFile,
|
|
10
|
+
)
|
|
11
|
+
from plain.runtime import settings
|
|
12
|
+
from plain.utils.module_loading import import_string
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"UploadFileException",
|
|
16
|
+
"StopUpload",
|
|
17
|
+
"SkipFile",
|
|
18
|
+
"FileUploadHandler",
|
|
19
|
+
"TemporaryFileUploadHandler",
|
|
20
|
+
"MemoryFileUploadHandler",
|
|
21
|
+
"load_handler",
|
|
22
|
+
"StopFutureHandlers",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class UploadFileException(Exception):
|
|
27
|
+
"""
|
|
28
|
+
Any error having to do with uploading files.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class StopUpload(UploadFileException):
|
|
35
|
+
"""
|
|
36
|
+
This exception is raised when an upload must abort.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, connection_reset=False):
|
|
40
|
+
"""
|
|
41
|
+
If ``connection_reset`` is ``True``, Plain knows will halt the upload
|
|
42
|
+
without consuming the rest of the upload. This will cause the browser to
|
|
43
|
+
show a "connection reset" error.
|
|
44
|
+
"""
|
|
45
|
+
self.connection_reset = connection_reset
|
|
46
|
+
|
|
47
|
+
def __str__(self):
|
|
48
|
+
if self.connection_reset:
|
|
49
|
+
return "StopUpload: Halt current upload."
|
|
50
|
+
else:
|
|
51
|
+
return "StopUpload: Consume request data, then halt."
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class SkipFile(UploadFileException):
|
|
55
|
+
"""
|
|
56
|
+
This exception is raised by an upload handler that wants to skip a given file.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class StopFutureHandlers(UploadFileException):
|
|
63
|
+
"""
|
|
64
|
+
Upload handlers that have handled a file and do not want future handlers to
|
|
65
|
+
run should raise this exception instead of returning None.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class FileUploadHandler:
|
|
72
|
+
"""
|
|
73
|
+
Base class for streaming upload handlers.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
chunk_size = 64 * 2**10 # : The default chunk size is 64 KB.
|
|
77
|
+
|
|
78
|
+
def __init__(self, request=None):
|
|
79
|
+
self.file_name = None
|
|
80
|
+
self.content_type = None
|
|
81
|
+
self.content_length = None
|
|
82
|
+
self.charset = None
|
|
83
|
+
self.content_type_extra = None
|
|
84
|
+
self.request = request
|
|
85
|
+
|
|
86
|
+
def handle_raw_input(
|
|
87
|
+
self, input_data, META, content_length, boundary, encoding=None
|
|
88
|
+
):
|
|
89
|
+
"""
|
|
90
|
+
Handle the raw input from the client.
|
|
91
|
+
|
|
92
|
+
Parameters:
|
|
93
|
+
|
|
94
|
+
:input_data:
|
|
95
|
+
An object that supports reading via .read().
|
|
96
|
+
:META:
|
|
97
|
+
``request.META``.
|
|
98
|
+
:content_length:
|
|
99
|
+
The (integer) value of the Content-Length header from the
|
|
100
|
+
client.
|
|
101
|
+
:boundary: The boundary from the Content-Type header. Be sure to
|
|
102
|
+
prepend two '--'.
|
|
103
|
+
"""
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
def new_file(
|
|
107
|
+
self,
|
|
108
|
+
field_name,
|
|
109
|
+
file_name,
|
|
110
|
+
content_type,
|
|
111
|
+
content_length,
|
|
112
|
+
charset=None,
|
|
113
|
+
content_type_extra=None,
|
|
114
|
+
):
|
|
115
|
+
"""
|
|
116
|
+
Signal that a new file has been started.
|
|
117
|
+
|
|
118
|
+
Warning: As with any data from the client, you should not trust
|
|
119
|
+
content_length (and sometimes won't even get it).
|
|
120
|
+
"""
|
|
121
|
+
self.field_name = field_name
|
|
122
|
+
self.file_name = file_name
|
|
123
|
+
self.content_type = content_type
|
|
124
|
+
self.content_length = content_length
|
|
125
|
+
self.charset = charset
|
|
126
|
+
self.content_type_extra = content_type_extra
|
|
127
|
+
|
|
128
|
+
def receive_data_chunk(self, raw_data, start):
|
|
129
|
+
"""
|
|
130
|
+
Receive data from the streamed upload parser. ``start`` is the position
|
|
131
|
+
in the file of the chunk.
|
|
132
|
+
"""
|
|
133
|
+
raise NotImplementedError(
|
|
134
|
+
"subclasses of FileUploadHandler must provide a receive_data_chunk() method"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def file_complete(self, file_size):
|
|
138
|
+
"""
|
|
139
|
+
Signal that a file has completed. File size corresponds to the actual
|
|
140
|
+
size accumulated by all the chunks.
|
|
141
|
+
|
|
142
|
+
Subclasses should return a valid ``UploadedFile`` object.
|
|
143
|
+
"""
|
|
144
|
+
raise NotImplementedError(
|
|
145
|
+
"subclasses of FileUploadHandler must provide a file_complete() method"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def upload_complete(self):
|
|
149
|
+
"""
|
|
150
|
+
Signal that the upload is complete. Subclasses should perform cleanup
|
|
151
|
+
that is necessary for this handler.
|
|
152
|
+
"""
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
def upload_interrupted(self):
|
|
156
|
+
"""
|
|
157
|
+
Signal that the upload was interrupted. Subclasses should perform
|
|
158
|
+
cleanup that is necessary for this handler.
|
|
159
|
+
"""
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class TemporaryFileUploadHandler(FileUploadHandler):
|
|
164
|
+
"""
|
|
165
|
+
Upload handler that streams data into a temporary file.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
def new_file(self, *args, **kwargs):
|
|
169
|
+
"""
|
|
170
|
+
Create the file object to append to as data is coming in.
|
|
171
|
+
"""
|
|
172
|
+
super().new_file(*args, **kwargs)
|
|
173
|
+
self.file = TemporaryUploadedFile(
|
|
174
|
+
self.file_name, self.content_type, 0, self.charset, self.content_type_extra
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def receive_data_chunk(self, raw_data, start):
|
|
178
|
+
self.file.write(raw_data)
|
|
179
|
+
|
|
180
|
+
def file_complete(self, file_size):
|
|
181
|
+
self.file.seek(0)
|
|
182
|
+
self.file.size = file_size
|
|
183
|
+
return self.file
|
|
184
|
+
|
|
185
|
+
def upload_interrupted(self):
|
|
186
|
+
if hasattr(self, "file"):
|
|
187
|
+
temp_location = self.file.temporary_file_path()
|
|
188
|
+
try:
|
|
189
|
+
self.file.close()
|
|
190
|
+
os.remove(temp_location)
|
|
191
|
+
except FileNotFoundError:
|
|
192
|
+
pass
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class MemoryFileUploadHandler(FileUploadHandler):
|
|
196
|
+
"""
|
|
197
|
+
File upload handler to stream uploads into memory (used for small files).
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
def handle_raw_input(
|
|
201
|
+
self, input_data, META, content_length, boundary, encoding=None
|
|
202
|
+
):
|
|
203
|
+
"""
|
|
204
|
+
Use the content_length to signal whether or not this handler should be
|
|
205
|
+
used.
|
|
206
|
+
"""
|
|
207
|
+
# Check the content-length header to see if we should
|
|
208
|
+
# If the post is too large, we cannot use the Memory handler.
|
|
209
|
+
self.activated = content_length <= settings.FILE_UPLOAD_MAX_MEMORY_SIZE
|
|
210
|
+
|
|
211
|
+
def new_file(self, *args, **kwargs):
|
|
212
|
+
super().new_file(*args, **kwargs)
|
|
213
|
+
if self.activated:
|
|
214
|
+
self.file = BytesIO()
|
|
215
|
+
raise StopFutureHandlers()
|
|
216
|
+
|
|
217
|
+
def receive_data_chunk(self, raw_data, start):
|
|
218
|
+
"""Add the data to the BytesIO file."""
|
|
219
|
+
if self.activated:
|
|
220
|
+
self.file.write(raw_data)
|
|
221
|
+
else:
|
|
222
|
+
return raw_data
|
|
223
|
+
|
|
224
|
+
def file_complete(self, file_size):
|
|
225
|
+
"""Return a file object if this handler is activated."""
|
|
226
|
+
if not self.activated:
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
self.file.seek(0)
|
|
230
|
+
return InMemoryUploadedFile(
|
|
231
|
+
file=self.file,
|
|
232
|
+
field_name=self.field_name,
|
|
233
|
+
name=self.file_name,
|
|
234
|
+
content_type=self.content_type,
|
|
235
|
+
size=file_size,
|
|
236
|
+
charset=self.charset,
|
|
237
|
+
content_type_extra=self.content_type_extra,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def load_handler(path, *args, **kwargs):
|
|
242
|
+
"""
|
|
243
|
+
Given a path to a handler, return an instance of that handler.
|
|
244
|
+
|
|
245
|
+
E.g.::
|
|
246
|
+
>>> from plain.http import HttpRequest
|
|
247
|
+
>>> request = HttpRequest()
|
|
248
|
+
>>> load_handler(
|
|
249
|
+
... 'plain.internal.files.uploadhandler.TemporaryFileUploadHandler',
|
|
250
|
+
... request,
|
|
251
|
+
... )
|
|
252
|
+
<TemporaryFileUploadHandler object at 0x...>
|
|
253
|
+
"""
|
|
254
|
+
return import_string(path)(*args, **kwargs)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pathlib
|
|
3
|
+
|
|
4
|
+
from plain.exceptions import SuspiciousFileOperation
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def validate_file_name(name, allow_relative_path=False):
|
|
8
|
+
# Remove potentially dangerous names
|
|
9
|
+
if os.path.basename(name) in {"", ".", ".."}:
|
|
10
|
+
raise SuspiciousFileOperation("Could not derive file name from '%s'" % name)
|
|
11
|
+
|
|
12
|
+
if allow_relative_path:
|
|
13
|
+
# Use PurePosixPath() because this branch is checked only in
|
|
14
|
+
# FileField.generate_filename() where all file paths are expected to be
|
|
15
|
+
# Unix style (with forward slashes).
|
|
16
|
+
path = pathlib.PurePosixPath(name)
|
|
17
|
+
if path.is_absolute() or ".." in path.parts:
|
|
18
|
+
raise SuspiciousFileOperation(
|
|
19
|
+
"Detected path traversal attempt in '%s'" % name
|
|
20
|
+
)
|
|
21
|
+
elif name != os.path.basename(name):
|
|
22
|
+
raise SuspiciousFileOperation("File name '%s' includes path elements" % name)
|
|
23
|
+
|
|
24
|
+
return name
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class FileProxyMixin:
|
|
28
|
+
"""
|
|
29
|
+
A mixin class used to forward file methods to an underlying file
|
|
30
|
+
object. The internal file object has to be called "file"::
|
|
31
|
+
|
|
32
|
+
class FileProxy(FileProxyMixin):
|
|
33
|
+
def __init__(self, file):
|
|
34
|
+
self.file = file
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
encoding = property(lambda self: self.file.encoding)
|
|
38
|
+
fileno = property(lambda self: self.file.fileno)
|
|
39
|
+
flush = property(lambda self: self.file.flush)
|
|
40
|
+
isatty = property(lambda self: self.file.isatty)
|
|
41
|
+
newlines = property(lambda self: self.file.newlines)
|
|
42
|
+
read = property(lambda self: self.file.read)
|
|
43
|
+
readinto = property(lambda self: self.file.readinto)
|
|
44
|
+
readline = property(lambda self: self.file.readline)
|
|
45
|
+
readlines = property(lambda self: self.file.readlines)
|
|
46
|
+
seek = property(lambda self: self.file.seek)
|
|
47
|
+
tell = property(lambda self: self.file.tell)
|
|
48
|
+
truncate = property(lambda self: self.file.truncate)
|
|
49
|
+
write = property(lambda self: self.file.write)
|
|
50
|
+
writelines = property(lambda self: self.file.writelines)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def closed(self):
|
|
54
|
+
return not self.file or self.file.closed
|
|
55
|
+
|
|
56
|
+
def readable(self):
|
|
57
|
+
if self.closed:
|
|
58
|
+
return False
|
|
59
|
+
if hasattr(self.file, "readable"):
|
|
60
|
+
return self.file.readable()
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
def writable(self):
|
|
64
|
+
if self.closed:
|
|
65
|
+
return False
|
|
66
|
+
if hasattr(self.file, "writable"):
|
|
67
|
+
return self.file.writable()
|
|
68
|
+
return "w" in getattr(self.file, "mode", "")
|
|
69
|
+
|
|
70
|
+
def seekable(self):
|
|
71
|
+
if self.closed:
|
|
72
|
+
return False
|
|
73
|
+
if hasattr(self.file, "seekable"):
|
|
74
|
+
return self.file.seekable()
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
def __iter__(self):
|
|
78
|
+
return iter(self.file)
|
|
File without changes
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import types
|
|
3
|
+
|
|
4
|
+
from plain.exceptions import ImproperlyConfigured
|
|
5
|
+
from plain.logs import log_response
|
|
6
|
+
from plain.runtime import settings
|
|
7
|
+
from plain.signals import request_finished
|
|
8
|
+
from plain.urls import get_resolver, set_urlconf
|
|
9
|
+
from plain.utils.module_loading import import_string
|
|
10
|
+
|
|
11
|
+
from .exception import convert_exception_to_response
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("plain.request")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BaseHandler:
|
|
17
|
+
_view_middleware = None
|
|
18
|
+
_middleware_chain = None
|
|
19
|
+
|
|
20
|
+
def load_middleware(self):
|
|
21
|
+
"""
|
|
22
|
+
Populate middleware lists from settings.MIDDLEWARE.
|
|
23
|
+
|
|
24
|
+
Must be called after the environment is fixed (see __call__ in subclasses).
|
|
25
|
+
"""
|
|
26
|
+
self._view_middleware = []
|
|
27
|
+
|
|
28
|
+
get_response = self._get_response
|
|
29
|
+
handler = convert_exception_to_response(get_response)
|
|
30
|
+
for middleware_path in reversed(settings.MIDDLEWARE):
|
|
31
|
+
middleware = import_string(middleware_path)
|
|
32
|
+
mw_instance = middleware(handler)
|
|
33
|
+
|
|
34
|
+
if mw_instance is None:
|
|
35
|
+
raise ImproperlyConfigured(
|
|
36
|
+
"Middleware factory %s returned None." % middleware_path
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
if hasattr(mw_instance, "process_view"):
|
|
40
|
+
self._view_middleware.insert(
|
|
41
|
+
0,
|
|
42
|
+
mw_instance.process_view,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
handler = convert_exception_to_response(mw_instance)
|
|
46
|
+
|
|
47
|
+
# We only assign to this when initialization is complete as it is used
|
|
48
|
+
# as a flag for initialization being complete.
|
|
49
|
+
self._middleware_chain = handler
|
|
50
|
+
|
|
51
|
+
def get_response(self, request):
|
|
52
|
+
"""Return a Response object for the given HttpRequest."""
|
|
53
|
+
# Setup default url resolver for this thread
|
|
54
|
+
set_urlconf(settings.ROOT_URLCONF)
|
|
55
|
+
response = self._middleware_chain(request)
|
|
56
|
+
response._resource_closers.append(request.close)
|
|
57
|
+
if response.status_code >= 400:
|
|
58
|
+
log_response(
|
|
59
|
+
"%s: %s",
|
|
60
|
+
response.reason_phrase,
|
|
61
|
+
request.path,
|
|
62
|
+
response=response,
|
|
63
|
+
request=request,
|
|
64
|
+
)
|
|
65
|
+
return response
|
|
66
|
+
|
|
67
|
+
def _get_response(self, request):
|
|
68
|
+
"""
|
|
69
|
+
Resolve and call the view, then apply view, exception, and
|
|
70
|
+
template_response middleware. This method is everything that happens
|
|
71
|
+
inside the request/response middleware.
|
|
72
|
+
"""
|
|
73
|
+
response = None
|
|
74
|
+
callback, callback_args, callback_kwargs = self.resolve_request(request)
|
|
75
|
+
|
|
76
|
+
# Apply view middleware
|
|
77
|
+
for middleware_method in self._view_middleware:
|
|
78
|
+
response = middleware_method(
|
|
79
|
+
request, callback, callback_args, callback_kwargs
|
|
80
|
+
)
|
|
81
|
+
if response:
|
|
82
|
+
break
|
|
83
|
+
|
|
84
|
+
if response is None:
|
|
85
|
+
response = callback(request, *callback_args, **callback_kwargs)
|
|
86
|
+
|
|
87
|
+
# Complain if the view returned None (a common error).
|
|
88
|
+
self.check_response(response, callback)
|
|
89
|
+
|
|
90
|
+
return response
|
|
91
|
+
|
|
92
|
+
def resolve_request(self, request):
|
|
93
|
+
"""
|
|
94
|
+
Retrieve/set the urlconf for the request. Return the view resolved,
|
|
95
|
+
with its args and kwargs.
|
|
96
|
+
"""
|
|
97
|
+
# Work out the resolver.
|
|
98
|
+
if hasattr(request, "urlconf"):
|
|
99
|
+
urlconf = request.urlconf
|
|
100
|
+
set_urlconf(urlconf)
|
|
101
|
+
resolver = get_resolver(urlconf)
|
|
102
|
+
else:
|
|
103
|
+
resolver = get_resolver()
|
|
104
|
+
# Resolve the view, and assign the match object back to the request.
|
|
105
|
+
resolver_match = resolver.resolve(request.path_info)
|
|
106
|
+
request.resolver_match = resolver_match
|
|
107
|
+
return resolver_match
|
|
108
|
+
|
|
109
|
+
def check_response(self, response, callback, name=None):
|
|
110
|
+
"""
|
|
111
|
+
Raise an error if the view returned None or an uncalled coroutine.
|
|
112
|
+
"""
|
|
113
|
+
if not name:
|
|
114
|
+
if isinstance(callback, types.FunctionType): # FBV
|
|
115
|
+
name = f"The view {callback.__module__}.{callback.__name__}"
|
|
116
|
+
else: # CBV
|
|
117
|
+
name = "The view {}.{}.__call__".format(
|
|
118
|
+
callback.__module__,
|
|
119
|
+
callback.__class__.__name__,
|
|
120
|
+
)
|
|
121
|
+
if response is None:
|
|
122
|
+
raise ValueError(
|
|
123
|
+
"%s didn't return a Response object. It returned None "
|
|
124
|
+
"instead." % name
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def reset_urlconf(sender, **kwargs):
|
|
129
|
+
"""Reset the URLconf after each request is finished."""
|
|
130
|
+
set_urlconf(None)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
request_finished.connect(reset_urlconf)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from functools import wraps
|
|
3
|
+
|
|
4
|
+
from plain import signals
|
|
5
|
+
from plain.exceptions import (
|
|
6
|
+
BadRequest,
|
|
7
|
+
PermissionDenied,
|
|
8
|
+
RequestDataTooBig,
|
|
9
|
+
SuspiciousOperation,
|
|
10
|
+
TooManyFieldsSent,
|
|
11
|
+
TooManyFilesSent,
|
|
12
|
+
)
|
|
13
|
+
from plain.http import Http404, ResponseServerError
|
|
14
|
+
from plain.http.multipartparser import MultiPartParserError
|
|
15
|
+
from plain.logs import log_response
|
|
16
|
+
from plain.runtime import settings
|
|
17
|
+
from plain.utils.module_loading import import_string
|
|
18
|
+
from plain.views.errors import ErrorView
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def convert_exception_to_response(get_response):
|
|
22
|
+
"""
|
|
23
|
+
Wrap the given get_response callable in exception-to-response conversion.
|
|
24
|
+
|
|
25
|
+
All exceptions will be converted. All known 4xx exceptions (Http404,
|
|
26
|
+
PermissionDenied, MultiPartParserError, SuspiciousOperation) will be
|
|
27
|
+
converted to the appropriate response, and all other exceptions will be
|
|
28
|
+
converted to 500 responses.
|
|
29
|
+
|
|
30
|
+
This decorator is automatically applied to all middleware to ensure that
|
|
31
|
+
no middleware leaks an exception and that the next middleware in the stack
|
|
32
|
+
can rely on getting a response instead of an exception.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
@wraps(get_response)
|
|
36
|
+
def inner(request):
|
|
37
|
+
try:
|
|
38
|
+
response = get_response(request)
|
|
39
|
+
except Exception as exc:
|
|
40
|
+
response = response_for_exception(request, exc)
|
|
41
|
+
return response
|
|
42
|
+
|
|
43
|
+
return inner
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def response_for_exception(request, exc):
|
|
47
|
+
if isinstance(exc, Http404):
|
|
48
|
+
response = get_exception_response(request, 404)
|
|
49
|
+
|
|
50
|
+
elif isinstance(exc, PermissionDenied):
|
|
51
|
+
response = get_exception_response(request, 403)
|
|
52
|
+
log_response(
|
|
53
|
+
"Forbidden (Permission denied): %s",
|
|
54
|
+
request.path,
|
|
55
|
+
response=response,
|
|
56
|
+
request=request,
|
|
57
|
+
exception=exc,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
elif isinstance(exc, MultiPartParserError):
|
|
61
|
+
response = get_exception_response(request, 400)
|
|
62
|
+
log_response(
|
|
63
|
+
"Bad request (Unable to parse request body): %s",
|
|
64
|
+
request.path,
|
|
65
|
+
response=response,
|
|
66
|
+
request=request,
|
|
67
|
+
exception=exc,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
elif isinstance(exc, BadRequest):
|
|
71
|
+
response = get_exception_response(request, 400)
|
|
72
|
+
log_response(
|
|
73
|
+
"%s: %s",
|
|
74
|
+
str(exc),
|
|
75
|
+
request.path,
|
|
76
|
+
response=response,
|
|
77
|
+
request=request,
|
|
78
|
+
exception=exc,
|
|
79
|
+
)
|
|
80
|
+
elif isinstance(exc, SuspiciousOperation):
|
|
81
|
+
if isinstance(exc, RequestDataTooBig | TooManyFieldsSent | TooManyFilesSent):
|
|
82
|
+
# POST data can't be accessed again, otherwise the original
|
|
83
|
+
# exception would be raised.
|
|
84
|
+
request._mark_post_parse_error()
|
|
85
|
+
|
|
86
|
+
# The request logger receives events for any problematic request
|
|
87
|
+
# The security logger receives events for all SuspiciousOperations
|
|
88
|
+
security_logger = logging.getLogger(
|
|
89
|
+
"plain.security.%s" % exc.__class__.__name__
|
|
90
|
+
)
|
|
91
|
+
security_logger.error(
|
|
92
|
+
str(exc),
|
|
93
|
+
exc_info=exc,
|
|
94
|
+
extra={"status_code": 400, "request": request},
|
|
95
|
+
)
|
|
96
|
+
response = get_exception_response(request, 400)
|
|
97
|
+
|
|
98
|
+
else:
|
|
99
|
+
signals.got_request_exception.send(sender=None, request=request)
|
|
100
|
+
response = get_exception_response(request, 500)
|
|
101
|
+
log_response(
|
|
102
|
+
"%s: %s",
|
|
103
|
+
response.reason_phrase,
|
|
104
|
+
request.path,
|
|
105
|
+
response=response,
|
|
106
|
+
request=request,
|
|
107
|
+
exception=exc,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Force a TemplateResponse to be rendered.
|
|
111
|
+
if not getattr(response, "is_rendered", True) and callable(
|
|
112
|
+
getattr(response, "render", None)
|
|
113
|
+
):
|
|
114
|
+
response = response.render()
|
|
115
|
+
|
|
116
|
+
return response
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def get_exception_response(request, status_code):
|
|
120
|
+
try:
|
|
121
|
+
return get_error_view(status_code)(request)
|
|
122
|
+
except Exception:
|
|
123
|
+
signals.got_request_exception.send(sender=None, request=request)
|
|
124
|
+
return handle_uncaught_exception()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def handle_uncaught_exception():
|
|
128
|
+
"""
|
|
129
|
+
Processing for any otherwise uncaught exceptions (those that will
|
|
130
|
+
generate HTTP 500 responses).
|
|
131
|
+
"""
|
|
132
|
+
return ResponseServerError()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_error_view(status_code):
|
|
136
|
+
views_by_status = settings.HTTP_ERROR_VIEWS
|
|
137
|
+
if status_code in views_by_status:
|
|
138
|
+
view = views_by_status[status_code]
|
|
139
|
+
if isinstance(view, str):
|
|
140
|
+
# Import the view if it's a string
|
|
141
|
+
view = import_string(view)
|
|
142
|
+
return view.as_view()
|
|
143
|
+
|
|
144
|
+
# Create a standard view for any other status code
|
|
145
|
+
return ErrorView.as_view(status_code=status_code)
|