"""$URL: svn://svn/repos/trunk/sitereaper/lib/cookies.py $
$Id: cookies.py 20955 2003-03-05 17:05:52Z dbinger $

An attempt to correct for the brain-damaged terminology ("morsel"?!?)
and excessively complex code in Cookie.py.
"""

import Cookie
from time import time, gmtime, strftime, strptime, mktime, timezone
from sitereaper.utils import std_repr
from sitereaper.verbose import Verbose


_cookie_re = Cookie._CookiePattern

_cookie_attrs = ("comment",
                 "domain",
                 "max-age",
                 "path",
                 "secure",
                 "version",
                 "expires",           # for old (pre-RFC 2109) cookies
                )

EXPIRES_FMT = "%a, %d-%b-%Y %H:%M:%S GMT"

class Cookie:
    """
    A single cookie, i.e. a name/value pair with several associated
    attributes (domain, path, secure, etc.).  For parsing the cookie-
    related HTTP headers ("Cookie" and "Set-Cookie"), use the
    CookieSet class; to generate those cookies, you can use either this
    class or CookieSet.

    Instance attributes:
      name : string
        the name of the cookie
      value : string
        the value of the cookie, untouched (eg. if the original value
        was quoted, this includes the quotes)
      comment : string
      domain : string
      path : string
      max_age : string
        maximum age of the cookie, in seconds from the time of
        receipt by the client
      secure : boolean
      version : string
      expires : int
        expiration time of the cookie, in seconds since the epoch
        (1970-01-01 00:00 UTC).

      creation_time : int
        the time this Cookie object was created, which is presumably
        equivalent to the time the cookie was received by the
        application.  Only relevant for client-side applications.

    You can set or get the cookie attributes specified by RFC 2109
    either as instance attributes or dictionary items:

      cookie["Domain"] = ".example.com"
      print cookie.domain

      cookie["Max-age"] = 600
      print cookie.max_age

    Pseudo-dictionary keys are case insensitive.

    The "Expires" attribute is subject to special handling if set like a
    dictionary item.  Eg. this assignment:

      cookie["expires"] = "Sun, 17-Jan-2038 19:14:07 GMT"

    results in the integer value 2147386447 (seconds since epoch for
    that date) being assigned to the 'expires' attribute.  A similar
    translation occurs on dictionary access to the "expires" attribute.
    """

    def __init__ (self, name, value):
        self.name = name
        self.value = value
        self.comment = None
        self.domain = None
        self.path = None
        self.max_age = None
        self.secure = None
        self.version = None
        self.expires = None

        self.creation_time = time()

    def __str__ (self):
        return "%s = %s" % (self.name or "*no name*",
                            self.value or "*no value*")

    __repr__ = std_repr

    def __eq__ (self, other):
        return vars(self) == vars(other)

    def __ne__ (self, other):
        return vars(self) != vars(other)

    # This is just for the convenience of CookieSet.parse_header().
    def __setitem__ (self, key, val):
        key = key.lower()
        if key not in _cookie_attrs:
            raise IndexError, key
        key = key.replace("-", "_")
        if key == "expires":
            try:
                val = mktime(strptime(val, EXPIRES_FMT)) - timezone
            except ValueError:
                val = None
        elif key == "max_age":
            try:
                val = int(val)
            except ValueError:
                val = None
        setattr(self, key, val)

    def __getitem__ (self, key):
        key = key.lower()
        if key not in _cookie_attrs:
            raise IndexError, key
        key = key.replace("-", "_")
        if key == "expires" and self.expires is not None:
            return strftime(EXPIRES_FMT, gmtime(self.expires))
        else:
            return getattr(self, key)

    def get_age (self):
        """Return the age of the cookie in seconds."""
        return time() - self.creation_time

    def is_expired (self):
        """Return true if the cookie has expired."""
        # Prefer the RFC 2109 'max-age' attribute.
        if self.max_age is not None:
            return self.get_age() > self.max_age

        # Fallback to pre-RFC 2109 (Netscape) "expires"
        if self.expires is not None:
            return time() > self.expires

    def path_matches (self, path):
        if (self.path is None or self.path == "/" or self.path == path):
            return 1
        else:
            return (path.startswith(self.path) and
                    path[len(self.path)] == "/")

    def make_response_header (self):
        r = ["%s=%s" % (self.name, self.value)]
        for attr in _cookie_attrs:
            val = self[attr]
            if val:
                r.append("%s=%s" % (attr.capitalize(), val))

        return ";".join(r)

    def make_request_header (self):
        assert self.name and self.value
        if self.version is None:        # pre-RFC 2109
            return "%s=%s" % (self.name, self.value)
        else:
            return "$Version=%s; %s=%s" % (self.version, self.name, self.value)


