#!/usr/bin/python

import os,sys,re,logging,urlparse,base64

import gobject, dbus, gnomevfs
import dbus,dbus.service,dbus.glib

# Custom module
import nssdecrypt

_logger = logging.getLogger("WebLoginDriver")

def do_idle(f, *args):
  f(*args)
  return False

def on_pidgin_gmail(signon):
  p = dbus.SessionBus().get_object('im.pidgin.purple.PurpleService', '/im/pidgin/purple/PurpleObject')
  target_username = signon['username'] + '@gmail.com'
  a = None
  for acctid in p.PurpleAccountsGetAllActive():
    username = p.PurpleAccountGetUsername(a)
    try:
    	(real_username, resource) = username.split('/', 1)
    except:
    	real_username = username   
    if real_username == target_username:
      a = accountid
      break
  if not a:
    _logger.debug("creating pidgin account for %s", target_username)
    a = p.PurpleAccountNew(target_username, 'prpl-jabber')
  p.PurpleAccountsAdd(a)     
  p.PurpleAccountSetPassword(a, base64.b64decode(signon['password']))
  p.PurpleAccountSetRememberPassword(a, dbus.UInt32(1))
  do_idle(p.PurpleAccountSetEnabled, a, "gtk-gaim", dbus.UInt32(1))

hints = {
'GMail': {'hostname': 'https://www.google.com', 'username_field': 'Email'},
'GAFYD-Mail': {'hostname': 'https://www.google.com', 'username_field': 'userName'},
}

internal_matchers = {
 'GMail': on_pidgin_gmail,
}

BUS_NAME_STR='org.gnome.WebLoginDriver'
BUS_IFACE_STR='org.gnome.WebLoginDriver'

def iterdir(path):
  for fname in os.listdir(path):
    yield os.path.join(path, fname)

class VfsMonitor(object):
  """Avoid some locking oddities in gnomevfs monitoring"""
  def __init__(self, path, montype, cb):
    self.__path = path
    self.__cb = cb
    self.__idle_id = 0
    self.__monid = gnomevfs.monitor_add(path, montype, self.__on_vfsmon)
  
  def __idle_emit(self):
    self.__idle_id = 0
    self.__cb()

  def __on_vfsmon(self, *args):
    if not self.__monid:
      return
    if self.__idle_id == 0:
      self.__idle_id = gobject.timeout_add(300, self.__idle_emit)

  def cancel(self):
    if self.__idle_id:
      gobject.source_remove(self.__idle_id)
      self.__idle_id = 0
    if self.__monid:
      gnomevfs.monitor_cancel(self.__monid)
      self.__monid = None
    
