Skip to content

Commit

Permalink
Remove method and content overriding
Browse files Browse the repository at this point in the history
  • Loading branch information
tomchristie committed Sep 17, 2015
1 parent a67eed1 commit 566812a
Show file tree
Hide file tree
Showing 10 changed files with 180 additions and 241 deletions.
38 changes: 11 additions & 27 deletions rest_framework/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,9 +420,6 @@ def show_form_for_method(self, view, method, request, obj):
if method not in view.allowed_methods:
return # Not a valid method

if not api_settings.FORM_METHOD_OVERRIDE:
return # Cannot use form overloading

try:
view.check_permissions(request)
if obj is not None:
Expand Down Expand Up @@ -530,13 +527,6 @@ def get_raw_data_form(self, data, view, method, request):
instance = None

with override_method(view, request, method) as request:
# If we're not using content overloading there's no point in
# supplying a generic form, as the view won't treat the form's
# value as the content of the request.
if not (api_settings.FORM_CONTENT_OVERRIDE and
api_settings.FORM_CONTENTTYPE_OVERRIDE):
return None

# Check permissions
if not self.show_form_for_method(view, method, request, instance):
return
Expand Down Expand Up @@ -564,28 +554,22 @@ def get_raw_data_form(self, data, view, method, request):

# Generate a generic form that includes a content type field,
# and a content field.
content_type_field = api_settings.FORM_CONTENTTYPE_OVERRIDE
content_field = api_settings.FORM_CONTENT_OVERRIDE

media_types = [parser.media_type for parser in view.parser_classes]
choices = [(media_type, media_type) for media_type in media_types]
initial = media_types[0]

# NB. http://jacobian.org/writing/dynamic-form-generation/
class GenericContentForm(forms.Form):
def __init__(self):
super(GenericContentForm, self).__init__()

self.fields[content_type_field] = forms.ChoiceField(
label='Media type',
choices=choices,
initial=initial
)
self.fields[content_field] = forms.CharField(
label='Content',
widget=forms.Textarea,
initial=content
)
_content_type = forms.ChoiceField(
label='Media type',
choices=choices,
initial=initial,
widget=forms.Select(attrs={'data-override': 'content-type'})
)
_content = forms.CharField(
label='Content',
widget=forms.Textarea(attrs={'data-override': 'content'}),
initial=content
)

return GenericContentForm()

Expand Down
98 changes: 4 additions & 94 deletions rest_framework/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,6 @@ 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
Expand Down Expand Up @@ -162,30 +157,10 @@ def __init__(self, request, parsers=None, authenticators=None,
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):
"""
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
meta = self._request.META
return meta.get('CONTENT_TYPE', meta.get('HTTP_CONTENT_TYPE', ''))

@property
def stream(self):
Expand Down Expand Up @@ -265,9 +240,6 @@ def _load_data_and_files(self):
"""
Parses the request content into `self.data`.
"""
if not _hasattr(self, '_content_type'):
self._load_method_and_content_type()

if not _hasattr(self, '_data'):
self._data, self._files = self._parse()
if self._files:
Expand All @@ -276,32 +248,14 @@ def _load_data_and_files(self):
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.
"""
meta = self._request.META
try:
content_length = int(
self.META.get(
'CONTENT_LENGTH', self.META.get('HTTP_CONTENT_LENGTH')
)
meta.get('CONTENT_LENGTH', meta.get('HTTP_CONTENT_LENGTH', 0))
)
except (ValueError, TypeError):
content_length = 0
Expand All @@ -313,50 +267,6 @@ def _load_stream(self):
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)
Expand Down
3 changes: 0 additions & 3 deletions rest_framework/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,6 @@
'TEST_REQUEST_DEFAULT_FORMAT': 'multipart',

# Browser enhancements
'FORM_METHOD_OVERRIDE': '_method',
'FORM_CONTENT_OVERRIDE': '_content',
'FORM_CONTENTTYPE_OVERRIDE': '_content_type',
'URL_ACCEPT_OVERRIDE': 'accept',
'URL_FORMAT_OVERRIDE': 'format',

