diff options
| author | Tom Carrick <tom@carrick.eu> | 2020-07-14 13:32:24 +0200 |
|---|---|---|
| committer | Mariusz Felisiak <felisiak.mariusz@gmail.com> | 2020-09-14 08:41:59 +0200 |
| commit | bcc2befd0e9c1885e45b46d0b0bcdc11def8b249 (patch) | |
| tree | 59fab69a3182286da87fcd6fe05a8ce0f4277a5a /django/http | |
| parent | 71ae1ab0123582cc5bfe0f7d5f4cc19a9412f396 (diff) | |
Fixed #31789 -- Added a new headers interface to HttpResponse.
Diffstat (limited to 'django/http')
| -rw-r--r-- | django/http/response.py | 128 |
1 files changed, 78 insertions, 50 deletions
diff --git a/django/http/response.py b/django/http/response.py index 64ac205087..e679c856c0 100644 --- a/django/http/response.py +++ b/django/http/response.py @@ -5,6 +5,7 @@ import os import re import sys import time +from collections.abc import Mapping from email.header import Header from http.client import responses from urllib.parse import quote, urlparse @@ -15,6 +16,7 @@ from django.core.exceptions import DisallowedRedirect from django.core.serializers.json import DjangoJSONEncoder from django.http.cookie import SimpleCookie from django.utils import timezone +from django.utils.datastructures import CaseInsensitiveMapping from django.utils.encoding import iri_to_uri from django.utils.http import http_date from django.utils.regex_helper import _lazy_re_compile @@ -22,6 +24,65 @@ from django.utils.regex_helper import _lazy_re_compile _charset_from_content_type_re = _lazy_re_compile(r';\s*charset=(?P<charset>[^\s;]+)', re.I) +class ResponseHeaders(CaseInsensitiveMapping): + def __init__(self, data): + """ + Populate the initial data using __setitem__ to ensure values are + correctly encoded. + """ + if not isinstance(data, Mapping): + data = { + k: v + for k, v in CaseInsensitiveMapping._destruct_iterable_mapping_values(data) + } + self._store = {} + for header, value in data.items(): + self[header] = value + + def _convert_to_charset(self, value, charset, mime_encode=False): + """ + Convert headers key/value to ascii/latin-1 native strings. + `charset` must be 'ascii' or 'latin-1'. If `mime_encode` is True and + `value` can't be represented in the given charset, apply MIME-encoding. + """ + if not isinstance(value, (bytes, str)): + value = str(value) + if ( + (isinstance(value, bytes) and (b'\n' in value or b'\r' in value)) or + (isinstance(value, str) and ('\n' in value or '\r' in value)) + ): + raise BadHeaderError("Header values can't contain newlines (got %r)" % value) + try: + if isinstance(value, str): + # Ensure string is valid in given charset + value.encode(charset) + else: + # Convert bytestring using given charset + value = value.decode(charset) + except UnicodeError as e: + if mime_encode: + value = Header(value, 'utf-8', maxlinelen=sys.maxsize).encode() + else: + e.reason += ', HTTP response headers must be in %s format' % charset + raise + return value + + def __delitem__(self, key): + self.pop(key) + + def __setitem__(self, key, value): + key = self._convert_to_charset(key, 'ascii') + value = self._convert_to_charset(value, 'latin-1', mime_encode=True) + self._store[key.lower()] = (key, value) + + def pop(self, key, default=None): + return self._store.pop(key.lower(), default) + + def setdefault(self, key, value): + if key not in self: + self[key] = value + + class BadHeaderError(ValueError): pass @@ -37,10 +98,7 @@ class HttpResponseBase: status_code = 200 def __init__(self, content_type=None, status=None, reason=None, charset=None): - # _headers is a mapping of the lowercase name to the original case of - # the header (required for working with legacy systems) and the header - # value. Both the name of the header and its value are ASCII strings. - self._headers = {} + self.headers = ResponseHeaders({}) self._resource_closers = [] # This parameter is set by the handler. It's necessary to preserve the # historical behavior of request_finished. @@ -95,7 +153,7 @@ class HttpResponseBase: headers = [ (to_bytes(key, 'ascii') + b': ' + to_bytes(value, 'latin-1')) - for key, value in self._headers.values() + for key, value in self.headers.items() ] return b'\r\n'.join(headers) @@ -103,57 +161,28 @@ class HttpResponseBase: @property def _content_type_for_repr(self): - return ', "%s"' % self['Content-Type'] if 'Content-Type' in self else '' - - def _convert_to_charset(self, value, charset, mime_encode=False): - """ - Convert headers key/value to ascii/latin-1 native strings. - - `charset` must be 'ascii' or 'latin-1'. If `mime_encode` is True and - `value` can't be represented in the given charset, apply MIME-encoding. - """ - if not isinstance(value, (bytes, str)): - value = str(value) - if ((isinstance(value, bytes) and (b'\n' in value or b'\r' in value)) or - isinstance(value, str) and ('\n' in value or '\r' in value)): - raise BadHeaderError("Header values can't contain newlines (got %r)" % value) - try: - if isinstance(value, str): - # Ensure string is valid in given charset - value.encode(charset) - else: - # Convert bytestring using given charset - value = value.decode(charset) - except UnicodeError as e: - if mime_encode: - value = Header(value, 'utf-8', maxlinelen=sys.maxsize).encode() - else: - e.reason += ', HTTP response headers must be in %s format' % charset - raise - return value + return ', "%s"' % self.headers['Content-Type'] if 'Content-Type' in self.headers else '' def __setitem__(self, header, value): - header = self._convert_to_charset(header, 'ascii') - value = self._convert_to_charset(value, 'latin-1', mime_encode=True) - self._headers[header.lower()] = (header, value) + self.headers[header] = value def __delitem__(self, header): - self._headers.pop(header.lower(), False) + del self.headers[header] def __getitem__(self, header): - return self._headers[header.lower()][1] + return self.headers[header] def has_header(self, header): """Case-insensitive check for a header.""" - return header.lower() in self._headers + return header in self.headers __contains__ = has_header def items(self): - return self._headers.values() + return self.headers.items() def get(self, header, alternate=None): - return self._headers.get(header.lower(), (None, alternate))[1] + return self.headers.get(header, alternate) def set_cookie(self, key, value='', max_age=None, expires=None, path='/', domain=None, secure=False, httponly=False, samesite=None): @@ -203,8 +232,7 @@ class HttpResponseBase: def setdefault(self, key, value): """Set a header unless it has already been set.""" - if key not in self: - self[key] = value + self.headers.setdefault(key, value) def set_signed_cookie(self, key, value, salt='', **kwargs): value = signing.get_cookie_signer(salt=key + salt).sign(value) @@ -430,19 +458,19 @@ class FileResponse(StreamingHttpResponse): filename = getattr(filelike, 'name', None) filename = filename if (isinstance(filename, str) and filename) else self.filename if os.path.isabs(filename): - self['Content-Length'] = os.path.getsize(filelike.name) + self.headers['Content-Length'] = os.path.getsize(filelike.name) elif hasattr(filelike, 'getbuffer'): - self['Content-Length'] = filelike.getbuffer().nbytes + self.headers['Content-Length'] = filelike.getbuffer().nbytes - if self.get('Content-Type', '').startswith('text/html'): + if self.headers.get('Content-Type', '').startswith('text/html'): if filename: content_type, encoding = mimetypes.guess_type(filename) # Encoding isn't set to prevent browsers from automatically # uncompressing files. content_type = encoding_map.get(encoding, content_type) - self['Content-Type'] = content_type or 'application/octet-stream' + self.headers['Content-Type'] = content_type or 'application/octet-stream' else: - self['Content-Type'] = 'application/octet-stream' + self.headers['Content-Type'] = 'application/octet-stream' filename = self.filename or os.path.basename(filename) if filename: @@ -452,9 +480,9 @@ class FileResponse(StreamingHttpResponse): file_expr = 'filename="{}"'.format(filename) except UnicodeEncodeError: file_expr = "filename*=utf-8''{}".format(quote(filename)) - self['Content-Disposition'] = '{}; {}'.format(disposition, file_expr) + self.headers['Content-Disposition'] = '{}; {}'.format(disposition, file_expr) elif self.as_attachment: - self['Content-Disposition'] = 'attachment' + self.headers['Content-Disposition'] = 'attachment' class HttpResponseRedirectBase(HttpResponse): |
