~ubuntu-branches/ubuntu/trusty/python-webob/trusty-proposed

« back to all changes in this revision

Viewing changes to webob/cookies.py

  • Committer: Package Import Robot
  • Author(s): Chuck Short
  • Date: 2013-01-07 07:52:32 UTC
  • mfrom: (1.3.5)
  • Revision ID: package-import@ubuntu.com-20130107075232-w6x8r94du3t48wj4
Tags: 1.2.3-0ubuntu1
* New upstream release:
  - Dropped debian/patches/01_lp_920197.patch: no longer needed.
  - debian/watch: Update to point to pypi.
  - debian/rules: Disable docs build.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
import re, time, string
2
 
from datetime import datetime, date, timedelta
 
1
import collections
 
2
 
 
3
from datetime import (
 
4
    date,
 
5
    datetime,
 
6
    timedelta,
 
7
    )
 
8
import re
 
9
import string
 
10
import time
 
11
 
 
12
from webob.compat import (
 
13
    PY3,
 
14
    text_type,
 
15
    bytes_,
 
16
    text_,
 
17
    native_,
 
18
    string_types,
 
19
    )
3
20
 
4
21
__all__ = ['Cookie']
5
22
 
 
23
_marker = object()
 
24
 
 
25
class RequestCookies(collections.MutableMapping):
 
26
 
 
27
    _cache_key = 'webob._parsed_cookies'
 
28
 
 
29
    def __init__(self, environ):
 
30
        self._environ = environ
 
31
 
 
32
    @property
 
33
    def _cache(self):
 
34
        env = self._environ
 
35
        header = env.get('HTTP_COOKIE', '')
 
36
        cache, cache_header = env.get(self._cache_key, ({}, None))
 
37
        if cache_header == header:
 
38
            return cache
 
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)
 
42
        return cache
 
43
 
 
44
    def _mutate_header(self, name, value):
 
45
        header = self._environ.get('HTTP_COOKIE')
 
46
        had_header = header is not None
 
47
        header = header or ''
 
48
        if PY3: # pragma: no cover
 
49
                header = header.encode('latin-1')
 
50
        bytes_name = bytes_(name, 'ascii')
 
51
        if value is None:
 
52
            replacement = None
 
53
        else:
 
54
            bytes_val = _quote(bytes_(value, 'utf-8'))
 
55
            replacement = bytes_name + b'=' + bytes_val
 
56
        matches = _rx_cookie.finditer(header)
 
57
        found = False
 
58
        for match in matches:
 
59
            start, end = match.span()
 
60
            match_name = match.group(1)
 
61
            if match_name == bytes_name:
 
62
                found = True
 
63
                if replacement is None: # remove value
 
64
                    header = header[:start].rstrip(b' ;') + header[end:]
 
65
                else: # replace value
 
66
                    header = header[:start] + replacement + header[end:]
 
67
                break
 
68
        else:
 
69
            if replacement is not None:
 
70
                if header:
 
71
                    header += b'; ' + replacement
 
72
                else:
 
73
                    header = replacement
 
74
 
 
75
        if header:
 
76
            self._environ['HTTP_COOKIE'] = native_(header, 'latin-1')
 
77
        elif had_header:
 
78
            self._environ['HTTP_COOKIE'] = ''
 
79
 
 
80
        return found
 
81
 
 
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')
 
87
        try:
 
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')
 
93
        return name
 
94
            
 
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):
 
100
            try:
 
101
                value = text_(value, 'utf-8')
 
102
            except UnicodeDecodeError:
 
103
                raise ValueError(
 
104
                    value, 'cookie value must be utf-8 binary or unicode')
 
105
        self._mutate_header(name, value)
 
106
 
 
107
    def __getitem__(self, name):
 
108
        return self._cache[name]
 
109
 
 
110
    def get(self, name, default=None):
 
111
        return self._cache.get(name, default)
 
112
 
 
113
    def __delitem__(self, name):
 
114
        name = self._valid_cookie_name(name)
 
115
        found = self._mutate_header(name, None)
 
116
        if not found:
 
117
            raise KeyError(name)
 
118
 
 
119
    def keys(self):
 
120
        return self._cache.keys()
 
121
 
 
122
    def values(self):
 
123
        return self._cache.values()
 
124
 
 
125
    def items(self):
 
126
        return self._cache.items()
 
127
 
 
128
    if not PY3:
 
