diff options
| author | Chris McDonough <chrism@plope.com> | 2011-10-10 12:00:59 -0400 |
|---|---|---|
| committer | Chris McDonough <chrism@plope.com> | 2011-10-10 12:00:59 -0400 |
| commit | 4b29bdaf6af8161320eb86ef25031afc11c4c083 (patch) | |
| tree | 6aef2b2fee3496563aa793beb3271d2f481281cc | |
| parent | 7c963af17947612b3b0a67ca09bd60da12594e13 (diff) | |
| download | webob-feature.cookie-bug-for-bug.tar.gz | |
* Mutating the ``request.cookies`` property now reflects the mutations intofeature.cookie-bug-for-bug
the ``HTTP_COOKIES`` environ header.
| -rw-r--r-- | docs/news.txt | 5 | ||||
| -rw-r--r-- | tests/test_cookies.py | 183 | ||||
| -rw-r--r-- | tests/test_request.py | 15 | ||||
| -rw-r--r-- | webob/cookies.py | 134 | ||||
| -rw-r--r-- | webob/request.py | 19 |
5 files changed, 340 insertions, 16 deletions
diff --git a/docs/news.txt b/docs/news.txt index b8fdca6..488733c 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -4,9 +4,8 @@ News master --------- -* Mutating the ``request.cookies`` property now retains the mutated value - during the course of a single request (bug-for-bug compatibility with - previous versions; in-the-wild codebases depend upon this behavior). +* Mutating the ``request.cookies`` property now reflects the mutations into + the ``HTTP_COOKIES`` environ header. * ``Response.etag = (tag, False)`` sets weak etag. diff --git a/tests/test_cookies.py b/tests/test_cookies.py index 923978d..6a4c163 100644 --- a/tests/test_cookies.py +++ b/tests/test_cookies.py @@ -3,6 +3,9 @@ from datetime import timedelta from webob import cookies from webob.compat import text_ from nose.tools import eq_ +import unittest +from webob.compat import native_ +from webob.compat import PY3 def test_cookie_empty(): c = cookies.Cookie() # empty cookie @@ -140,3 +143,183 @@ def test_morsel_repr(): result = repr(v) eq_(result, "<Morsel: a='b'>") +class TestRequestCookies(unittest.TestCase): + def _makeOne(self, environ): + from webob.cookies import RequestCookies + return RequestCookies(environ) + + def test_get_no_cache_key_in_environ_no_http_cookie_header(self): + environ = {} + inst = self._makeOne(environ) + self.assertEqual(inst.get('a'), None) + parsed = environ['webob._parsed_cookies'] + self.assertEqual(parsed, ({}, '')) + + def test_get_no_cache_key_in_environ_has_http_cookie_header(self): + header ='a=1; b=2' + environ = {'HTTP_COOKIE':header} + inst = self._makeOne(environ) + self.assertEqual(inst.get('a'), '1') + parsed = environ['webob._parsed_cookies'][0] + self.assertEqual(parsed['a'], '1') + self.assertEqual(parsed['b'], '2') + self.assertEqual(environ['HTTP_COOKIE'], header) # no change + + def test_get_cache_key_in_environ_no_http_cookie_header(self): + environ = {'webob._parsed_cookies':({}, '')} + inst = self._makeOne(environ) + self.assertEqual(inst.get('a'), None) + parsed = environ['webob._parsed_cookies'] + self.assertEqual(parsed, ({}, '')) + + def test_get_cache_key_in_environ_has_http_cookie_header(self): + header ='a=1; b=2' + environ = {'HTTP_COOKIE':header, 'webob._parsed_cookies':({}, '')} + inst = self._makeOne(environ) + self.assertEqual(inst.get('a'), '1') + parsed = environ['webob._parsed_cookies'][0] + self.assertEqual(parsed['a'], '1') + self.assertEqual(parsed['b'], '2') + self.assertEqual(environ['HTTP_COOKIE'], header) # no change + + def test___setitem__name_not_string_type(self): + inst = self._makeOne({}) + self.assertRaises(TypeError, inst.__setitem__, None, 1) + + def test___setitem__name_not_encodeable_to_ascii(self): + name = native_(b'La Pe\xc3\xb1a', 'utf-8') + inst = self._makeOne({}) + self.assertRaises(TypeError, inst.__setitem__, name, 'abc') + + def test___setitem__name_not_rfc2109_valid(self): + name = '$a' + inst = self._makeOne({}) + self.assertRaises(TypeError, inst.__setitem__, name, 'abc') + + def test___setitem__value_not_string_type(self): + inst = self._makeOne({}) + self.assertRaises(ValueError, inst.__setitem__, 'a', None) + + def test___setitem__value_not_utf_8_decodeable(self): + value = text_(b'La Pe\xc3\xb1a', 'utf-8') + value = value.encode('utf-16') + inst = self._makeOne({}) + self.assertRaises(ValueError, inst.__setitem__, 'a', value) + + def test__setitem__success_no_existing_headers(self): + value = native_(b'La Pe\xc3\xb1a', 'utf-8') + environ = {} + inst = self._makeOne(environ) + inst['a'] = value + self.assertEqual(environ['HTTP_COOKIE'], 'a="La Pe\\303\\261a"') + + def test__setitem__success_append(self): + value = native_(b'La Pe\xc3\xb1a', 'utf-8') + environ = {'HTTP_COOKIE':'a=1; b=2'} + inst = self._makeOne(environ) + inst['c'] = value + self.assertEqual( + environ['HTTP_COOKIE'], 'a=1; b=2; c="La Pe\\303\\261a"') + + def test__setitem__success_replace(self): + environ = {'HTTP_COOKIE':'a=1; b="La Pe\\303\\261a"; c=3'} + inst = self._makeOne(environ) + inst['b'] = 'abc' + self.assertEqual(environ['HTTP_COOKIE'], 'a=1; b=abc; c=3') + inst['c'] = '4' + self.assertEqual(environ['HTTP_COOKIE'], 'a=1; b=abc; c=4') + + def test__delitem__fail_no_http_cookie(self): + environ = {} + inst = self._makeOne(environ) + self.assertRaises(KeyError, inst.__delitem__, 'a') + self.assertEqual(environ, {}) + + def test__delitem__fail_with_http_cookie(self): + environ = {'HTTP_COOKIE':''} + inst = self._makeOne(environ) + self.assertRaises(KeyError, inst.__delitem__, 'a') + self.assertEqual(environ, {'HTTP_COOKIE':''}) + + def test__delitem__success(self): + environ = {'HTTP_COOKIE':'a=1'} + inst = self._makeOne(environ) + del inst['a'] + self.assertEqual(environ['HTTP_COOKIE'], '') + self.assertEqual(inst._cache, {}) + + def test_keys(self): + environ = {'HTTP_COOKIE':'a=1; b="La Pe\\303\\261a"; c=3'} + inst = self._makeOne(environ) + self.assertEqual(sorted(list(inst.keys())), ['a', 'b', 'c']) + + def test_values(self): + val = text_(b'La Pe\xc3\xb1a', 'utf-8') + environ = {'HTTP_COOKIE':'a=1; b="La Pe\\303\\261a"; c=3'} + inst = self._makeOne(environ) + self.assertEqual(sorted(list(inst.values())), ['1', '3', val]) + + def test_items(self): + val = text_(b'La Pe\xc3\xb1a', 'utf-8') + environ = {'HTTP_COOKIE':'a=1; b="La Pe\\303\\261a"; c=3'} + inst = self._makeOne(environ) + self.assertEqual(sorted(list(inst.items())), + [('a', '1'), ('b', val), ('c', '3')]) + + if not PY3: + def test_iterkeys(self): + environ = {'HTTP_COOKIE':'a=1; b="La Pe\\303\\261a"; c=3'} + inst = self._makeOne(environ) + self.assertEqual(sorted(list(inst.iterkeys())), ['a', 'b', 'c']) + + def test_itervalues(self): + val = text_(b'La Pe\xc3\xb1a', 'utf-8') + environ = {'HTTP_COOKIE':'a=1; b="La Pe\\303\\261a"; c=3'} + inst = self._makeOne(environ) + self.assertEqual(sorted(list(inst.itervalues())), ['1', '3', val]) + + def test_iteritems(self): + val = text_(b'La Pe\xc3\xb1a', 'utf-8') + environ = {'HTTP_COOKIE':'a=1; b="La Pe\\303\\261a"; c=3'} + inst = self._makeOne(environ) + self.assertEqual(sorted(list(inst.iteritems())), + [('a', '1'), ('b', val), ('c', '3')]) + + def test___contains__(self): + environ = {'HTTP_COOKIE':'a=1'} + inst = self._makeOne(environ) + self.assertTrue('a' in inst) + self.assertFalse('b' in inst) + + def test___iter__(self): + environ = {'HTTP_COOKIE':'a=1; b=2; c=3'} + inst = self._makeOne(environ) + self.assertEqual(sorted(list(iter(inst))), ['a', 'b', 'c']) + + def test___len__(self): + environ = {'HTTP_COOKIE':'a=1; b=2; c=3'} + inst = self._makeOne(environ) + self.assertEqual(len(inst), 3) + del inst['a'] + self.assertEqual(len(inst), 2) + + def test_clear(self): + environ = {'HTTP_COOKIE':'a=1; b=2; c=3'} + inst = self._makeOne(environ) + inst.clear() + self.assertEqual(environ['HTTP_COOKIE'], '') + self.assertEqual(inst.get('a'), None) + + def test___repr__(self): + environ = {'HTTP_COOKIE':'a=1; b=2; c=3'} + inst = self._makeOne(environ) + r = repr(inst) + self.assertTrue(r.startswith( + '<RequestCookies (dict-like) with values ')) + self.assertTrue(r.endswith('>')) + + + + + + diff --git a/tests/test_request.py b/tests/test_request.py index 9120425..70956db 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -1,3 +1,4 @@ +import collections import unittest, warnings from webob.request import Request from webob.request import BaseRequest @@ -790,6 +791,16 @@ class BaseRequestTests(unittest.TestCase): req = BaseRequest(environ) self.assertEqual(req.cookies, {'a': 'b'}) + def test_set_cookies(self): + environ = { + 'HTTP_COOKIE': 'a=b', + } + req = BaseRequest(environ) + req.cookies = {'a':'1', 'b': '2'} + self.assertEqual(req.cookies, {'a': '1', 'b':'2'}) + rcookies = [x.strip() for x in environ['HTTP_COOKIE'].split(';')] + self.assertEqual(sorted(rcookies), ['a=1', 'b=2']) + def test_is_xhr_no_header(self): req = BaseRequest({}) self.assert_(not req.is_xhr) @@ -1531,7 +1542,7 @@ class RequestTests_functional(unittest.TestCase): 'datetime.datetime(1994, 10, 29, 19, 43, 31, tzinfo=UTC)', "user_agent: 'Mozilla", 'is_xhr: True', - "cookies is {", + "cookies is <RequestCookies", 'var1', 'value1', 'params is NestedMultiDict', @@ -2237,7 +2248,7 @@ class RequestTests_functional(unittest.TestCase): # Cookies req.headers['Cookie'] = 'test=value' - self.assert_(isinstance(req.cookies, dict)) + self.assert_(isinstance(req.cookies, collections.MutableMapping)) self.assertEqual(list(req.cookies.items()), [('test', 'value')]) req.charset = None self.assertEqual(req.cookies, {'test': 'value'}) diff --git a/webob/cookies.py b/webob/cookies.py index aa6f65e..1b5d907 100644 --- a/webob/cookies.py +++ b/webob/cookies.py @@ -1,3 +1,5 @@ +import collections + from datetime import ( date, datetime, @@ -11,11 +13,143 @@ from webob.compat import ( PY3, text_type, bytes_, + text_, native_, + string_types, ) __all__ = ['Cookie'] +_marker = object() + +class RequestCookies(collections.MutableMapping): + + _cache_key = 'webob._parsed_cookies' + + def __init__(self, environ): + self._environ = environ + + @property + def _cache(self): + env = self._environ + header = env.get('HTTP_COOKIE', '') + cache, cache_header = env.get(self._cache_key, ({}, None)) + if cache_header == header: + return cache + d = lambda b: b.decode('utf8') + cache = dict((d(k), d(v)) for k,v in parse_cookie(header)) + env[self._cache_key] = (cache, header) + return cache + + def _mutate_header(self, name, value): + header = self._environ.get('HTTP_COOKIE') + had_header = header is not None + header = header or '' + if PY3: # pragma: no cover + header = header.encode('latin-1') + bytes_name = bytes_(name, 'ascii') + if value is None: + replacement = None + else: + bytes_val = _quote(bytes_(value, 'utf-8')) + replacement = bytes_name + b'=' + bytes_val + matches = _rx_cookie.finditer(header) + found = False + for match in matches: + start, end = match.span() + match_name = match.group(1) + if match_name == bytes_name: + found = True + if replacement is None: # remove value + header = header[:start].rstrip(b' ;') + header[end:] + else: # replace value + header = header[:start] + replacement + header[end:] + break + else: + if replacement is not None: + if header: + header += b'; ' + replacement + else: + header = replacement + + if header: + self._environ['HTTP_COOKIE'] = native_(header, 'latin-1') + elif had_header: + self._environ['HTTP_COOKIE'] = '' + + return found + + def _valid_cookie_name(self, name): + if not isinstance(name, string_types): + raise TypeError(name, 'cookie name must be a string') + if not isinstance(name, text_type): + name = text_(name, 'utf-8') + try: + bytes_cookie_name = bytes_(name, 'ascii') + except UnicodeEncodeError: + raise TypeError('cookie name must be encodable to ascii') + if not _valid_cookie_name(bytes_cookie_name): + raise TypeError('cookie name must be valid according to RFC 2109') + return name + + def __setitem__(self, name, value): + name = self._valid_cookie_name(name) + if not isinstance(value, string_types): + raise ValueError(value, 'cookie value must be a string') + if not isinstance(value, text_type): + try: + value = text_(value, 'utf-8') + except UnicodeDecodeError: + raise ValueError( + value, 'cookie value must be utf-8 binary or unicode') + self._mutate_header(name, value) + + def __getitem__(self, name): + return self._cache[name] + + def get(self, name, default=None): + return self._cache.get(name) + + def __delitem__(self, name): + name = self._valid_cookie_name(name) + found = self._mutate_header(name, None) + if not found: + raise KeyError(name) + + def keys(self): + return self._cache.keys() + + def values(self): + return self._cache.values() + + def items(self): + return self._cache.items() + + if not PY3: + def iterkeys(self): + return self._cache.iterkeys() + + def itervalues(self): + return self._cache.itervalues() + + def iteritems(self): + return self._cache.iteritems() + + def __contains__(self, name): + return name in self._cache + + def __iter__(self): + return self._cache.__iter__() + + def __len__(self): + return len(self._cache) + + def clear(self): + self._environ['HTTP_COOKIE'] = '' + + def __repr__(self): + return '<RequestCookies (dict-like) with values %r>' % (self._cache,) + class Cookie(dict): def __init__(self, input=None): if input: diff --git a/webob/request.py b/webob/request.py index bf6e826..f81cf9e 100644 --- a/webob/request.py +++ b/webob/request.py @@ -35,7 +35,7 @@ from webob.compat import ( urlparse, ) -from webob.cookies import parse_cookie +from webob.cookies import RequestCookies from webob.descriptors import ( CHARSET_RE, @@ -682,16 +682,13 @@ class BaseRequest(object): """ Return a dictionary of cookies as found in the request. """ - env = self.environ - data = self.environ.get('HTTP_COOKIE', '') - if 'webob._parsed_cookies' in env: - vars, var_source = env['webob._parsed_cookies'] - if var_source == data: - return vars - d = lambda b: b.decode('utf8') - vars = dict((d(k), d(v)) for k,v in parse_cookie(data)) - env['webob._parsed_cookies'] = (vars, data) - return vars + return RequestCookies(self.environ) + + @cookies.setter + def cookies(self, val): + self.environ.pop('HTTP_COOKIE', None) + r = RequestCookies(self.environ) + r.update(val) def copy(self): """ |
