online-desktop r7255 - in trunk: . weblogindriver weblogindriver/weblogindriver



Author: marinaz
Date: Fri Jun 20 17:56:22 2008
New Revision: 7255
URL: http://svn.gnome.org/viewvc/online-desktop?rev=7255&view=rev

Log:
Add onlineaccounts DBus service to the web-login-driver.


Added:
   trunk/weblogindriver/weblogindriver/
   trunk/weblogindriver/weblogindriver/__init__.py
   trunk/weblogindriver/weblogindriver/gutil.py
   trunk/weblogindriver/weblogindriver/keyring.py
   trunk/weblogindriver/weblogindriver/web-login-driver
      - copied unchanged from r7253, /trunk/weblogindriver/web-login-driver
Modified:
   trunk/Makefile-weblogindriver.am
   trunk/online-desktop.schemas.in
   trunk/weblogindriver/web-login-driver

Modified: trunk/Makefile-weblogindriver.am
==============================================================================
--- trunk/Makefile-weblogindriver.am	(original)
+++ trunk/Makefile-weblogindriver.am	Fri Jun 20 17:56:22 2008
@@ -7,6 +7,11 @@
 
 nssdecrypt_la_SOURCES=weblogindriver/pynssdecrypt.c
 
+weblogindriverdir=$(pyexecdir)/weblogindriver
+weblogindriver_PYTHON = weblogindriver/weblogindriver/__init__.py \
+	weblogindriver/weblogindriver/keyring.py \
+        weblogindriver/weblogindriver/gutil.py
+
 bin_SCRIPTS += weblogindriver/web-login-driver
 EXTRA_DIST += weblogindriver/web-login-driver
 
@@ -18,4 +23,4 @@
 $(service_DATA): $(service_in_files) Makefile
 	@sed -e "s|\ bindir\@|$(bindir)|" $< > $@
 
-DISTCLEANFILES += $(service_DATA)
\ No newline at end of file
+DISTCLEANFILES += $(service_DATA)

Modified: trunk/online-desktop.schemas.in
==============================================================================
--- trunk/online-desktop.schemas.in	(original)
+++ trunk/online-desktop.schemas.in	Fri Jun 20 17:56:22 2008
@@ -11,5 +11,32 @@
         <short>Whether to sync preferences to a network server</short>
       </locale>
     </schema>
+    <schema>
+      <key>/schemas/desktop/gnome/online-accounts/TEMPLATE/enabled</key>
+      <owner>gnome</owner>
+      <type>bool</type>
+      <default>TRUE</default>
+      <locale name="C">
+		<short>Whether the account is enabled by the user to be utilized by bigboard stocks.</short>
+      </locale>
+    </schema>
+    <schema>
+      <key>/schemas/desktop/gnome/online-accounts/TEMPLATE/kind</key>
+      <owner>gnome</owner>
+      <type>string</type>
+      <default></default>
+      <locale name="C">
+                <short>The type of the account.</short>
+      </locale>
+    </schema> 
+    <schema>
+      <key>/schemas/desktop/gnome/online-accounts/TEMPLATE/username</key>
+      <owner>gnome</owner>
+      <type>string</type>
+      <default></default>
+      <locale name="C">
+                <short>The username for the account.</short>
+      </locale>
+    </schema>  
   </schemalist>
 </gconfschemafile>

Modified: trunk/weblogindriver/web-login-driver
==============================================================================
--- trunk/weblogindriver/web-login-driver	(original)
+++ trunk/weblogindriver/web-login-driver	Fri Jun 20 17:56:22 2008
@@ -1,31 +1,44 @@
 #!/usr/bin/python
 
-import os,sys,re,logging,urlparse,base64
+import os,sys,re,logging,urlparse,base64,gconf,copy
 
-import gobject, dbus, gnomevfs
+import gobject, dbus, gnomevfs, gobject
 import dbus,dbus.service,dbus.glib
 
-# Custom module
+from ddm import DataModel
+
+# Custom modules
 import nssdecrypt
+import weblogindriver.keyring as keyring
+from weblogindriver.gutil import *
 
 _logger = logging.getLogger("WebLoginDriver")
 
+GCONF_BASE_DIR = "/desktop/gnome/online-accounts"
+
+_accounts_schema_dir = "/schemas" + GCONF_BASE_DIR + "/TEMPLATE"
+
 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')
+  p = None
+  try:
+      p = dbus.SessionBus().get_object('im.pidgin.purple.PurpleService', '/im/pidgin/purple/PurpleObject')
+  except dbus.DBusException, e:
+      _logger.debug("PurpleService not available")
+      return
   target_username = signon['username'] + '@gmail.com'
   a = None
   for acctid in p.PurpleAccountsGetAllActive():
-    username = p.PurpleAccountGetUsername(a)
+    username = p.PurpleAccountGetUsername(acctid)
     try:
-    	(real_username, resource) = username.split('/', 1)
+      (real_username, resource) = username.split('/', 1)    
     except:
-    	real_username = username   
+      real_username = username   
     if real_username == target_username:
-      a = accountid
+      a = acctid
       break
   if not a:
     _logger.debug("creating pidgin account for %s", target_username)
@@ -35,13 +48,33 @@
   p.PurpleAccountSetRememberPassword(a, dbus.UInt32(1))
   do_idle(p.PurpleAccountSetEnabled, a, "gtk-gaim", dbus.UInt32(1))
 
