diff --git a/rest_framework/request.py b/rest_framework/request.py index 846cadcd9..88f98b52e 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -86,7 +86,7 @@ def clone_request(request, method): ret._full_data = request._full_data ret._content_type = request._content_type ret._stream = request._stream - ret.method = method + ret._method = method if hasattr(request, '_user'): ret._user = request._user if hasattr(request, '_auth'): @@ -129,6 +129,11 @@ class Request(object): - authentication_classes(list/tuple). The authentications used to try authenticating the request's user. """ + + _METHOD_PARAM = api_settings.FORM_METHOD_OVERRIDE + _CONTENT_PARAM = api_settings.FORM_CONTENT_OVERRIDE + _CONTENTTYPE_PARAM = api_settings.FORM_CONTENTTYPE_OVERRIDE + def __init__(self, request, parsers=None, authenticators=None, negotiator=None, parser_context=None): self._request = request @@ -139,6 +144,7 @@ class Request(object): self._data = Empty self._files = Empty self._full_data = Empty + self._method = Empty self._content_type = Empty self._stream = Empty @@ -156,6 +162,18 @@ class Request(object): def _default_negotiator(self): return api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS() + @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): meta = self._request.META @@ -247,6 +265,23 @@ class Request(object): else: self._full_data = self._data + 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 not _hasattr(self, '_method'): + self._method = self._request.method + + # Allow X-HTTP-METHOD-OVERRIDE header + if 'HTTP_X_HTTP_METHOD_OVERRIDE' in self.META: + self._method = self.META['HTTP_X_HTTP_METHOD_OVERRIDE'].upper() + def _load_stream(self): """ Return the content body of the request, as a stream. @@ -254,7 +289,9 @@ class Request(object): meta = self._request.META try: content_length = int( - meta.get('CONTENT_LENGTH', meta.get('HTTP_CONTENT_LENGTH', 0)) + meta.get( + 'CONTENT_LENGTH', meta.get('HTTP_CONTENT_LENGTH') + ) ) except (ValueError, TypeError): content_length = 0 @@ -266,6 +303,50 @@ class Request(object): else: self._stream = six.BytesIO(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. + """ + + USE_FORM_OVERLOADING = ( + self._METHOD_PARAM or + (self._CONTENT_PARAM and self._CONTENTTYPE_PARAM) + ) + + # We only need to use form overloading on form POST requests. + if ( + self._request.method != 'POST' or + not USE_FORM_OVERLOADING 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 = self._request.POST + self._files = self._request.FILES + self._full_data = self._data.copy() + self._full_data.update(self._files) + + # Method overloading - change the method and remove the param from the content. + if ( + self._METHOD_PARAM and + self._METHOD_PARAM in self._data + ): + self._method = self._data[self._METHOD_PARAM].upper() + + # Content overloading - modify the content type, and force re-parse. + if ( + self._CONTENT_PARAM and + self._CONTENTTYPE_PARAM and + self._CONTENT_PARAM in self._data and + self._CONTENTTYPE_PARAM in self._data + ): + self._content_type = self._data[self._CONTENTTYPE_PARAM] + self._stream = six.BytesIO(self._data[self._CONTENT_PARAM].encode(self.parser_context['encoding'])) + self._data, self._files, self._full_data = (Empty, Empty, Empty) + def _parse(self): """ Parse the request content, returning a two-tuple of (data, files) diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 9679cad59..d637fdf2b 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -92,6 +92,9 @@ DEFAULTS = { 'rest_framework.renderers.JSONRenderer' ), 'TEST_REQUEST_DEFAULT_FORMAT': 'multipart', + 'FORM_METHOD_OVERRIDE': '_method', + 'FORM_CONTENT_OVERRIDE': '_content', + 'FORM_CONTENTTYPE_OVERRIDE': '_content_type', # Hyperlink settings 'URL_FORMAT_OVERRIDE': 'format',