Expand Down
97 changes: 97 additions & 0 deletions rest_framework/static/rest_framework/js/ajax-form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
function replaceDocument(docString) {
var doc = document.open("text/html");
doc.write(docString);
doc.close();
}


function doAjaxSubmit(e) {
var form = $(this);
var btn = $(this.clk);
var method = btn.data('method') || form.data('method') || form.attr('method') || 'GET';
method = method.toUpperCase()
if (method === 'GET') {
// GET requests can always use standard form submits.
return;
}

var contentType =
form.find('input[data-override="content-type"]').val() ||
form.find('select[data-override="content-type"] option:selected').text();
if (method === 'POST' && !contentType) {
// POST requests can use standard form submits, unless we have
// overridden the content type.
return;
}

// At this point we need to make an AJAX form submission.
e.preventDefault();

var url = form.attr('action');
var data;
if (contentType) {
data = form.find('[data-override="content"]').val() || ''
} else {
contentType = form.attr('enctype') || form.attr('encoding')
if (contentType === 'multipart/form-data') {
if (!window.FormData) {
alert('Your browser does not support AJAX multipart form submissions');
return;
}
// Use the FormData API and allow the content type to be set automatically,
// so it includes the boundary string.
// See https://developer.mozilla.org/en-US/docs/Web/API/FormData/Using_FormData_Objects
contentType = false;
data = new FormData(form[0]);
} else {
contentType = 'application/x-www-form-urlencoded; charset=UTF-8'
data = form.serialize();
}
}

var ret = $.ajax({
url: url,
method: method,
data: data,
contentType: contentType,
processData: false,
headers: {'Accept': 'text/html; q=1.0, */*'},
});
ret.always(function(data, textStatus, jqXHR) {
if (textStatus != 'success') {
jqXHR = data;
}
var responseContentType = jqXHR.getResponseHeader("content-type") || "";
if (responseContentType.toLowerCase().indexOf('text/html') === 0) {
replaceDocument(jqXHR.responseText);
try {
// Modify the location and scroll to top, as if after page load.
history.replaceState({}, '', url);
scroll(0,0);
} catch(err) {
// History API not supported, so redirect.
window.location = url;
}
} else {
// Not HTML content. We can't open this directly, so redirect.
window.location = url;
}
});
return ret;
}


function captureSubmittingElement(e) {
var target = e.target;
var form = this;
form.clk = target;
}


$.fn.ajaxForm = function() {
var options = {}
return this
.unbind('submit.form-plugin click.form-plugin')
.bind('submit.form-plugin', options, doAjaxSubmit)
.bind('click.form-plugin', options, captureSubmittingElement);
};
47 changes: 47 additions & 0 deletions rest_framework/static/rest_framework/js/csrf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie != '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i]);
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) == (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}

function csrfSafeMethod(method) {
// these HTTP methods do not require CSRF protection
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}

function sameOrigin(url) {
// test that a given url is a same-origin URL
// url could be relative or scheme relative or absolute
var host = document.location.host; // host + port
var protocol = document.location.protocol;
var sr_origin = '//' + host;
var origin = protocol + sr_origin;
// Allow absolute or scheme relative URLs to same origin
return (url == origin || url.slice(0, origin.length + 1) == origin + '/') ||
(url == sr_origin || url.slice(0, sr_origin.length + 1) == sr_origin + '/') ||
// or any other URL that isn't scheme relative or absolute i.e relative.
!(/^(\/\/|http:|https:).*/.test(url));
}

var csrftoken = getCookie('csrftoken');

$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!csrfSafeMethod(settings.type) && sameOrigin(settings.url)) {
// Send the token to same-origin, relative URLs only.
// Send the token only if the method warrants CSRF protection
// Using the CSRFToken value acquired earlier
xhr.setRequestHeader("X-CSRFToken", csrftoken);
}
}
});
5 changes: 5 additions & 0 deletions rest_framework/static/rest_framework/js/jquery-1.11.3.min.js

Large diffs are not rendered by default.

Loading

0 comments on commit 566812a

Please sign in to comment.