+def associate_schema(schema_dir, new_dir):
+  """
+
+  This function is used when dynamically creating a directory in
+  GConf that should be associated with a fixed schema.
+
+  schema_dir -- the directory where the schema is stored
+  new_dir -- the directory to associate the schema with
+
+  """
+    
+  client = gconf.client_get_default()
+  engine = gconf.engine_get_default()
+  _logger.debug("associating schema schema_dir %s" % schema_dir)
+  for entry in client.all_entries(schema_dir):
+    _logger.debug("entry key %s" % entry.get_key())
+    # There is a schema for each individual key, 
+    key = os.path.join(new_dir, os.path.basename(entry.get_key()))
+    engine.associate_schema(key, entry.get_key())
+
 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,
+  #'GMail': on_pidgin_gmail, # this causes invalid gmail accounts that are stuck in Firefox to be added to pidgin
 }
 
 BUS_NAME_STR='org.gnome.WebLoginDriver'
@@ -269,15 +302,714 @@
   def GetSignon(self, signon):
     return self.__signons[signon]
 
+KIND_GOOGLE = "google"
+KIND_TWITTER = "twitter"
+KIND_RTM = "rtm"
+KIND_LIBRARY_THING = "library"
+
+ONLINE_ACCOUNT_KINDS = {KIND_GOOGLE : "Google", KIND_TWITTER : "Twitter", KIND_RTM : "Remember The Milk", KIND_LIBRARY_THING : "LibraryThing"}
+
+class OnlineAccount(dbus.service.Object):
+  def __init__(self, account_id, bus_name, kind, username, password='', enabled=True, gconf_dir=None):
+    dbus.service.Object.__init__(self, object_path = '/onlineaccount/' + str(account_id), bus_name = bus_name)
+    # TODO: check if dbus.service.Object really doesn't have bus_name as a field
+    self.__account_id = account_id
+    self.__bus_name = bus_name 
+    self.__kind = kind
+    self.__username = username
+    self.__password = password
+    self.__enabled = enabled
+    self.__gconf_dir = gconf_dir
+
+    _logger.debug("Created account %s" % (str(self)))
+
+  # copy and deepcopy might not make much sense for this class, since each instance of the class should have
+  # a unique account_id
+  def __copy__(self):
+    return OnlineAccount(self.__account_id, self.__bus_name, self.__kind, self.__username, self.__password, self.__enabled, self.__gconf_dir)
+
+  def __deepcopy__(self, memo):
+    return OnlineAccount(copy.deepcopy(self.__account_id, memo), copy.deepcopy(self.__bus_name, memo), \
+                         copy.deepcopy(self.__kind, memo), copy.deepcopy(self.__username, memo), \
+                         copy.deepcopy(self.__password, memo), copy.deepcopy(self.__enabled, memo), \
+                         copy.deepcopy(self.__gconf_dir, memo))
+
+  @dbus.service.method(BUS_IFACE_STR,
+                       out_signature="s")
+  def GetKind(self):
+    return self.__kind
+
+  @dbus.service.method(BUS_IFACE_STR,
+                       out_signature="s")
+  def GetUsername(self):
+    return self.__username
+
+  @dbus.service.method(BUS_IFACE_STR,
+                       out_signature="s")
+  def GetPassword(self):
+    return self.__password
+
+  @dbus.service.method(BUS_IFACE_STR,
+                       out_signature="b")
+  def GetEnabled(self):
+    return self.__enabled
+
+  @dbus.service.method(BUS_IFACE_STR,
+                       out_signature="o")
+  def GetObjectPath(self):
+    return self._object_path
+
+  @dbus.service.signal(BUS_IFACE_STR,
+                       signature='')
+  def Changed(self):
+    pass
+
+  def _get_gconf_dir(self):
+    return self.__gconf_dir
+
+  def _update_from_origin(self, new_props):
+    _logger.debug("updating account from origin")
+    changed = False
+    for (key,value) in new_props.items():
+      if key not in ["password", "enabled", "gconf_dir"]:
+        _logger.error("key %s is unknown or can't be changed" % key)
+        continue    
+
+      if value is None:
+        value = ''
+
+      if key != "password":
+        _logger.debug("key %s, value %s" % (key, value))
+      else:
+        _logger.debug("key %s, value length %d" % (key, len(value)))
+
+      old = getattr(self, '_OnlineAccount__' + key)
+      if old != value:
+        setattr(self, '_OnlineAccount__' + key, value)
+        changed = True
+
+    # TODO: figure out if it makes sense to emit a changed signal if gconf_dir changed
+    if changed:
+      _logger.debug("emitting change!")
+      self.Changed()
+
+  def __str__(self):
+    return "<Account userame:%s, kind:%s, gconf_dir:%s, enabled:%s>" % (self.GetUsername(), self.GetKind(), self._get_gconf_dir(), self.GetEnabled())
+
+
+class OnlineAccounts(dbus.service.Object):
+  def __init__(self, bus_name):
+    dbus.service.Object.__init__(self, bus_name, '/onlineaccounts')
+    self.__bus_name = bus_name 
+    self.__received_response_from_server = set()
+
+    #TODO: do we need to pass in the server name, and if so how do we know here what to pass in? 
+    self.__model = DataModel()
+    self.__model.add_ready_handler(self.__on_ready)
+
+    self.__server_accounts = set()
+    self.__gconf_accounts = set()
+    self.__weblogin_accounts = set()
+    self.__enabled_accounts = set()
+
+    # This is a mapping from object paths to OnlineAccount objects for all accounts.
+    # It would have been nice to use the same id as the gconf key id, but we create
+    # accounts before we add them to gconf, and also we create accounts for the
+    # signons returned by web login driver, which we don't add to gconf.
+    self.__all_accounts = {}
+    self.__account_id = None
+
+    try:
+      self.__weblogindriver_proxy = dbus.SessionBus().get_object('org.gnome.WebLoginDriver', '/weblogindriver')
+      self.__weblogindriver_proxy.connect_to_signal("SignonChanged",
+                                                    self.__on_signon_changed)
+    except dbus.DBusException, e:
+      _logger.debug("weblogindriver not available")
+      self.__weblogindriver_proxy = None
+      
+    if self.__weblogindriver_proxy:
+      self.__recheck_signons()
+
+    self.__gconf = gconf.client_get_default()
+    self.__gconf.add_dir(GCONF_BASE_DIR, gconf.CLIENT_PRELOAD_RECURSIVE)
+    self.__gconf.notify_add(GCONF_BASE_DIR, self.__on_gconf_change)
+
+    ## a dict from gconf directory name underneath GCONF_BASE_DIR to
+    ## a dict of the gconf values underneath that account directory
+    self.__gconf_info = {}
+
+    self.__reload_from_gconf()
+
+    if self.__model.ready:
+        self.__on_ready()
+
+  @dbus.service.signal(BUS_IFACE_STR,
+                       signature='o')
+  def AccountAdded(self, account_object_path):
+    pass
+
+  @dbus.service.signal(BUS_IFACE_STR,
+                       signature='o')
+  def AccountRemoved(self, account_object_path):
+    pass
+
+  @dbus.service.signal(BUS_IFACE_STR,
+                       signature='o')
+  def AccountEnabled(self, account_object_path):
+    pass
+
+  @dbus.service.signal(BUS_IFACE_STR,
+                       signature='o')
+  def AccountDisabled(self, account_object_path):
+    pass
+
+  def __create_online_account(self, *args, **kwargs):
+    if self.__account_id is None:
+        self.__account_id = 0 
+    else:
+        self.__account_id = self.__account_id + 1
+    account = OnlineAccount(self.__account_id, self.__bus_name, *args, **kwargs)
+    self.__all_accounts[account.GetObjectPath()] = account   
+    return account  
+
+  def __find_weblogin_account(self, kind, username):
+    for a in self.__weblogin_accounts:
+      _logger.debug("comparing to a weblogin account %s bool %s a.GetKind() %s kind %s" % (a, a.GetKind() == kind, a.GetKind(), kind))
+      if a.GetKind() == kind and a.GetUsername() == username:
+        return a
+
+    return None
+
+  def __find_account_by_gconf_dir(self, gconf_dir):
+    for a in self.__gconf_accounts:
+      if a._get_gconf_dir() == gconf_dir:
+        return a
+
+    return None
+
+  def __get_gconf_accounts_by_kind(self, kind):
+    gconf_accounts_by_kind = set()
+    for a in self.__gconf_accounts:
+      if a.GetKind() == kind:
+        gconf_accounts_by_kind.add(a)
+    return gconf_accounts_by_kind 
+
+  def __get_server_accounts_by_kind(self, kind):
+    server_accounts_by_kind = set()
+    for a in self.__server_accounts:
+      if a.GetKind() == kind:
+        server_accounts_by_kind.add(a)
+    return server_accounts_by_kind 
+
+  def __update_account(self, account):
+    _logger.debug("Updating account %s" % (str(account)))
+
+    ## note that "kind" and "username" are never updated (not allowed to change without
+    ## making a new Account object)
+
+    was_enabled = account in self.__enabled_accounts
+
+    fields = {}
+
+    gconf_dir = account._get_gconf_dir()
+
+    if account in self.__server_accounts:
+      self.__ensure_account_in_gconf(account)
+    elif account.GetKind() in self.__received_response_from_server:
+      #remove gconf dir if one exists
+      if gconf_dir:
+        self.__remove_gconf_dir(gconf_dir)  
+
+    ## first, look for the info from our local gconf storage
+    if gconf_dir and gconf_dir in self.__gconf_info:
+      gconf_info = self.__gconf_info[gconf_dir]
+
+      if 'enabled' in gconf_info:
+        fields['enabled'] = gconf_info['enabled']
+
+    elif gconf_dir and gconf_dir not in self.__gconf_info:
+      ## Account object originating with a gconf item
+      ## apparently now deleted, so disable. This
+      ## does mean if you create a gconf item corresponding
+      ## to something from weblogin driver, then delete the
+      ## gconf item, the weblogin account doesn't come back
+      ## until you restart the process. Clunky.
+      ## deleting a gconf dir isn't what we'd normally do though,
+      ## we'd normally set enabled=False in gconf, which would
+      ## persist across restarts.
+      self.__gconf_accounts.remove(account)
+      if was_enabled:
+          self.__enabled_accounts.remove(account)
+          self.AccountDisabled(account.GetObjectPath())
+      del self.__all_accounts[account.GetObjectPath()]     
+      self.AccountRemoved(account.GetObjectPath())
+      return
+
+    ## after compositing all this information, update our account object
+    account._update_from_origin(fields)
+
+    ## second, look at weblogin driver, though we don't want to prefer
+    ## its password over keyring, so there's some trickiness
+    weblogin_password = None
+    _logger.debug("will look for a weblogin account with kind %s username %s" % (account.GetKind(), account.GetUsername()))
+    weblogin_account = self.__find_weblogin_account(account.GetKind(), account.GetUsername()) 
+    if weblogin_account:
+      _logger.debug("weblogin account found")
+      weblogin_password = weblogin_account.GetPassword()
+
+    ## use updated information to find password
+    fields = {}
+        
+    ## third, look for password in keyring
+    _logger.debug("here 1")    
+    k = keyring.get_keyring()
+    _logger.debug("here 2")                
+    password = k.get_password(kind=account.GetKind(),
+                              username=account.GetUsername(),
+                              url="")
+    _logger.debug("here 3")    
+    if password:
+      _logger.debug("using password from keyring")
+      fields['password'] = password
+            
+    ## fourth, if no password in keyring, use the weblogin one
+    if weblogin_password and 'password' not in fields:
+      _logger.debug("using password from weblogin")
+      fields['password'] = weblogin_password
+
+    ## if no password found, the password has to be set to empty
+    if 'password' not in fields:
+      fields['password'] = ''
+
+    ## update account object again with the password
+    account._update_from_origin(fields)
+
+    ## now add or remove the account from the set of enabled accounts
+    if was_enabled and not account.GetEnabled():
+      self.__enabled_accounts.remove(account)
+      self.AccountDisabled(account.GetObjectPath())
+    elif not was_enabled and account.GetEnabled():
+      self.__enabled_accounts.add(account)
+      _logger.debug("will emit account enabled signal")
+      self.AccountEnabled(account.GetObjectPath())     
+           
+  def __remove_gconf_dir(self, gconf_dir):
+    base_key = GCONF_BASE_DIR + '/' + gconf_dir
+    success = self.__gconf.recursive_unset(base_key, gconf.UNSET_INCLUDING_SCHEMA_NAMES)
+    _logger.debug("removed gconf dir %s success %s" % (base_key, success))
+    self.__gconf.suggest_sync() 
+    if self.__gconf_info.has_key(gconf_dir):
+      del self.__gconf_info[gconf_dir]
+
+  def __try_ensure_and_update_account_for_gconf_dir(self, gconf_dir):         
+    account = self.__find_account_by_gconf_dir(gconf_dir)
+    _logger.debug("ensuring account for %s gconf key %s", account, gconf_dir);
+    if account:
+      self.__update_account(account)
+      return
+        
+    if gconf_dir not in self.__gconf_info:
+      _logger.error("trying to create Account for a gconf dir that doesn't exist")
+      return
+
+    gconf_info = self.__gconf_info[gconf_dir]
+    if 'kind' not in gconf_info or 'username' not in gconf_info:
+      _logger.error("gconf account has no kind or username setting")
+      self.__remove_gconf_dir(gconf_dir)
+      return
+        
+    kind = gconf_info['kind']
+    username = gconf_info['username']
+    if kind not in ONLINE_ACCOUNT_KINDS.keys():
+      _logger.error("unknown account kind in gconf")
+      self.__remove_gconf_dir(gconf_dir)
+      return
+
+    # account = self.__find_weblogin_account_by_kind(kind)
+    # if account:
+    #    account._update_from_origin({"gconf_dir" : gconf_dir})
+    # else:
+    account = self.__create_online_account(kind, username, gconf_dir=gconf_dir, enabled=False)
+
+    self.__gconf_accounts.add(account)
+    self.AccountAdded(account.GetObjectPath()) 
+        
+    self.__update_account(account)
+
+  def __remove_dirname(self, gconf_key):
+    i = gconf_key.rfind('/')
+    return gconf_key[i+1:]
+
+  def __on_ready(self):
+    if self.__model.self_resource != None:
+      _logger.debug("will get online desktop accounts")
+      # TODO: get all gnomeExternalAccounts here later, googleEnabledEmails is just one case
+      query = self.__model.query_resource(self.__model.self_resource, "googleEnabledEmails +")
+      query.add_handler(self.__on_google_enabled_emails)
+      query.add_error_handler(self.__on_datamodel_error)        
+      query.execute()
+        
+  def __on_datamodel_error(self, code, str):
+    _logger.error("datamodel error %s: %s", code, str)        
+        
+  def __on_google_enabled_emails(self, myself):    
+    _logger.debug("received some google enabled emails")
+    myself.connect(self.__update_google_enabled_emails, 'googleEnabledEmails') 
+    self.__update_google_enabled_emails(myself)
+
+  def __update_google_enabled_emails(self, myself):
+    new_google_accounts = set() 
+    if not hasattr(myself, 'googleEnabledEmails'):
+      _logger.debug("No googleEnabledEmails in DDM identity")
+    elif len(myself.googleEnabledEmails) == 0:
+      _logger.debug("DDM identity has 0 googleEnabledEmails")
+    else:
+      for email in myself.googleEnabledEmails:                        
+          username = str(email)
+          _logger.debug("got a googleEnabledEmail %s", email)        
+          new_google_accounts.add(self.__create_online_account(KIND_GOOGLE, username))
+                               
+    self.update_accounts_from_server(KIND_GOOGLE, new_google_accounts)
+     
+  def update_accounts_from_server(self, account_kind, new_accounts):
+    existing_accounts = self.__get_server_accounts_by_kind(account_kind) 
+
+    for new_account in new_accounts:                        
+      # if acccount already in the list of server_accounts, don't do anything
+      # if it is new, add it, and update information for it
+      # if it is no longer returned by the server, remove it and remove it from other
+      # lists, including gconf
+      account_found = False
+      for existing_account in existing_accounts:
+        if existing_account.GetUsername() == new_account.GetUsername():
+          # we found it, we don't need to change anything about it
+          account_found = True
+          existing_accounts.remove(existing_account)
+          break
+             
+      if not account_found:             
+        new_account_to_add = self.__find_account_in_gconf(new_account.GetKind(), new_account.GetUsername())
+        if new_account_to_add is None:
+          new_account_to_add = new_account # we used to use copy.deepcopy here, not sure we need it
+        self.__server_accounts.add(new_account_to_add)
+        # this will add the account to gconf and enabled accounts, and check if 
+        # we have a password for it
+        self.__update_account(new_account_to_add)
+
+    # clear out accounts that are no longer found in the list of accounts of this kind from the server
+    for existing_account in existing_accounts:
+      self.__server_accounts.remove(existing_account)
+      # this should remove the account from gconf and gconf_accounts
+      self.__update_account(existing_account)
+
+    self.__received_response_from_server.add(account_kind)
+
+    # make sure that all accounts in gconf correspond to the ones returned from the server;
+    # this will remove old accounts from gconf 
+    existing_accounts_in_gconf = self.__get_gconf_accounts_by_kind(account_kind) 
+    _logger.debug("existing_accounts_in_gconf of type %s: %d" % (account_kind, len(existing_accounts_in_gconf)))
+    for gconf_account in existing_accounts_in_gconf:
+      self.__update_account(gconf_account) 
+
+  def __get_gconf_info(self, gconf_dir):
+    base_key = GCONF_BASE_DIR + "/" + gconf_dir
+    gconf_info = {}
+    def get_account_prop(gconf, gconf_info, base_key, prop):
+      try:
+        value = gconf.get_value(base_key + '/' + prop)
+      except ValueError:
+        value = None
+      if value:
+        # _logger.debug("key %s, value %s" % (prop, value))  
+        gconf_info[prop] = value
+    get_account_prop(self.__gconf, gconf_info, base_key, 'kind')
+    get_account_prop(self.__gconf, gconf_info, base_key, 'username')
+    get_account_prop(self.__gconf, gconf_info, base_key, 'enabled')
+    # GConf returns None for get_value if the vale is False  
+    if 'enabled' not in gconf_info:
+      gconf_info['enabled'] = False   
+    return gconf_info
+            
+  def __reload_from_gconf(self):
+    gconf_dirs = self.__gconf.all_dirs(GCONF_BASE_DIR)
+
+    _logger.debug("Reloading %s from gconf" % (str(gconf_dirs)))
+
+    new_gconf_infos = {}
+    for gconf_dir in gconf_dirs:
+      base_key = gconf_dir
+      gconf_dir = self.__remove_dirname(gconf_dir)            
+      new_gconf_infos[gconf_dir] = self.__get_gconf_info(gconf_dir)
+
+      # GConf might already have some settings that did not have a schema.
+      # This portion is only for bootstrapping these settings to have a schema.
+      # We check if one of the entries that must be there (username) has a schema,
+      # and associate a schema with the whole directory if it doesn't.
+      username_entry = self.__gconf.get_entry(base_key + '/username', '', True)
+      if username_entry and not username_entry.get_schema_name():
+        associate_schema(_accounts_schema_dir, base_key)              
+      elif not username_entry:
+        _logger.warn("We expected username entry to be in %s, but it was not found" % base_key)
+ 
+    self.__gconf_info = new_gconf_infos
+
+    ## create any new accounts
+    gconf_info_keys = copy.copy(self.__gconf_info.keys())  
+    for gconf_dir in gconf_info_keys:
+      self.__try_ensure_and_update_account_for_gconf_dir(gconf_dir)
+
+    ## now update any old accounts that are no longer in gconf,
+    ## which should result in enabled=False
+    for a in self.__gconf_accounts:
+      gconf_dir = a._get_gconf_dir()
+      _logger.debug("processing gconf account for %s" % gconf_dir)
+      if gconf_dir and gconf_dir not in self.__gconf_info:
+        self.__update_account(a)
+        
+  @defer_idle_func(timeout=400)        
+  def __on_gconf_change(self, *args):
+    _logger.debug("gconf change notify for accounts")
+    self.__reload_from_gconf()
+
+  def __check_signons(self, signons):
+    # this processes signons for a specific hostname
+    # TODO: extend this to store signons for different types of accounts once we are ready to support them;
+    #       make sure we store signon information for GAFYD accounts
+    # actually, it looks like GAFYD accounts are indistinguishable in Firefox from regular Google accounts
+    for signon in signons: 
+      if 'hint' not in signon: continue
+      if signon['hint'] == 'GMail' or signon['hint'] == 'GAFYD-Mail':         
+        username = str(signon['username'])
+        if signon['hint'] == 'GMail':
+            username = str(signon['username']) + "@gmail.com" 
+        password = base64.b64decode(signon['password'])
+        _logger.debug("got signon information %s" % str(signon))                
+        account = self.__find_weblogin_account(KIND_GOOGLE, username)
+        _logger.debug("found weblogin account %s" % str(account))
+        if not account:
+          account = self.__create_online_account(KIND_GOOGLE, username, enabled=False, password=password)
+          self.__weblogin_accounts.add(account)
+          self.__update_account(account)
+        elif password != account.GetPassword():
+          account._set_password(password)
+          self.__update_account(account)
+            
+  def __recheck_signons(self):
+    self.__weblogindriver_proxy.GetSignons(reply_handler=self.__on_get_signons_reply,
+                                           error_handler=self.__on_dbus_error)
+
+  def __on_get_signons_reply(self, signondata):
+    _logger.debug("got signons reply")
+    for hostname,signons in signondata.iteritems():
+      self.__check_signons(signons)
+
+  def __on_signon_changed(self, signons):
+    # TODO: __on_signon_changed is called with all signons for a given hostname.
+    # However, we need the hostname to be passed in as an argument, since it is needed
+    # for the case when no more signons remain for a given hostname.
+    # We should make this change, but for now, we just never remove existing weblogin accounts.
+    # (The list will be re-created when the bigboard is restarted.) We will update a password
+    # for an existing weblogin account if the updated signon information is passed in here.
+    # Once we make this change, we'll need to make sure to associate the right hostnames with
+    # weblogin accounts, in case they don't match what we store in the url.
+    _logger.debug("signons changed: %s", signons)
+    self.__check_signons(signons)
+
+  def __on_dbus_error(self, err):
+    self.__logger.error("D-BUS error: %s", err)    
+
+  def __find_unused_gconf_dir(self, kind):
+    ## find an unused gconf dir
+    i = 0
+    while True:
+      gconf_dir = kind + "_" + str(i)
+      if not self.__find_account_by_gconf_dir(gconf_dir):
+        _logger.debug("returning %s for unused gconf_dir" % gconf_dir)
+        return gconf_dir
+      else:
+        i = i + 1
+
+  def __find_account_in_gconf(self, kind, username):
+    for a in self.__gconf_accounts:
+      if a.GetKind() == kind and a.GetUsername() == username:  
+        return a
+    return None 
+
+  # new_properties is optional, it should be passed in if there are changes
+  # to an existing account and we will update other data constructs that contain
+  # this account based on the information from gconf
+  #
+  # an account object passed in to this method must have the gconf_dir set or must not be found in gconf
+  def __ensure_account_in_gconf(self, account, new_properties={}):       
+    gconf_dir = account._get_gconf_dir()
+    new_gconf_dir = False
+                     
+    # create a new GConf dir if it is a completely new account    
+    if not gconf_dir:
+      if self.__find_account_in_gconf(account.GetKind(), account.GetUsername()):
+        _logger.error("found an account in gconf that matches the passed in account object %s, but the passed in account object is missing a gconf_dir" % str(account))
+        return
+ 
+      gconf_dir = self.__find_unused_gconf_dir(account.GetKind())
+      new_gconf_dir = True
+      account._update_from_origin({"gconf_dir" : gconf_dir})
+      self.__gconf_accounts.add(account)
+            
+    base_key = GCONF_BASE_DIR + '/' + gconf_dir
+
+    _logger.debug("base_key is %s" % base_key)
+        
+    def set_account_prop(base_key, prop, value):
+      _logger.debug("prop %s value %s" % (prop, str(value)))
+      if isinstance(value, bool):
+        self.__gconf.set_bool(base_key + '/' + prop, value)
+      elif isinstance(value, str):
+        self.__gconf.set_string(base_key + '/' + prop, value)
+      else:
+        _logger.error("prop %s with value %s has an unexpected type %s" % (prop, str(value), type(value)))
+
+    if new_gconf_dir:
+      set_account_prop(base_key, 'kind', account.GetKind())
+      set_account_prop(base_key, 'username', account.GetUsername())
+  
+    ## enable it last, so we ignore the other settings until we do this
+    if 'enabled' in new_properties:
+      _logger.debug("setting new enabled property to be %s" % new_properties['enabled'])
+      set_account_prop(base_key, 'enabled', new_properties['enabled'])
+    elif new_gconf_dir:
+      set_account_prop(base_key, 'enabled', account.GetEnabled())
+
+    if new_gconf_dir:
+      associate_schema(_accounts_schema_dir, base_key)
+      self.__gconf_info[gconf_dir]=self.__get_gconf_info(gconf_dir)
+      self.AccountAdded(account.GetObjectPath()) 
+
+  # new_properties is a dictionary, valid properties are "password" and "enabled"
+  @dbus.service.method(BUS_IFACE_STR,                      
+                       in_signature="oa{ss}")
+  def SaveAccountChanges(self, account_object_path, new_properties):
+
+    properties = {}
+    for (key, value) in new_properties.items():
+      if key == 'enabled':   
+          properties[str(key)] = (value == 'True')
+      else:
+          properties[str(key)] = str(value)
+
+    account = None
+    if self.__all_accounts.has_key(account_object_path):
+      account = self.__all_accounts[account_object_path]
+    else:
+      _logger.error("SaveAccountChanges was called with an unknown object path %s" % account_object_path)
+      return
+ 
+    _logger.debug("Saving new props for account %s: %s" % (str(account), str(properties.keys())))
+
+    ## special-case handling of password since it goes in the keyring
+    if 'password' in properties:
+      username = account.GetUsername()
+      url=''          
+      _logger.debug("here 4")    
+      k = keyring.get_keyring()
+      _logger.debug("here 5")    
+      k.store_login(kind=account.GetKind(),
+                    username=username,
+                    url=url,
+                    password=properties['password'])
+      _logger.debug("here 6")
+    
+    ## now do everything else by stuffing it in gconf
+    self.__ensure_account_in_gconf(account, properties)
+      
+    ## keyring doesn't have change notification so we have to do the work for it
+    ## if the password was the only thing that got updated, otherwise we should wait 
+    ## till gconf is reloaded
+    if 'password' in properties and (len(properties) == 1 or ('enabled' in properties and properties['enabled'] == account.GetEnabled())):
+      ## this should notice a new password
+      self.__update_account(account)
+
+  # return a tuple with an account object path as the first element, and a boolean indicating if a new account was created
+  @dbus.service.method(BUS_IFACE_STR,                      
+                       in_signature="ss",
+                       out_signature="(ob)")
+  def GetOrCreateAccount(self, kind, username):        
+    kind = str(kind)
+    username = str(username) 
+    account = self.__find_account_in_gconf(kind, username)
+    if account:         
+      return (account.GetObjectPath(), False)
+          
+    account = self.__create_online_account(kind, username)
+    self.__ensure_account_in_gconf(account)     
+    self.__update_account(account) # this might find a password for the account and emit AccountEnabled signal 
+    return (account.GetObjectPath(), True)
+
+  @dbus.service.method(BUS_IFACE_STR,                      
+                       in_signature="o")
+  def RemoveAccount(self, account):
+    account = None
+    if self.__all_accounts.has_key(account_object_path):
+      account = self.__all_accounts[account_object_path]
+    else:
+      _logger.error("SaveAccountChanges was called with an unknown object path %s" % account_object_path)
+      return
+
+    _logger.debug("will remove account with gconf_dir %s" % account._get_gconf_dir())  
+    self.__remove_gconf_dir(account._get_gconf_dir())
+    # TODO: also remove the account on the server 
+    if account in self.__server_accounts:
+        self.__server_accounts.remove(account)
+    # self.__update_account will remove the account from self.__all_accounts 
+    self.__update_account(account)
+
+  @dbus.service.method(BUS_IFACE_STR,                      
+                       out_signature="ao")
+  def GetEnabledAccounts(self):
+    object_paths_list = [a.GetObjectPath() for a in self.__enabled_accounts]
+    return object_paths_list
+
+  @dbus.service.method(BUS_IFACE_STR,
+                       in_signature="as",                      
+                       out_signature="ao")
+  def GetEnabledAccountsWithKinds(self, kinds):
+    object_paths_list = [a.GetObjectPath() for a in \
+                         filter(lambda account: account.GetKind() in kinds, self.__enabled_accounts)]
+    return object_paths_list
+
+  @dbus.service.method(BUS_IFACE_STR,                      
+                       out_signature="ao")
+  def GetAllAccounts(self):
+    # we use self.__gconf_accounts here, because self.__all_accounts also has random accounts 
+    # from the WebLoginDriver
+    object_paths_list = [a.GetObjectPath() for a in self.__gconf_accounts]
+    return object_paths_list
+
+  @dbus.service.method(BUS_IFACE_STR,
+                       in_signature="as",                      
+                       out_signature="ao")
+  def GetAllAccountsWithKinds(self, kinds):
+    object_paths_list = [a.GetObjectPath() for a in \
+                         filter(lambda account: account.GetKind() in kinds, self.__gconf_accounts)]
+    return object_paths_list
+
+  @dbus.service.method(BUS_IFACE_STR,                      
+                       out_signature="a{ss}")
+  def GetAllAccountKinds(self):
+    return ONLINE_ACCOUNT_KINDS
+
 _driver = None
