""" Basic cookie manager
    forwards cookies to all other instances connected to the event manager"""

from collections import defaultdict
import os, re, stat

# these are symbolic names for the components of the cookie tuple
symbolic = {'domain': 0, 'path':1, 'name':2, 'value':3, 'scheme':4, 'expires':5}

# allows for partial cookies
# ? allow wildcard in key
def match(key, cookie):
    for k,c in zip(key,cookie):
        if k != c:
            return False
    return True

class NullStore(object):
    def add_cookie(self, rawcookie, cookie):
        pass

    def delete_cookie(self, rkey, key):
        pass

class ListStore(list):
    def add_cookie(self, rawcookie, cookie):
        self.append(rawcookie)

    def delete_cookie(self, rkey, key):
        self[:] = [x for x in self if not match(key, splitquoted(x))]

class TextStore(object):
    def __init__(self, filename):
        self.filename = filename
        try:
          # make sure existing cookie jar is not world-open
          perm_mode = os.stat(self.filename).st_mode
          if (perm_mode & (stat.S_IRWXO | stat.S_IRWXG)) > 0:
              safe_perm = stat.S_IMODE(perm_mode) & ~(stat.S_IRWXO | stat.S_IRWXG)
              os.chmod(self.filename, safe_perm)
        except OSError:
            pass

    def as_event(self, cookie):
        """Convert cookie.txt row to uzbls cookie event format"""
        scheme = {
            'TRUE'  : 'https',
            'FALSE' : 'http'
        }
        extra = ''
        if cookie[0].startswith("#HttpOnly_"):
            extra = 'Only'
            domain = cookie[0][len("#HttpOnly_"):]
        elif cookie[0].startswith('#'):
            return None
        else:
            domain = cookie[0]
        try:
            return (domain,
                cookie[2],
                cookie[5],
                cookie[6],
                scheme[cookie[3]] + extra,
                cookie[4])
        except (KeyError,IndexError):
            # Let malformed rows pass through like comments
            return None

    def as_file(self, cookie):
        """Convert cookie event to cookie.txt row"""
        secure = {
            'https' : 'TRUE',
            'http'  : 'FALSE',
            'httpsOnly' : 'TRUE',
            'httpOnly'  : 'FALSE'
        }
        http_only = {
            'https' : '',
            'http'  : '',
            'httpsOnly' : '#HttpOnly_',
            'httpOnly'  : '#HttpOnly_'
        }
        return (http_only[cookie[4]] + cookie[0],
            'TRUE' if cookie[0].startswith('.') else 'FALSE',
            cookie[1],
            secure[cookie[4]],
            cookie[5],
            cookie[2],
            cookie[3])

    def add_cookie(self, rawcookie, cookie):
        assert len(cookie) == 6

        # delete equal cookies (ignoring expire time, value and secure flag)
        self.delete_cookie(None, cookie[:-3])

        # restrict umask before creating the cookie jar
        curmask=os.umask(0)
        os.umask(curmask| stat.S_IRWXO | stat.S_IRWXG)

        first = not os.path.exists(self.filename)
        with open(self.filename, 'a') as f:
            if first:
                print >> f, "# HTTP Cookie File"
            print >> f, '\t'.join(self.as_file(cookie))
        os.umask(curmask)

    def delete_cookie(self, rkey, key):
        if not os.path.exists(self.filename):
            return

        # restrict umask before creating the cookie jar
        curmask=os.umask(0)
        os.umask(curmask | stat.S_IRWXO | stat.S_IRWXG)

        # read all cookies
        with open(self.filename, 'r') as f:
            cookies = f.readlines()

        # write those that don't match the cookie to delete
        with open(self.filename, 'w') as f:
            for l in cookies:
                c = self.as_event(l.split('\t'))
                if c is None or not match(key, c):
                    print >> f, l,
        os.umask(curmask)

xdg_data_home = os.environ.get('XDG_DATA_HOME', os.path.join(os.environ['HOME'], '.local/share'))
DefaultStore = TextStore(os.path.join(xdg_data_home, 'uzbl/cookies.txt'))
SessionStore = TextStore(os.path.join(xdg_data_home, 'uzbl/session-cookies.txt'))

def match_list(_list, cookie):
    for matcher in _list:
        for component, match in matcher:
            if match(cookie[component]) is None:
                break
        else:
            return True
    return False

# accept a cookie only when:
# a. there is no whitelist and the cookie is in the blacklist
# b. the cookie is in the whitelist and not in the blacklist
def accept_cookie(uzbl, cookie):
    if uzbl.cookie_whitelist:
        if match_list(uzbl.cookie_whitelist, cookie):
            return not match_list(uzbl.cookie_blacklist, cookie)
        return False

    return not match_list(uzbl.cookie_blacklist, cookie)

def expires_with_session(uzbl, cookie):
    return cookie[5] == ''

def get_recipents(uzbl):
    """ get a list of Uzbl instances to send the cookie too. """
    # This could be a lot more interesting
    return [u for u in uzbl.parent.uzbls.values() if u is not uzbl]

def get_store(uzbl, session=False):
    if session:
        return SessionStore
    return DefaultStore

def add_cookie(uzbl, cookie):
    splitted = splitquoted(cookie)
    if accept_cookie(uzbl, splitted):
        for u in get_recipents(uzbl):
            u.send('add_cookie %s' % cookie)

        get_store(uzbl, expires_with_session(uzbl, splitted)).add_cookie(cookie, splitted)
    else:
        logger.debug('cookie %r is blacklisted' % splitted)
        uzbl.send('delete_cookie %s' % cookie)

def delete_cookie(uzbl, cookie):
    for u in get_recipents(uzbl):
        u.send('delete_cookie %s' % cookie)

    splitted = splitquoted(cookie)
    if len(splitted) == 6:
        get_store(uzbl, expires_with_session(uzbl, splitted)).delete_cookie(cookie, splitted)
    else:
        for store in set([get_store(uzbl, session) for session in (True, False)]):
            store.delete_cookie(cookie, splitted)

# add a cookie matcher to a whitelist or a blacklist.
# a matcher is a list of (component, re) tuples that matches a cookie when the
# "component" part of the cookie matches the regular expression "re".
# "component" is one of the keys defined in the variable "symbolic" above,
# or the index of a component of a cookie tuple.
def add_cookie_matcher(_list, arg):
    args = splitquoted(arg)
    mlist = []
    for (component, regexp) in zip(args[0::2], args[1::2]):
        try:
            component = symbolic[component]
        except KeyError:
            component = int(component)
        assert component <= 5
        mlist.append((component, re.compile(regexp).search))
    _list.append(mlist)

def blacklist(uzbl, arg):
    add_cookie_matcher(uzbl.cookie_blacklist, arg)

def whitelist(uzbl, arg):
    add_cookie_matcher(uzbl.cookie_whitelist, arg)

def init(uzbl):
    connect_dict(uzbl, {
        'ADD_COOKIE':       add_cookie,
        'DELETE_COOKIE':    delete_cookie,
        'BLACKLIST_COOKIE': blacklist,
        'WHITELIST_COOKIE': whitelist
    })
    export_dict(uzbl, {
        'cookie_blacklist' : [],
        'cookie_whitelist' : []
    })

# vi: set et ts=4:
