Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove browser overriding #3410

Closed
wants to merge 8 commits into from
Closed
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
101 changes: 5 additions & 96 deletions rest_framework/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'):
Expand Down 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 All @@ -144,7 +139,6 @@ def __init__(self, request, parsers=None, authenticators=None,
self._data = Empty
self._files = Empty
self._full_data = Empty
self._method = Empty
self._content_type = Empty
self._stream = Empty

Expand All @@ -162,30 +156,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 +239,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 +247,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 +266,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.

2 changes: 0 additions & 2 deletions rest_framework/static/rest_framework/js/jquery-1.8.1-min.js

This file was deleted.

Loading