+_online_accounts = None
 def modmain():
   bus = dbus.SessionBus() 
   bus_name = dbus.service.BusName(BUS_NAME_STR, bus=bus)
   global _driver
   _driver = WebLoginDriver(bus_name)
+  global _online_accounts # so that it doesn't get garbage collected
+  _online_accounts = OnlineAccounts(bus_name)
 
-def main():
-  logging.basicConfig()
+def main(): 
+  gobject.set_application_name("WebLoginDriver")
+  logging.basicConfig(level=logging.DEBUG)
   gobject.threads_init()
   dbus.glib.threads_init()
   modmain()

Added: trunk/weblogindriver/weblogindriver/__init__.py
==============================================================================

Added: trunk/weblogindriver/weblogindriver/gutil.py
==============================================================================
--- (empty file)
+++ trunk/weblogindriver/weblogindriver/gutil.py	Fri Jun 20 17:56:22 2008
@@ -0,0 +1,89 @@
+import os, sys, logging, weakref, functools
+from StringIO import StringIO
+
+import gobject
+
+def _run_logging(f, logger, *args):
+    try:    
+        f(*args)
+    except:
+        logger.exception('Exception in idle')        
+    return False
+
+class DisconnectSet(object):
+    def __init__(self):
+        super(DisconnectSet, self).__init__()
+        self.__connections = set()
+
+    def add(self, object, id):
+        self.__connections.add((object, id))
+
+    def disconnect_object(self, object_to_disconnect):
+        to_remove = []        
+        for (object, id) in self.__connections:
+            if object == object_to_disconnect:
+                object.disconnect(id)
+                to_remove.append((object, id))
+
+        for (object, id) in to_remove:
+            self.__connections.remove((object, id))
+                
+    def disconnect_all(self):
+        for (object, id) in self.__connections:
+            object.disconnect(id)
+
+def call_timeout(timeout, func, *args, **kwargs):
+    if 'logger' in kwargs:
+        logger = kwargs['logger']
+        del kwargs['logger']
+    else:
+        logger = logging
+    return gobject.timeout_add(timeout, functools.partial(_run_logging, func, logger, *args), **kwargs)
+
+def call_idle(func, *args, **kwargs):
+    return call_timeout(0, func, *args, **kwargs)
+
+_global_call_once_funcs = {}
+def _run_removing_from_call_once(f):
+    try:
+        f()
+    finally:
+        del _global_call_once_funcs[f]
+    
+def call_timeout_once(timeout, func, **kwargs):
+    """Call given func exactly once in the next idle time; if func is already pending,
+    it will not be queued multiple times."""
+
+    if func in _global_call_once_funcs:
+        return
+    id = call_timeout(timeout, _run_removing_from_call_once, func, **kwargs)
+    _global_call_once_funcs[func] = id
+    return id
+    
+def call_idle_once(func, **kwargs):
+    return call_timeout_once(0, func, **kwargs)
+
+def defer_idle_func(timeout=100, **kwargs):
+    def wrapped(f):
+        return lambda *margs: call_timeout_once(timeout, functools.partial(f, *margs), **kwargs)
+    return wrapped
+
+def read_subprocess_idle(args, cb):
+    import subprocess
+    subp = subprocess.Popen(args, stdout=subprocess.PIPE, close_fds=True)
+    buf = StringIO()
+    watchid = None
+    def handle_data_avail(src, condition):
+        if (condition & gobject.IO_IN):
+            buf.write(os.read(src, 8192))
+        if ((condition & gobject.IO_HUP) or (condition & gobject.IO_ERR)):
+            cb(buf.getvalue())
+            os.close(src)
+            subp.wait()
+            gobject.source_remove(watchid)
+            return False
+        return True
+    watchid = gobject.io_add_watch(subp.stdout.fileno(), gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP, handle_data_avail)
+    return watchid
+
+__all__ = ['call_timeout', 'call_idle', 'call_timeout_once', 'call_idle_once', 'defer_idle_func', 'read_subprocess_idle', 'DisconnectSet']