129
        def iterkeys(self):
 
130
            return self._cache.iterkeys()
 
131
 
 
132
        def itervalues(self):
 
133
            return self._cache.itervalues()
 
134
 
 
135
        def iteritems(self):
 
136
            return self._cache.iteritems()
 
137
 
 
138
    def __contains__(self, name):
 
139
        return name in self._cache
 
140
 
 
141
    def __iter__(self):
 
142
        return self._cache.__iter__()
 
143
 
 
144
    def __len__(self):
 
145
        return len(self._cache)
 
146
 
 
147
    def clear(self):
 
148
        self._environ['HTTP_COOKIE'] = ''
 
149
 
 
150
    def __repr__(self):
 
151
        return '<RequestCookies (dict-like) with values %r>' % (self._cache,)
 
152
    
6
153
class Cookie(dict):
7
154
    def __init__(self, input=None):
8
155
        if input:
9
156
            self.load(input)
10
157
 
11
158
    def load(self, data):
12
 
        ckey = None
13
 
        for key, val in _rx_cookie.findall(data):
 
159
        morsel = {}
 
160
        for key, val in _parse_cookie(data):
14
161
            if key.lower() in _c_keys:
15
 
                if ckey:
16
 
                    self[ckey][key] = _unquote(val)
17
 
            elif key[0] == '$':
18
 
                # RFC2109: NAMEs that begin with $ are reserved for other uses
19
 
                # and must not be used by applications.
20
 
                continue
 
162
                morsel[key] = val
21
163
            else:
22
 
                self[key] = _unquote(val)
23
 
                ckey = key
 
164
                morsel = self.add(key, val)
24
165
 
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):
 
170
            return {}
 
171
        r = Morsel(key, val)
 
172
        dict.__setitem__(self, key, r)
 
173
        return r
 
174
    __setitem__ = add
28
175
 
29
176
    def serialize(self, full=True):
30
177
        return '; '.join(m.serialize(full) for m in self.values())
31
178
 
32
179
    def values(self):
33
 
        return [m for _,m in sorted(self.items())]
 
180
        return [m for _, m in sorted(self.items())]
34
181
 
35
182
    __str__ = serialize
36
183
 
39
186
                               ', '.join(map(repr, self.values())))
40
187
 
41
188
 
 
189
def _parse_cookie(data):
 
190
    if PY3: # pragma: no cover
 
191
        data = data.encode('latin-1')
 
192
    for key, val in _rx_cookie.findall(data):
 
193
        yield key, _unquote(val)
 
194
 
 
195
def parse_cookie(data):
 
196
    """
 
197
    Parse cookies ignoring anything except names and values
 
198
    """
 
199
    return ((k,v) for k,v in _parse_cookie(data) if _valid_cookie_name(k))
 
200
 
 
201
 
42
202
def cookie_property(key, serialize=lambda v: v):
43
203
    def fset(self, v):
44
204
        self[key] = serialize(v)
46
206
 
47
207
def serialize_max_age(v):
48
208
    if isinstance(v, timedelta):
49
 
        return str(v.seconds + v.days*24*60*60)
 
209
        v = str(v.seconds + v.days*24*60*60)
50
210
    elif isinstance(v, int):
51
 
        return str(v)
52
 
    else:
53
 
        return v
 
211
        v = str(v)
 
212
    return bytes_(v)
54
213
 
55
214
def serialize_cookie_date(v):
56
215
    if v is None:
57
216
        return None
58
 
    elif isinstance(v, str):
 
217
    elif isinstance(v, bytes):
59
218
        return v
 
219
    elif isinstance(v, text_type):
 
220
        return v.encode('ascii')
60
221
    elif isinstance(v, int):
61
222
        v = timedelta(seconds=v)
62
223
    if isinstance(v, timedelta):
64
225
    if isinstance(v, (datetime, date)):
65
226
        v = v.timetuple()
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')
68
229
 
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)
75
235
        self.name = name
76
 
        # we can encode the unicode value as UTF-8 here,
77
 
        # but then the decoded cookie would still be str,
78
 
        # so we don't do that
79
236
        self.value = value
80
237
        self.update(dict.fromkeys(_c_keys, None))
81
238
 
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)
89
246
 
90
247
    def __setitem__(self, k, v):
91
 
        k = k.lower()
 
248
        k = bytes_(k.lower(), 'ascii')
92
249
        if k in _c_keys:
93
250
            dict.__setitem__(self, k, v)
94
251
 
95
252
    def serialize(self, full=True):
96
253
        result = []
97
254
        add = result.append
98
 
        add("%s=%s" % (self.name, _quote(self.value)))
 
255
        add(self.name + b'=' + _quote(self.value))
99
256
        if full:
100
257
            for k in _c_valkeys:
101
258
                v = self[k]
102
259
                if v:
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']
106
262
            if expires:
107
 
                add("expires=%s" % expires)
 
263
                add(b'expires=' + expires)
108
264
            if self.secure:
109
 
                add('secure')
 
265
                add(b'secure')
110
266
            if self.httponly:
111
 
                add('HttpOnly')
112
 
        return '; '.join(result)
 
267
                add(b'HttpOnly')
 
268
        return native_(b'; '.join(result), 'ascii')
113
269
 
114
270
    __str__ = serialize
115
271
 
116
272
    def __repr__(self):
117
 
        return '<%s: %s=%s>' % (self.__class__.__name__,
118
 
                                self.name, repr(self.value))
119
 
 
120
 
def _valid_cookie_name(key):
121
 
    try:
122
 
        key = key.encode('ascii')
123
 
    except UnicodeError:
124
 
        return False
125
 
    return not needs_quoting(key)
 
273
        return '<%s: %s=%r>' % (self.__class__.__name__,
 
274
            native_(self.name),
 
275
            native_(self.value)
 
276
        )
126
277
 
127
278
_c_renames = {
128
 
    "path" : "Path",
129
 
    "comment" : "Comment",
130
 
    "domain" : "Domain",
131
 
    "max-age" : "Max-Age",
 
279
    b"path" : b"Path",
 
280
    b"comment" : b"Comment",
 
281
    b"domain" : b"Domain",
 
282
    b"max-age" : b"Max-Age",
132
283
}
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'])
136
287
 
137
288
 
138
289
 
141
292
# parsing
142
293
#
143
294
 
 
295
 
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(
149
 
    # key
150
 
    (r"(%s+?)" % _re_legal_char)
151
 
    # =
152
 
    + r"\s*=\s*"
153
 
    # val
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,
 
304
                                       _re_unquoted_val)
 
305
_re_cookie_str = _re_cookie_str_key + _re_cookie_str_equal + _re_cookie_str_val
 
306
 
 
307
_rx_cookie = re.compile(bytes_(_re_cookie_str, 'ascii'))
 
308
_rx_unquote = re.compile(bytes_(r'\\([0-3][0-7][0-7]|.)', 'ascii'))
 
309
 
 
310
_bchr = (lambda i: bytes([i])) if PY3 else chr
 
311
_ch_unquote_map = dict((bytes_('%03o' % i), _bchr(i))
 
312
    for i in range(256)
155
313
)
 
314
_ch_unquote_map.update((v, v) for v in list(_ch_unquote_map.values()))
156
315
 
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 '"'
158
318
 
159
319
def _unquote(v):
160
 
    if v and v[0] == v[-1] == '"':
 
320
    #assert isinstance(v, bytes)
 
321
    if v and v[0] == v[-1] == _b_quote_mark:
161
322
        v = v[1:-1]
162
 
        def _ch_unquote(m):
163
 
            v = m.group(1)
164
 
            if v.isdigit():
165
 
                return chr(int(v, 8))
166
 
            return v
167
 
        v = _rx_unquote.sub(_ch_unquote, v)
168
 
    return v
 
323
    return _rx_unquote.sub(_ch_unquote, v)
169
324
 
 
325
def _ch_unquote(m):
 
326
    return _ch_unquote_map[m.group(1)]
170
327
 
171
328
 
172
329
#
173
330
# serializing
174
331
#
175
332
 
176
 
_notrans = ' '*256
177
 
 
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__
190
349
 
191
 
 
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')
195
353
 
 
354
_notrans_binary = b' '*256
196
355
 
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)
199
358
 
200
359
def _quote(v):
201
 
    #assert isinstance(v, str)
202
 
    if needs_quoting(v):
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'"'
204
363
    return v
 
364
 
 
365
def _valid_cookie_name(key):
 
366
    return isinstance(key, bytes) and not (
 
367
        _needs_quoting(key)
 
368
        or key[0] == _b_dollar_sign
 
369
        or key.lower() in _c_keys
 
370
    )