class WebLoginDriver(dbus.service.Object):
  def __init__(self, bus_name):
    dbus.service.Object.__init__(self, bus_name, '/weblogindriver')  
      
    self.__ffpath = os.path.expanduser('~/.mozilla/firefox')
    self.__profpath = None
    self.__profile_monid = None
    self.__signons = {} # domain->(user,pass,{details})
    self.__signons_monid = None
    self.__profile_monid = None
    self.__uncreated_monid = None
    try:
      profpath = self.__get_profile_path()
    except KeyError, e:
      profpath = None
      _logger.debug("couldn't find default profile, awaiting creation")
      self.__uncreated_monid = VfsMonitor('file://' + self.__ffpath, gnomevfs.MONITOR_DIRECTORY, self.__on_profile_created)
    if profpath:
      self.__monitor_signons(profpath)

  def __monitor_signons(self, profpath):
    if profpath:
      _logger.debug("starting monitoring of signons in %s", profpath)
      self.__profpath = profpath
    signons_path = os.path.join(self.__profpath, 'signons2.txt')
    if (self.__signons_monid is None) and os.path.isfile(signons_path):
      _logger.debug("monitoring signons file")
      _logger.debug("initializing NSS in %s", self.__profpath)
      nssdecrypt.init(self.__profpath)
      self.__signons_monid = VfsMonitor('file://' + signons_path, gnomevfs.MONITOR_FILE, self.__on_signons_changed)
      if self.__profile_monid is not None:
        self.__profile_monid.cancel()
        self.__profile_monid = None
      gobject.idle_add(self.__idle_read_signons)
    elif self.__profile_monid is None:
      self.__profile_monid = VfsMonitor(profpath, gnomevfs.MONITOR_DIRECTORY, self.__on_profdir_changed)

  def __on_profdir_changed(self, *args):
    if self.__signons_monid is not None:
      return
    _logger.debug("profile dir changed: %s", args)
    self.__monitor_signons(None)
  
  def __on_signons_changed(self, *args):
    _logger.debug("signons changed: %s", args)
    if self.__idle_read_signons_id > 0:
      _logger.debug("canceling queued signons read")
      gobject.source_remove(self.__idle_read_signons_id)
    _logger.debug("queued signons read for 3s")
    self.__idle_read_signons_id = gobject.timeout_add(3000, self.__idle_read_signons)

  def __idle_read_signons(self):
    _logger.debug("in idle signons read")
    self.__idle_read_signons_id = 0

    new_signons = self.__read_signons()
    for signons in new_signons.itervalues():
      for signon in signons:
        for hintname,hintitems in hints.iteritems():
          matched = True
          for k,v in hintitems.iteritems():
            if signon[k] != v:
              matched = False
              break
          if matched:
            signon['hint'] = hintname
            break
    if len(self.__signons) > 0:
      changed_signons = []
      for hostname,signons in new_signons.iteritems():
        if (hostname not in self.__signons) or (self.__signons[hostname] != signons):
          self.SignonChanged(signons)
          changed_signons.append(signons)
    else:
      changed_signons = new_signons.itervalues()
      # todo signal on delete?
    for signons in changed_signons:
      _logger.debug("evaluating %s", signons)
      for signon in signons:
        for hintname,handler in internal_matchers.iteritems():
          if signon.get('hint', '') == hintname:
            try:
              _logger.debug("invoking handler %s for signon %s", handler, signon)
              handler(signon)
            except:
              _logger.error("handler %s failed", handler, exc_info=True)
    self.__signons = new_signons
  
  def __read_signons(self):
    f = open(os.path.join(self.__profpath, 'signons2.txt'))
    parse_states = dict(zip(['header', 'reject', 'realm', 'userfield', 'uservalue', 'passfield', 'passvalue', 'actionurl'],
                            xrange(100)))
    parse_state = 0

    realm_re = re.compile(r'^(.+?) \((.*)\)$')

    new_signons = {}

    # Manually adapted from mozilla/toolkit/components/passwordmgr/src/storage-Legacy.js 20071017
    signon = {}
    havemore = True
    while havemore:
      line = f.readline()
      havemore = not not line
      line = line.strip()
      process_entry = False
      if parse_state == parse_states['header']:
        if line != '#2d':
          raise ValueError("Invalid header: %s" % (line,))
        parse_state += 1
      elif parse_state == parse_states['reject']:
        if line == '.':
          parse_state += 1 # ignore disabled hosts stuff for now
      elif parse_state == parse_states['realm']:
        m = realm_re.match(line)
        signon = {}
        if not m:
          signon['hostname'] = line
        else:
          signon['hostname'] = m.group(1)
          signon['http_realm'] = m.group(2)
        parse_state += 1
      elif parse_state == parse_states['userfield']:
        if line == '.':
          parse_state = parse_states['realm']
        else:
          signon['username_field'] = line
          parse_state += 1 
      elif parse_state == parse_states['uservalue']:
        try:
          signon['username'] = nssdecrypt.decrypt(base64.b64decode(line))
        except KeyError, e:
          _logger.error("failed decrypt while processing host '%s'", signon.get('hostname', '(None)'))
        parse_state += 1
      elif parse_state == parse_states['passfield']:
        signon['password_field'] = line
        parse_state += 1
      elif parse_state == parse_states['passvalue']:
        signon['password'] = base64.b64encode(nssdecrypt.decrypt(base64.b64decode(line)))
        parse_state += 1
      elif parse_state == parse_states['actionurl']:
        signon['submiturl'] = line
        process_entry = True
        parse_state = parse_states['userfield']

      if process_entry:
        hostname = signon['hostname']
        if hostname not in new_signons:
          new_signons[hostname] = []
        l = new_signons[hostname]
        l.append(dict(signon))
        _logger.debug("processed signon for %s: %s", hostname, signon)
        process_entry = False

    return new_signons

  def __on_profile_created(self, *args):
    if self.__uncreated_monid is None:
      return
    _logger.debug("profile directory changed")
    try:
      profpath = self.__get_profile_path()
    except KeyError, e:
      return
    self.__uncreated_monid.cancel()
    self.__uncreated_monid = None
    self.__monitor_signons(profpath)

  def __get_profile_path(self):
    if not os.path.isdir(self.__ffpath):
      os.makedirs(self.__ffpath)
    for p in iterdir(self.__ffpath):
      if not p.endswith('.default'): continue
      return p
    raise KeyError("Couldn't find mozilla profile")

  @dbus.service.method(BUS_IFACE_STR,
                       out_signature="a{saa{sv}}")
  def GetSignons(self):
    return self.__signons

  @dbus.service.signal(BUS_IFACE_STR,
                       signature='aa{sv}')
  def SignonChanged(self, signon):
    pass

  @dbus.service.method(BUS_IFACE_STR,
                       in_signature="s",
                       out_signature="aa{sv}")
  def GetSignon(self, signon):
    return self.__signons[signon]

_driver = None
def modmain():
  bus = dbus.SessionBus() 
  bus_name = dbus.service.BusName(BUS_NAME_STR, bus=bus)
  global _driver
  _driver = WebLoginDriver(bus_name)

def main():
  logging.basicConfig()
  gobject.threads_init()
  dbus.glib.threads_init()
  modmain()
  m = gobject.MainLoop()
  m.run()

if __name__ == '__main__':
  main()