Added: trunk/weblogindriver/weblogindriver/keyring.py
==============================================================================
--- (empty file)
+++ trunk/weblogindriver/weblogindriver/keyring.py	Fri Jun 20 17:56:22 2008
@@ -0,0 +1,175 @@
+import logging, gnomekeyring
+
+_logger = logging.getLogger("weblogindriver.Keyring")
+
+class KeyringItem(object):
+    def __init__(self, kind, username='', url='', password=''):
+        super(KeyringItem, self).__init__()        
+        self.__kind = kind
+        self.__username = username
+        self.__url = url
+        self.__password = password
+
+    def get_kind(self):
+        return self.__kind
+
+    def get_username(self):
+        return self.__username
+
+    def get_url(self):
+        return self.__url
+
+    def get_password(self):
+        return self.__password
+
+    def set_kind(self, kind):
+        self.__kind = kind
+
+    def set_username(self, username):
+        self.__username = username
+
+    def set_url(self, url):
+        self.__url = url
+
+    def set_password(self, password):
+        self.__password = password
+
+    def __repr__(self):
+        return '{kind=%s username=%s url=%s len(password)=%d}' % (self.__kind, self.__username, self.__url, len(self.__password))
+
+### The keyring is a map from the tuple (kind,username,url) to a password.
+### In gnome-keyring itself we add to the tuple appname=WebLoginDriver
+class Keyring:
+    def __init__(self, is_singleton):
+        if not is_singleton == 42:
+            raise Exception("use keyring.get_keyring()")
+
+        ### an in-memory substitute for gnome-keyring, set of KeyringItem, used
+        ### when the real keyring is not available
+        self.__fallback_items = set()
+
+    def is_available(self):
+        return gnomekeyring.is_available()
+
+    # Returns a set of KeyringItem
+    def get_logins(self, kind, username, url):
+        matches = set()
+
+        if not self.is_available():
+            for ki in self.__fallback_items:
+                if ki.get_kind() == kind and \
+                   ki.get_username() == username and \
+                   ki.get_url() == url:
+                    matches.add(ki)
+
+        else:
+
+            try:
+                found = gnomekeyring.find_items_sync(gnomekeyring.ITEM_GENERIC_SECRET,
+                                                     dict(appname='WebLoginDriver',
+                                                          kind=kind,
+                                                          username=username,
+                                                          url=url))
+            except gnomekeyring.NoMatchError:
+                found = set()
+                
+            for f in found:
+                ki = KeyringItem(kind=f.attributes['kind'],
+                                 username=f.attributes['username'],
+                                 url=f.attributes['url'],
+                                 password=f.secret)
+                matches.add(ki)
+
+        return matches
+
+    def get_password(self, kind, username, url):
+      logins = self.get_logins(kind, username, url)
+      _logger.debug("got logins: %s" % (str(logins)))
+      if len(logins) > 0:
+          return logins.pop().get_password()
+      else:
+          return None
+        
+    def remove_logins(self, kind, username, url):
+        _logger.debug("removing login (%s, %s, %s)" % (kind, username, url))
+        new_fallbacks = set()
+        for ki in self.__fallback_items:
+            if ki.get_kind() == kind and \
+                   ki.get_username() == username and \
+                   ki.get_url() == url:
+                pass
+            else:
+                new_fallbacks.add(ki)
+                
+        self.__fallback_items = new_fallbacks
+
+        if self.is_available():
+            try:   
+                found = gnomekeyring.find_items_sync(gnomekeyring.ITEM_GENERIC_SECRET,
+                                                     dict(appname='WebLoginDriver',
+                                                          kind=kind,
+                                                          username=username,
+                                                          url=url))
+            except gnomekeyring.NoMatchError:
+                found = set()
+                
+            for f in found:
+                gnomekeyring.item_delete_sync('session', f.item_id)
+  
+    def store_login(self, kind, username, url, password):
+
+        if not password:
+            self.remove_logins(kind, username, url)
+            return
+
+        _logger.debug("storing login (%s, %s, %s)" % (kind, username, url))
+        if not self.is_available():
+            found = None
+            for ki in self.__fallback_items:
+                if ki.get_kind() == kind and \
+                       ki.get_username() == username and \
+                       ki.get_url() == url:
+                    found = ki
+
+            if found:
+                found.set_password(password)
+            else:
+                ki = KeyringItem(kind=kind,
+                                 username=username,
+                                 url=url,
+                                 password=password)
+                self.__fallback_items.add(ki)
+
+        else:  
+            keyring_item_id = gnomekeyring.item_create_sync('session',
+                                                            gnomekeyring.ITEM_GENERIC_SECRET,
+                                                            "WebLoginDriver",
+                                                            dict(appname="WebLoginDriver",
+                                                                 kind=kind,
+                                                                 username=username,
+                                                                 url=url),
+                                                            password, True)
+
+keyring_inst = None
+def get_keyring():
+    global keyring_inst
+    if keyring_inst is None:
+        keyring_inst = Keyring(42)
+    return keyring_inst
+
+
+if __name__ == '__main__':
+    ring = get_keyring()
+
+    print ring.is_available()
+
+    print "storing"
+    ring.store_login(kind='google', username='havoc pennington+foo gmail com',
+                     url='http://google.com/', password='frob')
+
+    print "getting"
+    print ring.get_logins(kind='google', username='havoc pennington+foo gmail com',
+                          url='http://google.com/')
+
+    print "done"
+    



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]