class CookieSet:

    def __init__ (self, cookies=None):
        if cookies:
            self.cookies = cookies
        else:
            self.cookies = []

    def __str__ (self):
        if self.cookies:
            return ", ".join([cookie.name for cookie in self.cookies])
        else:
            return "none"

    __repr__ = std_repr

    def __len__ (self):
        return len(self.cookies)

    def __getitem__ (self, i):
        return self.cookies[i]

    def __nonzero__ (self):
        return len(self.cookies) > 0

    def parse_header (self, str, server, request_uri=None):
        """
        Parse an entire RFC 2109 "Set-cookie" header, resulting in
        one or more Cookie objects (stored in self.cookies).
        """
        i = 0                           # current char of str
        cookie = None                   # current cookie -- doesn't exist
                                        # until we see the cookie's name
        path = None
        if request_uri:
            lastslash = request_uri.rindex('/')
            if lastslash != -1:
                path = request_uri[0:lastslash]

        # XXX doesn't do anything about unquoting quoted cookie values.
        # NB. neither does the standard lib's Cookie.py, and it might
        # not matter if all we're doing is storing the value briefly
        # before sending it back to the server.

        while i < len(str):
            match = _cookie_re.search(str, i)
            if not match:
                break

            key = match.group("key")
            val = match.group("val")
            i = match.end(0)

            if key[0] == "$":
                pass                    # XXX what to do about these?
            elif key.lower() in _cookie_attrs:
                if cookie:
                    cookie[key.lower()] = val
                else:
                    raise ValueError(
                        "bad cookie header: attr %s before name: %r"
                        % (key, str))
            else:
                cookie = Cookie(key, val)
                cookie.domain = server  # default -- can be overridden
                cookie.path = path
                self.cookies.append(cookie)

    def make_response_header (self):
        hdrs = [cookie.make_response_header() for cookie in self.cookies]
        return ", ".join(hdrs)

    def make_request_header (self):
        hdrs = [cookie.make_request_header() for cookie in self.cookies]
        return "; ".join(hdrs)


class CookieJar (Verbose):
    """
    Client-side collection of HTTP cookies to pass to various servers
    under various conditions.
    """

    def __init__ (self):
        # map domain (server) names to a list of cookies: eg.
        #  { "cthulhu.gerg.ca" :
        #    [Cookie("userid", "gward", ...),
        #     Cookie("session", "127.0.0.1:20020130184242:....")],
        #    "google.com" :
        #    [Cookie("PREF", "...")],
        #  }
        self.cookies = {}

    __repr__ = std_repr

    # XXX misnamed, because it can remove a cookie too!
    def add_cookie (self, cookie):
        """Feed the cookie jar by adding a new cookie to it."""
        cookies = self.cookies.get(cookie.domain)
        if cookies:
            for i in range(len(cookies)-1, -1, -1):
                if (cookies[i].name == cookie.name and
                    cookies[i].path == cookie.path):
                    if cookie.max_age == 0:
                        del cookies[i]
                    else:
                        cookies[i] = cookie
        else:
            if cookie.max_age != 0:
                self.cookies[cookie.domain] = [cookie]

    def add_cookies (self, cookie_set):
        for cookie in cookie_set:
            self.add_cookie(cookie)

    def get_cookies (self, server, path):
        """get_cookies(server : string, path : string) -> CookieSet

        Determine which cookies should be sent to 'path' on 'server'
        and return a CookieSet containing them.
        """

        # First pass: get cookies that match on server/domain.
        cookies = self.cookies.get(server)
        if cookies:
            cookies = cookies[:]        # make a copy to extend
        else:
            cookies = []

        # Umm, this might be too general... "x.y.foo.com" is not
        # supposed to match ".foo.com".
        name_bits = server.split(".")[1:]
        while len(name_bits) > 1:
            n = "." + ".".join(name_bits)
            c = self.cookies.get(n)
            if c:
                cookies.extend(c)
            del name_bits[0]

        # Now go through that list, throwing away cookies that
        # don't match by path or time.
        now = time()
        for i in range(len(cookies)-1, -1, -1):
            cookie = cookies[i]
            if cookie.is_expired():
                del cookies[i]
                self.cookies[cookie.domain].remove(cookie)
            elif not cookie.path_matches(path):
                del cookies[i]

        return CookieSet(cookies)


if __name__ == "__main__":
    server = "cthulhu.gerg.ca"

    cset = CookieSet()
    cset.parse_header('userid="gward";Domain=.gerg.ca', server)
    print cset[0]
    print cset[0].make_response_header()
    print cset[0].make_request_header()
    print

    jar = CookieJar()
    jar.add_cookie(cset[0])
    cset2 = jar.get_cookies("cthulhu.gerg.ca", "/")
    print cset2[0]
    assert len(cset2) == 1 and cset2[0] == cset[0]

    cset3 = jar.get_cookies("blah.gerg.ca", "/foo")
    assert len(cset3) == 1 and cset3[0] == cset[0]

    cset4 = jar.get_cookies("foogerg.ca", "/")
    assert len(cset4) == 0

    cset = CookieSet()
    cset.parse_header('userid="gward";doMaiN=.gerg.ca; '
                      'session=ab93cnd;Path=/foo; '
                      'foo=bar; Domain=.gerg.ca; Path=/foo',
                      server)
    assert len(cset) == 3, `cset`
    print cset[0]
    print cset[0].make_response_header()
    print cset[0].make_request_header()
    print
    print cset[1]
    print cset[1].make_response_header()
    print cset[1].make_request_header()
    print
    print cset[2]
    print cset[2].make_response_header()
    print cset[2].make_request_header()
    print
    print cset.make_response_header()
    print cset.make_request_header()

    cjar = CookieJar()
    cjar.add_cookies(cset)
    print `cjar.get_cookies("cthulhu.gerg.ca", "/")` # _userid only
    print `cjar.get_cookies("cthulhu.gerg.ca", "/foo")` # both
    print `cjar.get_cookies("cthulhu.gerg.ca", "/foobar")` # userid only
    print `cjar.get_cookies("www.gerg.ca", "/")` # userid only
    print `cjar.get_cookies("www.gerg.ca", "/foo/baz")` # session only
    print `cjar.get_cookies("www.gerg.ca", "/foob/ar")`

