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
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,30 @@ else:
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
# See: https://bugs.python.org/issue22767
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 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 @@ class DefaultContentNegotiation(BaseContentNegotiation):
# 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'

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.
"""
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 import QueryDict
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 @@ class FileUploadParser(BaseParser):
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

View File

@ -14,7 +14,6 @@ from django import forms
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 django.utils.html import mark_safe
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 @@ class JSONRenderer(BaseRenderer):
# 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):

View File

@ -14,11 +14,11 @@ from contextlib import contextmanager
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')

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
"""
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 @@ class _MediaType:
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