| import re, time, string |
| from datetime import datetime, date, timedelta |
| |
| __all__ = ['Cookie'] |
| |
| class Cookie(dict): |
| def __init__(self, input=None): |
| if input: |
| self.load(input) |
| |
| def load(self, data): |
| ckey = None |
| for key, val in _rx_cookie.findall(data): |
| if key.lower() in _c_keys: |
| if ckey: |
| self[ckey][key] = _unquote(val) |
| elif key[0] == '$': |
| # RFC2109: NAMEs that begin with $ are reserved for other uses |
| # and must not be used by applications. |
| continue |
| else: |
| self[key] = _unquote(val) |
| ckey = key |
| |
| def __setitem__(self, key, val): |
| if _valid_cookie_name(key): |
| dict.__setitem__(self, key, Morsel(key, val)) |
| |
| def serialize(self, full=True): |
| return '; '.join(m.serialize(full) for m in self.values()) |
| |
| def values(self): |
| return [m for _,m in sorted(self.items())] |
| |
| __str__ = serialize |
| |
| def __repr__(self): |
| return '<%s: [%s]>' % (self.__class__.__name__, |
| ', '.join(map(repr, self.values()))) |
| |
| |
| def cookie_property(key, serialize=lambda v: v): |
| def fset(self, v): |
| self[key] = serialize(v) |
| return property(lambda self: self[key], fset) |
| |
| def serialize_max_age(v): |
| if isinstance(v, timedelta): |
| return str(v.seconds + v.days*24*60*60) |
| elif isinstance(v, int): |
| return str(v) |
| else: |
| return v |
| |
| def serialize_cookie_date(v): |
| if v is None: |
| return None |
| elif isinstance(v, str): |
| return v |
| elif isinstance(v, int): |
| v = timedelta(seconds=v) |
| if isinstance(v, timedelta): |
| v = datetime.utcnow() + v |
| if isinstance(v, (datetime, date)): |
| v = v.timetuple() |
| r = time.strftime('%%s, %d-%%s-%Y %H:%M:%S GMT', v) |
| return r % (weekdays[v[6]], months[v[1]]) |
| |
| class Morsel(dict): |
| __slots__ = ('name', 'value') |
| def __init__(self, name, value): |
| assert name.lower() not in _c_keys |
| assert _valid_cookie_name(name) |
| assert isinstance(value, str) |
| self.name = name |
| # we can encode the unicode value as UTF-8 here, |
| # but then the decoded cookie would still be str, |
| # so we don't do that |
| self.value = value |
| self.update(dict.fromkeys(_c_keys, None)) |
| |
| path = cookie_property('path') |
| domain = cookie_property('domain') |
| comment = cookie_property('comment') |
| expires = cookie_property('expires', serialize_cookie_date) |
| max_age = cookie_property('max-age', serialize_max_age) |
| httponly = cookie_property('httponly', bool) |
| secure = cookie_property('secure', bool) |
| |
| def __setitem__(self, k, v): |
| k = k.lower() |
| if k in _c_keys: |
| dict.__setitem__(self, k, v) |
| |
| def serialize(self, full=True): |
| result = [] |
| add = result.append |
| add("%s=%s" % (self.name, _quote(self.value))) |
| if full: |
| for k in _c_valkeys: |
| v = self[k] |
| if v: |
| assert isinstance(v, str), v |
| add("%s=%s" % (_c_renames[k], _quote(v))) |
| expires = self['expires'] |
| if expires: |
| add("expires=%s" % expires) |
| if self.secure: |
| add('secure') |
| if self.httponly: |
| add('HttpOnly') |
| return '; '.join(result) |
| |
| __str__ = serialize |
| |
| def __repr__(self): |
| return '<%s: %s=%s>' % (self.__class__.__name__, |
| self.name, repr(self.value)) |
| |
| def _valid_cookie_name(key): |
| try: |
| key = key.encode('ascii') |
| except UnicodeError: |
| return False |
| return not needs_quoting(key) |
| |
| _c_renames = { |
| "path" : "Path", |
| "comment" : "Comment", |
| "domain" : "Domain", |
| "max-age" : "Max-Age", |
| } |
| _c_valkeys = sorted(_c_renames) |
| _c_keys = set(_c_renames) |
| _c_keys.update(['expires', 'secure', 'httponly']) |
| |
| |
| |
| |
| # |
| # parsing |
| # |
| |
| _re_quoted = r'"(?:\\"|.)*?"' # any doublequoted string |
| _legal_special_chars = "~!@#$%^&*()_+=-`.?|:/(){}<>'" |
| _re_legal_char = r"[\w\d%s]" % re.escape(_legal_special_chars) |
| _re_expires_val = r"\w{3},\s[\w\d-]{9,11}\s[\d:]{8}\sGMT" |
| _rx_cookie = re.compile( |
| # key |
| (r"(%s+?)" % _re_legal_char) |
| # = |
| + r"\s*=\s*" |
| # val |
| + r"(%s|%s|%s*)" % (_re_quoted, _re_expires_val, _re_legal_char) |
| ) |
| |
| _rx_unquote = re.compile(r'\\([0-3][0-7][0-7]|.)') |
| |
| def _unquote(v): |
| if v and v[0] == v[-1] == '"': |
| v = v[1:-1] |
| def _ch_unquote(m): |
| v = m.group(1) |
| if v.isdigit(): |
| return chr(int(v, 8)) |
| return v |
| v = _rx_unquote.sub(_ch_unquote, v) |
| return v |
| |
| |
| |
| # |
| # serializing |
| # |
| |
| _notrans = ' '*256 |
| |
| # these chars can be in cookie value w/o causing it to be quoted |
| _no_escape_special_chars = "!#$%&'*+-.^_`|~/" |
| _no_escape_chars = string.ascii_letters + string.digits + \ |
| _no_escape_special_chars |
| # these chars never need to be quoted |
| _escape_noop_chars = _no_escape_chars+': ' |
| # this is a map used to escape the values |
| _escape_map = dict((chr(i), '\\%03o' % i) for i in xrange(256)) |
| _escape_map.update(zip(_escape_noop_chars, _escape_noop_chars.decode('ascii'))) |
| _escape_map['"'] = u'\\"' |
| _escape_map['\\'] = u'\\\\' |
| _escape_char = _escape_map.__getitem__ |
| |
| |
| weekdays = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun') |
| months = (None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', |
| 'Oct', 'Nov', 'Dec') |
| |
| |
| def needs_quoting(v): |
| return v.translate(_notrans, _no_escape_chars) |
| |
| def _quote(v): |
| #assert isinstance(v, str) |
| if needs_quoting(v): |
| return '"' + ''.join(map(_escape_char, v)) + '"' |
| return v |