Replaced parse_header with parse_header_parameters. (#8556)

Add a backwards compatibility shim for Django versions that have no (or an incompatible)
django.utils.http.parse_header_parameters implementation.

Thanks to Shai Berger for review. 

Co-authored-by: Jaap Roes <jroes@leukeleu.nl>
This commit is contained in:
Carlton Gibson 2022-07-14 14:20:36 +02:00 committed by GitHub
parent 101aff6c43
commit ad282da97c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 46 additions and 36 deletions

View File

@ -2,6 +2,7 @@
The `compat` module provides support for backwards compatibility with older The `compat` module provides support for backwards compatibility with older
versions of Django/Python, and compatibility wrappers around optional packages. versions of Django/Python, and compatibility wrappers around optional packages.
""" """
import django
from django.conf import settings from django.conf import settings
from django.views.generic import View from django.views.generic import View
@ -152,6 +153,30 @@ else:
return False 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
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 # `separators` argument to `json.dumps()` differs between 2.x and 3.x
# See: https://bugs.python.org/issue22767 # See: https://bugs.python.org/issue22767
SHORT_SEPARATORS = (',', ':') SHORT_SEPARATORS = (',', ':')

View File

@ -4,7 +4,7 @@ incoming request. Typically this will be based on the request's Accept header.
""" """
from django.http import Http404 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.settings import api_settings
from rest_framework.utils.mediatypes import ( from rest_framework.utils.mediatypes import (
_MediaType, media_type_matches, order_by_precedence _MediaType, media_type_matches, order_by_precedence
@ -64,9 +64,11 @@ class DefaultContentNegotiation(BaseContentNegotiation):
# Accepted media type is 'application/json' # Accepted media type is 'application/json'
full_media_type = ';'.join( full_media_type = ';'.join(
(renderer.media_type,) + (renderer.media_type,) +
tuple('{}={}'.format( tuple(
key, value.decode(HTTP_HEADER_ENCODING)) '{}={}'.format(key, value)
for key, value in media_type_wrapper.params.items())) for key, value in media_type_wrapper.params.items()
)
)
return renderer, full_media_type return renderer, full_media_type
else: else:
# Eg client requests 'application/json; indent=8' # Eg client requests 'application/json; indent=8'

View File

@ -5,7 +5,6 @@ They give us a generic way of being able to handle various media types
on the request, such as form content or json encoded data. on the request, such as form content or json encoded data.
""" """
import codecs import codecs
from urllib import parse
from django.conf import settings from django.conf import settings
from django.core.files.uploadhandler import StopFutureHandlers from django.core.files.uploadhandler import StopFutureHandlers
@ -13,10 +12,10 @@ from django.http import QueryDict
from django.http.multipartparser import ChunkIter from django.http.multipartparser import ChunkIter
from django.http.multipartparser import \ from django.http.multipartparser import \
MultiPartParser as DjangoMultiPartParser MultiPartParser as DjangoMultiPartParser
from django.http.multipartparser import MultiPartParserError, parse_header from django.http.multipartparser import MultiPartParserError
from django.utils.encoding import force_str
from rest_framework import renderers from rest_framework import renderers
from rest_framework.compat import parse_header_parameters
from rest_framework.exceptions import ParseError from rest_framework.exceptions import ParseError
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from rest_framework.utils import json from rest_framework.utils import json
@ -201,23 +200,10 @@ class FileUploadParser(BaseParser):
try: try:
meta = parser_context['request'].META meta = parser_context['request'].META
disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION'].encode()) disposition, params = parse_header_parameters(meta['HTTP_CONTENT_DISPOSITION'])
filename_parm = disposition[1] if 'filename*' in params:
if 'filename*' in filename_parm: return params['filename*']
return self.get_encoded_filename(filename_parm) else:
return force_str(filename_parm['filename']) return params['filename']
except (AttributeError, KeyError, ValueError): except (AttributeError, KeyError, ValueError):
pass 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

View File

@ -14,7 +14,6 @@ from django import forms
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.core.paginator import Page from django.core.paginator import Page
from django.http.multipartparser import parse_header
from django.template import engines, loader from django.template import engines, loader
from django.urls import NoReverseMatch from django.urls import NoReverseMatch
from django.utils.html import mark_safe from django.utils.html import mark_safe
@ -22,7 +21,7 @@ from django.utils.html import mark_safe
from rest_framework import VERSION, exceptions, serializers, status from rest_framework import VERSION, exceptions, serializers, status
from rest_framework.compat import ( from rest_framework.compat import (
INDENT_SEPARATORS, LONG_SEPARATORS, SHORT_SEPARATORS, coreapi, coreschema, 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.exceptions import ParseError
from rest_framework.request import is_form_media_type, override_method from rest_framework.request import is_form_media_type, override_method
@ -72,7 +71,7 @@ class JSONRenderer(BaseRenderer):
# If the media type looks like 'application/json; indent=4', # If the media type looks like 'application/json; indent=4',
# then pretty print the result. # then pretty print the result.
# Note that we coerce `indent=0` into `indent=None`. # 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: try:
return zero_as_none(max(min(int(params['indent']), 8), 0)) return zero_as_none(max(min(int(params['indent']), 8), 0))
except (KeyError, ValueError, TypeError): except (KeyError, ValueError, TypeError):

View File

@ -14,11 +14,11 @@ from contextlib import contextmanager
from django.conf import settings from django.conf import settings
from django.http import HttpRequest, QueryDict from django.http import HttpRequest, QueryDict
from django.http.multipartparser import parse_header
from django.http.request import RawPostDataException from django.http.request import RawPostDataException
from django.utils.datastructures import MultiValueDict 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 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. 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 return (base_media_type == 'application/x-www-form-urlencoded' or
base_media_type == 'multipart/form-data') base_media_type == 'multipart/form-data')

View File

@ -3,9 +3,7 @@ Handling of media types, as found in HTTP Content-Type and Accept headers.
See https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7 See https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
""" """
from django.http.multipartparser import parse_header from rest_framework.compat import parse_header_parameters
from rest_framework import HTTP_HEADER_ENCODING
def media_type_matches(lhs, rhs): def media_type_matches(lhs, rhs):
@ -46,7 +44,7 @@ def order_by_precedence(media_type_lst):
class _MediaType: class _MediaType:
def __init__(self, media_type_str): def __init__(self, media_type_str):
self.orig = '' if (media_type_str is None) else 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('/') self.main_type, sep, self.sub_type = self.full_type.partition('/')
def match(self, other): def match(self, other):
@ -79,5 +77,5 @@ class _MediaType:
def __str__(self): def __str__(self):
ret = "%s/%s" % (self.main_type, self.sub_type) ret = "%s/%s" % (self.main_type, self.sub_type)
for key, val in self.params.items(): for key, val in self.params.items():
ret += "; %s=%s" % (key, val.decode('ascii')) ret += "; %s=%s" % (key, val)
return ret return ret