diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 4bae7729f7..5b76b2a2d0 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -2,6 +2,7 @@ The `compat` module provides support for backwards compatibility with older versions of Django/Python, and compatibility wrappers around optional packages. """ +import django from django.conf import settings from django.views.generic import View @@ -152,6 +153,32 @@ def md_filter_add_syntax_highlight(md): return False +if django.VERSION >= (4, 2): + # Django 4.2+: use the stock parse_header_parameters function + # Note: Django 4.1 also has an implementation of parse_header_parameters + # which is slightly different from the one in 4.2, it needs + # the compatibility shim as well. + from django.utils.http import parse_header_parameters + parse_header_parameters = parse_header_parameters + +else: + # Django <= 4.1: create a compatibility shim for parse_header_parameters + from django.http.multipartparser import parse_header + + def parse_header_parameters(line): + # parse_header works with bytes, but parse_header_parameters + # works with strings. Call encode to convert the line to bytes. + main_value_pair, params = parse_header(line.encode()) + return main_value_pair, { + # parse_header will convert *some* values to string. + # parse_header_parameters converts *all* values to string. + # Make sure all values are converted by calling decode on + # any remaining non-string values. + k: v if isinstance(v, str) else v.decode() + for k, v in params.items() + } + + # `separators` argument to `json.dumps()` differs between 2.x and 3.x # See: https://bugs.python.org/issue22767 SHORT_SEPARATORS = (',', ':') diff --git a/rest_framework/negotiation.py b/rest_framework/negotiation.py index 76113a827f..b4bbfa1f54 100644 --- a/rest_framework/negotiation.py +++ b/rest_framework/negotiation.py @@ -4,7 +4,7 @@ """ from django.http import Http404 -from rest_framework import HTTP_HEADER_ENCODING, exceptions +from rest_framework import exceptions from rest_framework.settings import api_settings from rest_framework.utils.mediatypes import ( _MediaType, media_type_matches, order_by_precedence @@ -64,9 +64,11 @@ def select_renderer(self, request, renderers, format_suffix=None): # Accepted media type is 'application/json' full_media_type = ';'.join( (renderer.media_type,) + - tuple('{}={}'.format( - key, value.decode(HTTP_HEADER_ENCODING)) - for key, value in media_type_wrapper.params.items())) + tuple( + '{}={}'.format(key, value) + for key, value in media_type_wrapper.params.items() + ) + ) return renderer, full_media_type else: # Eg client requests 'application/json; indent=8' diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index fc4eb14283..4ee8e578b8 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -5,7 +5,6 @@ on the request, such as form content or json encoded data. """ import codecs -from urllib import parse from django.conf import settings from django.core.files.uploadhandler import StopFutureHandlers @@ -13,10 +12,10 @@ from django.http.multipartparser import ChunkIter from django.http.multipartparser import \ MultiPartParser as DjangoMultiPartParser -from django.http.multipartparser import MultiPartParserError, parse_header -from django.utils.encoding import force_str +from django.http.multipartparser import MultiPartParserError from rest_framework import renderers +from rest_framework.compat import parse_header_parameters from rest_framework.exceptions import ParseError from rest_framework.settings import api_settings from rest_framework.utils import json @@ -201,23 +200,10 @@ def get_filename(self, stream, media_type, parser_context): try: meta = parser_context['request'].META - disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION'].encode()) - filename_parm = disposition[1] - if 'filename*' in filename_parm: - return self.get_encoded_filename(filename_parm) - return force_str(filename_parm['filename']) + disposition, params = parse_header_parameters(meta['HTTP_CONTENT_DISPOSITION']) + if 'filename*' in params: + return params['filename*'] + else: + return params['filename'] except (AttributeError, KeyError, ValueError): pass - - def get_encoded_filename(self, filename_parm): - """ - Handle encoded filenames per RFC6266. See also: - https://tools.ietf.org/html/rfc2231#section-4 - """ - encoded_filename = force_str(filename_parm['filename*']) - try: - charset, lang, filename = encoded_filename.split('\'', 2) - filename = parse.unquote(filename) - except (ValueError, LookupError): - filename = force_str(filename_parm['filename']) - return filename diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 8824fa6601..b74df9a0bb 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -14,7 +14,6 @@ from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.core.paginator import Page -from django.http.multipartparser import parse_header from django.template import engines, loader from django.urls import NoReverseMatch from django.utils.html import mark_safe @@ -22,7 +21,7 @@ from rest_framework import VERSION, exceptions, serializers, status from rest_framework.compat import ( INDENT_SEPARATORS, LONG_SEPARATORS, SHORT_SEPARATORS, coreapi, coreschema, - pygments_css, yaml + parse_header_parameters, pygments_css, yaml ) from rest_framework.exceptions import ParseError from rest_framework.request import is_form_media_type, override_method @@ -72,7 +71,7 @@ def get_indent(self, accepted_media_type, renderer_context): # If the media type looks like 'application/json; indent=4', # then pretty print the result. # Note that we coerce `indent=0` into `indent=None`. - base_media_type, params = parse_header(accepted_media_type.encode('ascii')) + base_media_type, params = parse_header_parameters(accepted_media_type) try: return zero_as_none(max(min(int(params['indent']), 8), 0)) except (KeyError, ValueError, TypeError): diff --git a/rest_framework/request.py b/rest_framework/request.py index 17ceadb08e..93634e667d 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -14,11 +14,11 @@ from django.conf import settings from django.http import HttpRequest, QueryDict -from django.http.multipartparser import parse_header from django.http.request import RawPostDataException from django.utils.datastructures import MultiValueDict -from rest_framework import HTTP_HEADER_ENCODING, exceptions +from rest_framework import exceptions +from rest_framework.compat import parse_header_parameters from rest_framework.settings import api_settings @@ -26,7 +26,7 @@ def is_form_media_type(media_type): """ Return True if the media type is a valid form media type. """ - base_media_type, params = parse_header(media_type.encode(HTTP_HEADER_ENCODING)) + base_media_type, params = parse_header_parameters(media_type) return (base_media_type == 'application/x-www-form-urlencoded' or base_media_type == 'multipart/form-data') diff --git a/rest_framework/utils/mediatypes.py b/rest_framework/utils/mediatypes.py index 40bdf26153..b9004d4963 100644 --- a/rest_framework/utils/mediatypes.py +++ b/rest_framework/utils/mediatypes.py @@ -3,9 +3,7 @@ See https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7 """ -from django.http.multipartparser import parse_header - -from rest_framework import HTTP_HEADER_ENCODING +from rest_framework.compat import parse_header_parameters def media_type_matches(lhs, rhs): @@ -46,7 +44,7 @@ def order_by_precedence(media_type_lst): class _MediaType: def __init__(self, media_type_str): self.orig = '' if (media_type_str is None) else media_type_str - self.full_type, self.params = parse_header(self.orig.encode(HTTP_HEADER_ENCODING)) + self.full_type, self.params = parse_header_parameters(self.orig) self.main_type, sep, self.sub_type = self.full_type.partition('/') def match(self, other): @@ -79,5 +77,5 @@ def precedence(self): def __str__(self): ret = "%s/%s" % (self.main_type, self.sub_type) for key, val in self.params.items(): - ret += "; %s=%s" % (key, val.decode('ascii')) + ret += "; %s=%s" % (key, val) return ret