Code

Ticket #18916: 18916.diff

File 18916.diff, 6.5 KB (added by aaugustin, 20 months ago)
Line 
1diff --git a/django/http/__init__.py b/django/http/__init__.py
2index 2198f38..b9f29ae 100644
3--- a/django/http/__init__.py
4+++ b/django/http/__init__.py
5@@ -2,6 +2,7 @@ from __future__ import absolute_import, unicode_literals
6 
7 import copy
8 import datetime
9+from email.header import Header
10 import os
11 import re
12 import sys
13@@ -560,31 +561,37 @@ class HttpResponse(object):
14     else:
15         __str__ = serialize
16 
17-    def _convert_to_ascii(self, *values):
18-        """Converts all values to ascii strings."""
19-        for value in values:
20-            if not isinstance(value, six.string_types):
21-                value = str(value)
22-            try:
23-                if six.PY3:
24-                    # Ensure string only contains ASCII
25-                    value.encode('us-ascii')
26+    def _convert_to_charset(self, value, charset, mime_encode=False):
27+        """Converts headers key/value to ascii/latin1 native strings
28+
29+        and optionally MIME-encode if the input doesn't fit in the charset.
30+        """
31+        if not isinstance(value, six.string_types):
32+            value = str(value)
33+        try:
34+            if six.PY3:
35+                # Ensure string is valid in given charset
36+                value.encode(charset)
37+            else:
38+                if isinstance(value, str):
39+                    # Ensure string is valid in given charset
40+                    value.decode(charset)
41                 else:
42-                    if isinstance(value, str):
43-                        # Ensure string only contains ASCII
44-                        value.decode('us-ascii')
45-                    else:
46-                        # Convert unicode to an ASCII string
47-                        value = value.encode('us-ascii')
48-            except UnicodeError as e:
49-                e.reason += ', HTTP response headers must be in US-ASCII format'
50+                    # Convert unicode string to given charset
51+                    value = value.encode(charset)
52+        except UnicodeError as e:
53+            if mime_encode:
54+                value = Header(value, 'utf-8').encode()
55+            else:
56+                e.reason += ', HTTP response headers must be in %s format' % charset
57                 raise
58-            if '\n' in value or '\r' in value:
59-                raise BadHeaderError("Header values can't contain newlines (got %r)" % value)
60-            yield value
61+        if str('\n') in value or str('\r') in value:
62+            raise BadHeaderError("Header values can't contain newlines (got %r)" % value)
63+        return value
64 
65     def __setitem__(self, header, value):
66-        header, value = self._convert_to_ascii(header, value)
67+        header = self._convert_to_charset(header, 'ascii')
68+        value = self._convert_to_charset(value, 'latin1', mime_encode=True)
69         self._headers[header.lower()] = (header, value)
70 
71     def __delitem__(self, header):
72diff --git a/tests/regressiontests/httpwrappers/tests.py b/tests/regressiontests/httpwrappers/tests.py
73index 21ba198..4c6aed1 100644
74--- a/tests/regressiontests/httpwrappers/tests.py
75+++ b/tests/regressiontests/httpwrappers/tests.py
76@@ -11,6 +11,7 @@ from django.http import (QueryDict, HttpResponse, HttpResponseRedirect,
77                          SimpleCookie, BadHeaderError,
78                          parse_cookie)
79 from django.test import TestCase
80+from django.utils.encoding import smart_str
81 from django.utils import six
82 from django.utils import unittest
83 
84@@ -228,33 +229,52 @@ class QueryDictTests(unittest.TestCase):
85         self.assertEqual(copy.deepcopy(q).encoding, 'iso-8859-15')
86 
87 class HttpResponseTests(unittest.TestCase):
88-    def test_unicode_headers(self):
89-        r = HttpResponse()
90-
91-        # If we insert a unicode value it will be converted to an ascii
92-        r['value'] = 'test value'
93-        self.assertTrue(isinstance(r['value'], str))
94-
95-        # An error is raised when a unicode object with non-ascii is assigned.
96-        self.assertRaises(UnicodeEncodeError, r.__setitem__, 'value', 't\xebst value')
97 
98-        # An error is raised when  a unicode object with non-ASCII format is
99-        # passed as initial mimetype or content_type.
100-        self.assertRaises(UnicodeEncodeError, HttpResponse,
101-                content_type='t\xebst value')
102+    def test_headers_type(self):
103+        r = HttpResponse()
104 
105-        # HttpResponse headers must be convertible to ASCII.
106-        self.assertRaises(UnicodeEncodeError, HttpResponse,
107-                content_type='t\xebst value')
108+        # The following tests explicitly test types in addition to values
109+        # because in Python 2 u'foo' == b'foo'.
110+
111+        # ASCII unicode or bytes values are converted to native strings.
112+        r['key'] = 'test'
113+        self.assertEqual(r['key'], str('test'))
114+        self.assertIsInstance(r['key'], str)
115+        r['key'] = 'test'.encode('ascii')
116+        self.assertEqual(r['key'], str('test'))
117+        self.assertIsInstance(r['key'], str)
118+
119+        # Latin-1 unicode or bytes values are also converted to native strings.
120+        r['key'] = 'café'
121+        self.assertEqual(r['key'], smart_str('café', 'latin-1'))
122+        self.assertIsInstance(r['key'], str)
123+        r['key'] = 'café'.encode('latin-1')
124+        self.assertEqual(r['key'], smart_str('café', 'latin-1'))
125+        self.assertIsInstance(r['key'], str)
126+
127+        # Other unicode values are MIME-encoded (there's no way to pass them as bytes).
128+        r['key'] = '†'
129+        self.assertEqual(r['key'], str('=?utf-8?b?4oCg?='))
130+        self.assertIsInstance(r['key'], str)
131+
132+        # The response also converts unicode or bytes keys to strings, but requires
133+        # them to contain ASCII
134+        r = HttpResponse()
135+        r['foo'] = 'bar'
136+        l = list(r.items())
137+        self.assertEqual(l[0], ('foo', 'bar'))
138+        self.assertIsInstance(l[0][0], str)
139 
140-        # The response also converts unicode keys to strings.)
141-        r['test'] = 'testing key'
142+        r = HttpResponse()
143+        r[b'foo'] = 'bar'
144         l = list(r.items())
145-        l.sort()
146-        self.assertEqual(l[1], ('test', 'testing key'))
147+        self.assertEqual(l[0], ('foo', 'bar'))
148+        self.assertIsInstance(l[0][0], str)
149+
150+        r = HttpResponse()
151+        self.assertRaises(UnicodeError, r.__setitem__, 'føø', 'bar')
152+        self.assertRaises(UnicodeError, r.__setitem__, 'føø'.encode('utf-8'), 'bar')
153 
154-        # It will also raise errors for keys with non-ascii data.
155-        self.assertRaises(UnicodeEncodeError, r.__setitem__, 't\xebst key', 'value')
156 
157     def test_newlines_in_headers(self):
158         # Bug #10188: Do not allow newlines in headers (CR or LF)