Skip to content

Commit

Permalink
Replaced parse_header with parse_header_parameters. (#8556)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
carltongibson and jaap3 authored Jul 14, 2022
1 parent 101aff6 commit ad282da
Show file tree
Hide file tree
Showing 6 changed files with 46 additions and 36 deletions.
25 changes: 25 additions & 0 deletions rest_framework/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -152,6 +153,30 @@ 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
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 = (',', ':')
Expand Down
10 changes: 6 additions & 4 deletions rest_framework/negotiation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand Down
28 changes: 7 additions & 21 deletions rest_framework/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,17 @@
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
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
Expand Down Expand Up @@ -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
5 changes: 2 additions & 3 deletions rest_framework/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,14 @@
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

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
Expand Down Expand Up @@ -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):
Expand Down
6 changes: 3 additions & 3 deletions rest_framework/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,19 @@

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


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')

Expand Down
8 changes: 3 additions & 5 deletions rest_framework/utils/mediatypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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

1 comment on commit ad282da

@carltongibson
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jaap3 was main author here. GitHub's squash and merge still isn't great.

Please sign in to comment.