diff -r 9a55a2a1dc6f Lib/http/cookies.py --- a/Lib/http/cookies.py Tue Feb 17 23:36:19 2015 +0100 +++ b/Lib/http/cookies.py Wed Feb 18 07:54:14 2015 -0800 @@ -157,6 +157,10 @@ # _Translator hash-table for fast quoting # _LegalChars = string.ascii_letters + string.digits + "!#$%&'*+-.^_`|~:" +_LegalMorselKeyTmpl = '^[{}]+$' +_LegalMorselKeyPatt = re.compile( + _LegalMorselKeyTmpl.format(re.escape(_LegalChars))) + _Translator = { '\000' : '\\000', '\001' : '\\001', '\002' : '\\002', '\003' : '\\003', '\004' : '\\004', '\005' : '\\005', @@ -229,7 +233,7 @@ string. Otherwise, surround the string in doublequotes and quote (with a \) special characters. """ - if all(c in LegalChars for c in str): + if str is None or all(c in LegalChars for c in str): return str else: return '"' + _nulljoin(_Translator.get(s, s) for s in str) + '"' @@ -241,7 +245,7 @@ def _unquote(str): # If there aren't any doublequotes, # then there can't be any special characters. See RFC 2109. - if len(str) < 2: + if str is None or len(str) < 2: return str if str[0] != '"' or str[-1] != '"': return str @@ -339,11 +343,46 @@ def __init__(self): # Set defaults - self.key = self.value = self.coded_value = None + self._key = self._value = self._coded_value = None - # Set default attributes - for key in self._reserved: - dict.__setitem__(self, key, "") + self._legal_key_patt = _LegalMorselKeyPatt + + # Set required flags + self['secure'] = False + self['httponly'] = False + + @property + def key(self): + return self._key + + @key.setter + def key(self, key): + if key is not None: + # First we verify that the key isn't a reserved word + # Second we make sure it only contains legal characters + if key.lower() in self._reserved: + raise CookieError("Attempt to set a reserved key: %s" % key) + if not self._legal_key_patt.match(key): + raise CookieError("Illegal key value: %s" % key) + self._key = key + + @property + def value(self): + return self._value + + @value.setter + def value(self, value): + self._coded_value = _quote(value) + self._value = value + + @property + def coded_value(self): + return self._coded_value + + @coded_value.setter + def coded_value(self, coded_value): + self._value = _unquote(coded_value) + self._coded_value = coded_value def __setitem__(self, K, V): K = K.lower() @@ -351,21 +390,47 @@ raise CookieError("Invalid Attribute %s" % K) dict.__setitem__(self, K, V) + def __eq__(self, morsel): + return dict.__eq__(self, morsel) and \ + self.value == morsel.value and self.key == morsel.key and \ + self.coded_value == morsel.coded_value + + def __copy__(self): + morsel = Morsel() + morsel.update(self) + for attr in self.__dict__.keys(): + setattr(morsel, attr, getattr(self, attr)) + return morsel + + def __len__(self): + return len([key for key, val in self.items() if val != '']) + + def update(self, values): + invalid_attrs = [ + key for key in values.keys() if key.lower() not in self._reserved] + + if len(invalid_attrs) > 0: + raise CookieError( + 'Invalid Attibutes {}'.format(', '.join(invalid_attrs))) + + dict.update(self, values) + def isReservedKey(self, K): return K.lower() in self._reserved def set(self, key, val, coded_val, LegalChars=_LegalChars): - # First we verify that the key isn't a reserved word - # Second we make sure it only contains legal characters - if key.lower() in self._reserved: - raise CookieError("Attempt to set a reserved key: %s" % key) - if any(c not in LegalChars for c in key): - raise CookieError("Illegal key value: %s" % key) + if LegalChars != _LegalChars: + self._legal_key_patt = re.compile( + _LegalMorselKeyTmpl.format(re.escape(LegalChars))) - # It's a good key, so save it. self.key = key - self.value = val - self.coded_value = coded_val + + # this is a bit of a hack, allowing for unrelated .value and + # .coded_value. this should never actually happen in practice, but + # as it was previously allowed, the functionality should be kept for + # the short term. + self._value = val + self._coded_value = coded_val def output(self, attrs=None, header="Set-Cookie:"): return "%s %s" % (header, self.OutputString(attrs)) @@ -373,8 +438,8 @@ __str__ = output def __repr__(self): - return '<%s: %s=%s>' % (self.__class__.__name__, - self.key, repr(self.value)) + return '<{}: {}>'.format( + self.__class__.__name__, self.OutputString()) def js_output(self, attrs=None): # Print javascript @@ -408,9 +473,9 @@ append("%s=%s" % (self._reserved[key], _getdate(value))) elif key == "max-age" and isinstance(value, int): append("%s=%d" % (self._reserved[key], value)) - elif key == "secure": - append(str(self._reserved[key])) - elif key == "httponly": + elif key in ('secure', 'httponly'): + if not value: + continue append(str(self._reserved[key])) else: append("%s=%s" % (self._reserved[key], value)) diff -r 9a55a2a1dc6f Lib/test/test_http_cookies.py --- a/Lib/test/test_http_cookies.py Tue Feb 17 23:36:19 2015 +0100 +++ b/Lib/test/test_http_cookies.py Wed Feb 18 07:54:14 2015 -0800 @@ -1,5 +1,6 @@ # Simple test suite for http/cookies.py +import copy from test.support import run_unittest, run_doctest, check_warnings import unittest from http import cookies @@ -243,6 +244,91 @@ self.assertRaises(cookies.CookieError, M.set, i, '%s_value' % i, '%s_value' % i) + def test_property_side_effects(self): + morsel = cookies.Morsel() + + expected = 'foo' + morsel.value = expected + self.assertEqual(morsel.coded_value, cookies._quote(expected)) + + expected = 'bar' + morsel.coded_value = expected + self.assertEqual(morsel.value, cookies._unquote(expected)) + + expected = '"foo"; bar; \'baz\'' + morsel.value = expected + self.assertEqual(morsel.coded_value, cookies._quote(expected)) + + expected = cookies._quote('"foo"; bar; \'baz\'') + morsel.coded_value = expected + self.assertEqual(morsel.value, cookies._unquote(expected)) + + def test_eq(self): + base_case = ('key', 'value', 'value') + cases = ( + (base_case, ('key', 'value', 'mismatch')), + (base_case, ('key', 'mismatch', 'value')), + (base_case, ('mismatch', 'value', 'value')), + ) + for case_a, case_b in cases: + morsel_a = cookies.Morsel() + morsel_b = cookies.Morsel() + + morsel_a.set(*case_a) + morsel_b.set(*case_b) + + self.assertEqual(morsel_a == morsel_b, case_a == case_b) + + def test_copy(self): + morsel_a = cookies.Morsel() + morsel_b = copy.copy(morsel_a) + + self.assertTrue(isinstance(morsel_b, cookies.Morsel)) + self.assertNotEqual(id(morsel_a), id(morsel_b)) + self.assertEqual(morsel_a, morsel_b) + + def test_update(self): + morsel = cookies.Morsel() + morsel['expires'] = 0 + self.assertEqual(morsel['expires'], 0) + + morsel.update({'expires': 1}) + self.assertEqual(morsel['expires'], 1) + + # invalid keys should be treated consistently between .__setitem__() + # and .update() + self.assertRaises( + cookies.CookieError, morsel.__setitem__, 'foo', 'bar') + self.assertRaises( + cookies.CookieError, morsel.update, {'foo': 'bar'}) + + def test_repr(self): + morsel = cookies.Morsel() + morsel.update({ + 'expires': 0, + 'path': '/', + 'comment': 'foo', + 'domain': 'example.com', + 'max-age': 0, + 'secure': 1, + 'httponly': 1, + 'version': 1, + }) + morsel.set('key', 'val', 'coded_val') + self.assertEqual( + repr(morsel), '<{}: {}>'.format( + morsel.__class__.__name__, morsel.OutputString())) + + def test_len(self): + morsel = cookies.Morsel() + # morsel flags are set by default + base_len = len(morsel._flags) + self.assertEqual(len(morsel), base_len) + morsel['expires'] = 0 + self.assertEqual(len(morsel), base_len + 1) + + morsel.set('foo', 'bar', 'bar') + self.assertEqual(len(morsel), base_len + 1) def test_main(): run_unittest(CookieTests, MorselTests)