1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
|
"""
The :mod:`request` module provides a :class:`Request` class that can be used
to enhance the standard `request` object received in all the views.
This enhanced request object offers the following :
- content automatically parsed according to `Content-Type` header, and available as :meth:`request.DATA<Request.DATA>`
- full support of PUT method, including support for file uploads
- form overloading of HTTP method, content type and content
"""
from django.http import HttpRequest
from djangorestframework.response import ErrorResponse
from djangorestframework import status
from djangorestframework.utils.mediatypes import is_form_media_type, order_by_precedence
from djangorestframework.utils import as_tuple
from StringIO import StringIO
__all__ = ('Request',)
def request_class_factory(request):
"""
Builds and returns a request class, to be used as a replacement of Django's built-in.
In fact :class:`request.Request` needs to be mixed-in with a subclass of `HttpRequest` for use,
and we cannot do that before knowing which subclass of `HttpRequest` is used. So this function
takes a request instance as only argument, and returns a properly mixed-in request class.
"""
request_class = type(request)
return type(request_class.__name__, (Request, request_class), {})
class Request(object):
"""
A mixin class allowing to enhance Django's standard HttpRequest.
"""
_USE_FORM_OVERLOADING = True
_METHOD_PARAM = '_method'
_CONTENTTYPE_PARAM = '_content_type'
_CONTENT_PARAM = '_content'
parsers = ()
"""
The set of parsers that the request can handle.
Should be a tuple/list of classes as described in the :mod:`parsers` module.
"""
def __init__(self, request):
# this allows to "copy" a request object into a new instance
# of our custom request class.
# First, we prepare the attributes to copy.
attrs_dict = request.__dict__.copy()
attrs_dict.pop('method', None)
attrs_dict['_raw_method'] = request.method
# Then, put them in the instance's own __dict__
self.__dict__ = attrs_dict
@property
def method(self):
"""
Returns the HTTP method.
This allows the `method` to be overridden by using a hidden `form` field
on a form POST request.
"""
if not hasattr(self, '_method'):
self._load_method_and_content_type()
return self._method
@property
def content_type(self):
"""
Returns the content type header.
This should be used instead of ``request.META.get('HTTP_CONTENT_TYPE')``,
as it allows the content type to be overridden by using a hidden form
field on a form POST request.
"""
if not hasattr(self, '_content_type'):
self._load_method_and_content_type()
return self._content_type
@property
def DATA(self):
"""
Parses the request body and returns the data.
Similar to ``request.POST``, except that it handles arbitrary parsers,
and also works on methods other than POST (eg PUT).
"""
if not hasattr(self, '_data'):
self._load_data_and_files()
return self._data
@property
def FILES(self):
"""
Parses the request body and returns the files.
Similar to ``request.FILES``, except that it handles arbitrary parsers,
and also works on methods other than POST (eg PUT).
"""
if not hasattr(self, '_files'):
self._load_data_and_files()
return self._files
def _load_post_and_files(self):
"""
Overrides the parent's `_load_post_and_files` to isolate it
from the form overloading mechanism (see: `_perform_form_overloading`).
"""
# When self.POST or self.FILES are called they need to know the original
# HTTP method, not our overloaded HTTP method. So, we save our overloaded
# HTTP method and restore it after the call to parent.
method_mem = getattr(self, '_method', None)
self._method = self._raw_method
super(Request, self)._load_post_and_files()
if method_mem is None:
del self._method
else:
self._method = method_mem
def _load_data_and_files(self):
"""
Parses the request content into self.DATA and self.FILES.
"""
if not hasattr(self, '_content_type'):
self._load_method_and_content_type()
if not hasattr(self, '_data'):
(self._data, self._files) = self._parse(self._get_stream(), self._content_type)
def _load_method_and_content_type(self):
"""
Sets the method and content_type, and then check if they've been overridden.
"""
self._content_type = self.META.get('HTTP_CONTENT_TYPE', self.META.get('CONTENT_TYPE', ''))
self._perform_form_overloading()
# if the HTTP method was not overloaded, we take the raw HTTP method
if not hasattr(self, '_method'):
self._method = self._raw_method
def _get_stream(self):
"""
Returns an object that may be used to stream the request content.
"""
try:
content_length = int(self.META.get('CONTENT_LENGTH', self.META.get('HTTP_CONTENT_LENGTH')))
except (ValueError, TypeError):
content_length = 0
# TODO: Add 1.3's LimitedStream to compat and use that.
# NOTE: Currently only supports parsing request body as a stream with 1.3
if content_length == 0:
return None
elif hasattr(self, 'read'):
return self
return StringIO(self.raw_post_data)
def _perform_form_overloading(self):
"""
If this is a form POST request, then we need to check if the method and content/content_type have been
overridden by setting them in hidden form fields or not.
"""
# We only need to use form overloading on form POST requests.
if not self._USE_FORM_OVERLOADING or self._raw_method != 'POST' or not is_form_media_type(self._content_type):
return
# At this point we're committed to parsing the request as form data.
self._data = data = self.POST.copy()
self._files = self.FILES
# Method overloading - change the method and remove the param from the content.
if self._METHOD_PARAM in data:
# NOTE: unlike `get`, `pop` on a `QueryDict` seems to return a list of values.
self._method = self._data.pop(self._METHOD_PARAM)[0].upper()
# Content overloading - modify the content type, and re-parse.
if self._CONTENT_PARAM in data and self._CONTENTTYPE_PARAM in data:
self._content_type = self._data.pop(self._CONTENTTYPE_PARAM)[0]
stream = StringIO(self._data.pop(self._CONTENT_PARAM)[0])
(self._data, self._files) = self._parse(stream, self._content_type)
def _parse(self, stream, content_type):
"""
Parse the request content.
May raise a 415 ErrorResponse (Unsupported Media Type), or a 400 ErrorResponse (Bad Request).
"""
if stream is None or content_type is None:
return (None, None)
parsers = as_tuple(self.parsers)
for parser_cls in parsers:
parser = parser_cls(self)
if parser.can_handle_request(content_type):
return parser.parse(stream)
raise ErrorResponse(content={'error':
'Unsupported media type in request \'%s\'.' % content_type},
status=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE)
@property
def _parsed_media_types(self):
"""
Return a list of all the media types that this view can parse.
"""
return [parser.media_type for parser in self.parsers]
@property
def _default_parser(self):
"""
Return the view's default parser class.
"""
return self.parsers[0]
|