1
import re, time, string
2
from datetime import datetime, date, timedelta
12
from webob.compat import (
4
21
__all__ = ['Cookie']
25
class RequestCookies(collections.MutableMapping):
27
_cache_key = 'webob._parsed_cookies'
29
def __init__(self, environ):
30
self._environ = environ
35
header = env.get('HTTP_COOKIE', '')
36
cache, cache_header = env.get(self._cache_key, ({}, None))
37
if cache_header == header:
39
d = lambda b: b.decode('utf8')
40
cache = dict((d(k), d(v)) for k,v in parse_cookie(header))
41
env[self._cache_key] = (cache, header)
44
def _mutate_header(self, name, value):
45
header = self._environ.get('HTTP_COOKIE')
46
had_header = header is not None
48
if PY3: # pragma: no cover
49
header = header.encode('latin-1')
50
bytes_name = bytes_(name, 'ascii')
54
bytes_val = _quote(bytes_(value, 'utf-8'))
55
replacement = bytes_name + b'=' + bytes_val
56
matches = _rx_cookie.finditer(header)
59
start, end = match.span()
60
match_name = match.group(1)
61
if match_name == bytes_name:
63
if replacement is None: # remove value
64
header = header[:start].rstrip(b' ;') + header[end:]
66
header = header[:start] + replacement + header[end:]
69
if replacement is not None:
71
header += b'; ' + replacement
76
self._environ['HTTP_COOKIE'] = native_(header, 'latin-1')
78
self._environ['HTTP_COOKIE'] = ''
82
def _valid_cookie_name(self, name):
83
if not isinstance(name, string_types):
84
raise TypeError(name, 'cookie name must be a string')
85
if not isinstance(name, text_type):
86
name = text_(name, 'utf-8')
88
bytes_cookie_name = bytes_(name, 'ascii')
89
except UnicodeEncodeError:
90
raise TypeError('cookie name must be encodable to ascii')
91
if not _valid_cookie_name(bytes_cookie_name):
92
raise TypeError('cookie name must be valid according to RFC 2109')
95
def __setitem__(self, name, value):
96
name = self._valid_cookie_name(name)
97
if not isinstance(value, string_types):
98
raise ValueError(value, 'cookie value must be a string')
99
if not isinstance(value, text_type):
101
value = text_(value, 'utf-8')
102
except UnicodeDecodeError:
104
value, 'cookie value must be utf-8 binary or unicode')
105
self._mutate_header(name, value)
107
def __getitem__(self, name):
108
return self._cache[name]
110
def get(self, name, default=None):
111
return self._cache.get(name, default)
113
def __delitem__(self, name):
114
name = self._valid_cookie_name(name)
115
found = self._mutate_header(name, None)
120
return self._cache.keys()
123
return self._cache.values()
126
return self._cache.items()
130
return self._cache.iterkeys()
132
def itervalues(self):
133
return self._cache.itervalues()
136
return self._cache.iteritems()
138
def __contains__(self, name):
139
return name in self._cache
142
return self._cache.__iter__()
145
return len(self._cache)
148
self._environ['HTTP_COOKIE'] = ''
151
return '<RequestCookies (dict-like) with values %r>' % (self._cache,)
6
153
class Cookie(dict):
7
154
def __init__(self, input=None):
11
158
def load(self, data):
13
for key, val in _rx_cookie.findall(data):
160
for key, val in _parse_cookie(data):
14
161
if key.lower() in _c_keys:
16
self[ckey][key] = _unquote(val)
18
# RFC2109: NAMEs that begin with $ are reserved for other uses
19
# and must not be used by applications.
22
self[key] = _unquote(val)
164
morsel = self.add(key, val)
25
def __setitem__(self, key, val):
26
if _valid_cookie_name(key):
27
dict.__setitem__(self, key, Morsel(key, val))
166
def add(self, key, val):
167
if not isinstance(key, bytes):
168
key = key.encode('ascii', 'replace')
169
if not _valid_cookie_name(key):
172
dict.__setitem__(self, key, r)
29
176
def serialize(self, full=True):
30
177
return '; '.join(m.serialize(full) for m in self.values())
33
return [m for _,m in sorted(self.items())]
180
return [m for _, m in sorted(self.items())]
35
182
__str__ = serialize
64
225
if isinstance(v, (datetime, date)):
66
227
r = time.strftime('%%s, %d-%%s-%Y %H:%M:%S GMT', v)
67
return r % (weekdays[v[6]], months[v[1]])
228
return bytes_(r % (weekdays[v[6]], months[v[1]]), 'ascii')
69
230
class Morsel(dict):
70
231
__slots__ = ('name', 'value')
71
232
def __init__(self, name, value):
72
assert name.lower() not in _c_keys
73
233
assert _valid_cookie_name(name)
74
assert isinstance(value, str)
234
assert isinstance(value, bytes)
76
# we can encode the unicode value as UTF-8 here,
77
# but then the decoded cookie would still be str,
79
236
self.value = value
80
237
self.update(dict.fromkeys(_c_keys, None))
82
path = cookie_property('path')
83
domain = cookie_property('domain')
84
comment = cookie_property('comment')
85
expires = cookie_property('expires', serialize_cookie_date)
86
max_age = cookie_property('max-age', serialize_max_age)
87
httponly = cookie_property('httponly', bool)
88
secure = cookie_property('secure', bool)
239
path = cookie_property(b'path')
240
domain = cookie_property(b'domain')
241
comment = cookie_property(b'comment')
242
expires = cookie_property(b'expires', serialize_cookie_date)
243
max_age = cookie_property(b'max-age', serialize_max_age)
244
httponly = cookie_property(b'httponly', bool)
245
secure = cookie_property(b'secure', bool)
90
247
def __setitem__(self, k, v):
248
k = bytes_(k.lower(), 'ascii')
93
250
dict.__setitem__(self, k, v)
95
252
def serialize(self, full=True):
97
254
add = result.append
98
add("%s=%s" % (self.name, _quote(self.value)))
255
add(self.name + b'=' + _quote(self.value))
100
257
for k in _c_valkeys:
103
assert isinstance(v, str), v
104
add("%s=%s" % (_c_renames[k], _quote(v)))
105
expires = self['expires']
260
add(_c_renames[k]+b'='+_quote(v))
261
expires = self[b'expires']
107
add("expires=%s" % expires)
263
add(b'expires=' + expires)
110
266
if self.httponly:
112
return '; '.join(result)
268
return native_(b'; '.join(result), 'ascii')
114
270
__str__ = serialize
116
272
def __repr__(self):
117
return '<%s: %s=%s>' % (self.__class__.__name__,
118
self.name, repr(self.value))
120
def _valid_cookie_name(key):
122
key = key.encode('ascii')
125
return not needs_quoting(key)
273
return '<%s: %s=%r>' % (self.__class__.__name__,
129
"comment" : "Comment",
131
"max-age" : "Max-Age",
280
b"comment" : b"Comment",
281
b"domain" : b"Domain",
282
b"max-age" : b"Max-Age",
133
284
_c_valkeys = sorted(_c_renames)
134
285
_c_keys = set(_c_renames)
135
_c_keys.update(['expires', 'secure', 'httponly'])
286
_c_keys.update([b'expires', b'secure', b'httponly'])
144
296
_re_quoted = r'"(?:\\"|.)*?"' # any doublequoted string
145
297
_legal_special_chars = "~!@#$%^&*()_+=-`.?|:/(){}<>'"
146
298
_re_legal_char = r"[\w\d%s]" % re.escape(_legal_special_chars)
147
299
_re_expires_val = r"\w{3},\s[\w\d-]{9,11}\s[\d:]{8}\sGMT"
148
_rx_cookie = re.compile(
150
(r"(%s+?)" % _re_legal_char)
154
+ r"(%s|%s|%s*)" % (_re_quoted, _re_expires_val, _re_legal_char)
300
_re_cookie_str_key = r"(%s+?)" % _re_legal_char
301
_re_cookie_str_equal = r"\s*=\s*"
302
_re_unquoted_val = r"(?:%s|\\(?:[0-3][0-7][0-7]|.))*" % _re_legal_char
303
_re_cookie_str_val = r"(%s|%s|%s)" % (_re_quoted, _re_expires_val,
305
_re_cookie_str = _re_cookie_str_key + _re_cookie_str_equal + _re_cookie_str_val
307
_rx_cookie = re.compile(bytes_(_re_cookie_str, 'ascii'))
308
_rx_unquote = re.compile(bytes_(r'\\([0-3][0-7][0-7]|.)', 'ascii'))
310
_bchr = (lambda i: bytes([i])) if PY3 else chr
311
_ch_unquote_map = dict((bytes_('%03o' % i), _bchr(i))
314
_ch_unquote_map.update((v, v) for v in list(_ch_unquote_map.values()))
157
_rx_unquote = re.compile(r'\\([0-3][0-7][0-7]|.)')
316
_b_dollar_sign = 36 if PY3 else '$'
317
_b_quote_mark = 34 if PY3 else '"'
160
if v and v[0] == v[-1] == '"':
320
#assert isinstance(v, bytes)
321
if v and v[0] == v[-1] == _b_quote_mark:
165
return chr(int(v, 8))
167
v = _rx_unquote.sub(_ch_unquote, v)
323
return _rx_unquote.sub(_ch_unquote, v)
326
return _ch_unquote_map[m.group(1)]
178
333
# these chars can be in cookie value w/o causing it to be quoted
179
334
_no_escape_special_chars = "!#$%&'*+-.^_`|~/"
180
_no_escape_chars = string.ascii_letters + string.digits + \
181
_no_escape_special_chars
335
_no_escape_chars = (string.ascii_letters + string.digits +
336
_no_escape_special_chars)
337
_no_escape_bytes = bytes_(_no_escape_chars)
182
338
# these chars never need to be quoted
183
_escape_noop_chars = _no_escape_chars+': '
339
_escape_noop_chars = _no_escape_chars + ': '
184
340
# this is a map used to escape the values
185
_escape_map = dict((chr(i), '\\%03o' % i) for i in xrange(256))
186
_escape_map.update(zip(_escape_noop_chars, _escape_noop_chars.decode('ascii')))
187
_escape_map['"'] = u'\\"'
188
_escape_map['\\'] = u'\\\\'
341
_escape_map = dict((chr(i), '\\%03o' % i) for i in range(256))
342
_escape_map.update(zip(_escape_noop_chars, _escape_noop_chars))
343
_escape_map['"'] = r'\"'
344
_escape_map['\\'] = r'\\'
345
if PY3: # pragma: no cover
346
# convert to {int -> bytes}
347
_escape_map = dict((ord(k), bytes_(v, 'ascii')) for k, v in _escape_map.items())
189
348
_escape_char = _escape_map.__getitem__
192
350
weekdays = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun')
193
351
months = (None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep',
194
352
'Oct', 'Nov', 'Dec')
354
_notrans_binary = b' '*256
197
def needs_quoting(v):
198
return v.translate(_notrans, _no_escape_chars)
356
def _needs_quoting(v):
357
return v.translate(_notrans_binary, _no_escape_bytes)
201
#assert isinstance(v, str)
203
return '"' + ''.join(map(_escape_char, v)) + '"'
360
#assert isinstance(v, bytes)
361
if _needs_quoting(v):
362
return b'"' + b''.join(map(_escape_char, v)) + b'"'
365
def _valid_cookie_name(key):
366
return isinstance(key, bytes) and not (
368
or key[0] == _b_dollar_sign
369
or key.lower() in _c_keys