diff --git a/django/http/__init__.py b/django/http/__init__.py index 7b0c469c51..6353637a95 100644 --- a/django/http/__init__.py +++ b/django/http/__init__.py @@ -248,12 +248,40 @@ class QueryDict(MultiValueDict): output.extend([urlencode({k: smart_str(v, self.encoding)}) for v in list_]) return '&'.join(output) +class CompatCookie(SimpleCookie): + """ + Cookie class that handles some issues with browser compatibility. + """ + def value_encode(self, val): + # Some browsers do not support quoted-string from RFC 2109, + # including some versions of Safari and Internet Explorer. + # These browsers split on ';', and some versions of Safari + # are known to split on ', '. Therefore, we encode ';' and ',' + + # SimpleCookie already does the hard work of encoding and decoding. + # It uses octal sequences like '\\012' for newline etc. + # and non-ASCII chars. We just make use of this mechanism, to + # avoid introducing two encoding schemes which would be confusing + # and especially awkward for javascript. + + # NB, contrary to Python docs, value_encode returns a tuple containing + # (real val, encoded_val) + val, encoded = super(CompatCookie, self).value_encode(val) + + encoded = encoded.replace(";", "\\073").replace(",","\\054") + # If encoded now contains any quoted chars, we need double quotes + # around the whole string. + if "\\" in encoded and not encoded.startswith('"'): + encoded = '"' + encoded + '"' + + return val, encoded + def parse_cookie(cookie): if cookie == '': return {} if not isinstance(cookie, BaseCookie): try: - c = SimpleCookie() + c = CompatCookie() c.load(cookie) except CookieError: # Invalid cookie @@ -288,7 +316,7 @@ class HttpResponse(object): else: self._container = [content] self._is_string = True - self.cookies = SimpleCookie() + self.cookies = CompatCookie() if status: self.status_code = status diff --git a/docs/releases/1.2.txt b/docs/releases/1.2.txt index f5e5f8118b..a2c70fed06 100644 --- a/docs/releases/1.2.txt +++ b/docs/releases/1.2.txt @@ -240,6 +240,15 @@ status code for the test runner is now 0 for success (no failing tests) and 1 for any number of test failures. If needed, the number of test failures can be found at the end of the test runner's output. +Cookie quoting +-------------- + +Due to bugs and variations in web browsers, we've slightly changed the way that +cookie values are encoded. Comma (',') and semi-colon (';') now use the same +octal encoding mechanism that is used for other special characters. If you +were not previously storing either of these two characters in cookies you are +not affected. + .. _deprecated-features-1.2: Features deprecated in 1.2 diff --git a/tests/regressiontests/httpwrappers/tests.py b/tests/regressiontests/httpwrappers/tests.py index 04099be16e..09da4476b4 100644 --- a/tests/regressiontests/httpwrappers/tests.py +++ b/tests/regressiontests/httpwrappers/tests.py @@ -465,7 +465,40 @@ BadHeaderError: Header values can't contain newlines (got 'test\\nstr') [u'1', u'2', u'3', u'4'] """ -from django.http import QueryDict, HttpResponse +from django.http import QueryDict, HttpResponse, CompatCookie +from django.test import TestCase + + +class Cookies(TestCase): + + def test_encode(self): + """ + Test that we don't output tricky characters in encoded value + """ + c = CompatCookie() + c['test'] = "An,awkward;value" + self.assert_(";" not in c.output()) # IE compat + self.assert_("," not in c.output()) # Safari compat + + def test_decode(self): + """ + Test that we can still preserve semi-colons and commas + """ + c = CompatCookie() + c['test'] = "An,awkward;value" + c2 = CompatCookie() + c2.load(c.output()) + self.assertEqual(c['test'].value, c2['test'].value) + + def test_decode_2(self): + """ + Test that we haven't broken normal encoding + """ + c = CompatCookie() + c['test'] = "\xf0" + c2 = CompatCookie() + c2.load(c.output()) + self.assertEqual(c['test'].value, c2['test'].value) if __name__ == "__main__": import doctest