// vim: ts=2 sw=2 expandtab cindent
//
// BEGIN FLOCK GPL
//
// Copyright Flock Inc. 2005-2009
// http://flock.com
//
// This file may be used under the terms of the
// GNU General Public License Version 2 or later (the "GPL"),
// http://www.gnu.org/licenses/gpl.html
//
// Software distributed under the License is distributed on an "AS IS" basis,
// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
// for the specific language governing rights and limitations under the
// License.
//
// END FLOCK GPL

const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;

const WEBSVCMGR_CID = Components.ID("{23d157f0-5941-11db-b0de-0800200c9a66}");
const WEBSVCMGR_CONTRACTID = "@flock.com/webservice-manager;1";

const OBS_TOPIC_FLOCKDOCREADY = "FlockDocumentReady";
const OBS_TOPIC_FORMSUBMIT = "earlyformsubmit";
const OBS_TOPIC_QUIT_APP = "quit-application";


// ========================================================
// ========== BEGIN flockWebServiceManager class ==========
// ========================================================

function flockWebServiceManager()
{
  this._logger = Cc["@flock.com/logger;1"].createInstance(Ci.flockILogger);
  this._logger.init("webServiceManager");
  this._logger.info("Created Web Service Manager Object");

  // Associative array where keys are service contract IDs and values are URNs
  this.mAllServicesByContract = null;

  // Associative array where keys are URNs and values are XPCOM services
  this.mInstantiatedServices = [];

  // Cache the .domains properties of each service, by URN
  this.mDomains = [];

  // mTempIgnore is used to keep track of which services the user has elected
  // NOT to enable Flock support for during this browser session
  this.mTempIgnore = [];

  // mTempAccountsWithNotiesSuppressed is used to keep track of which 
  // accounts the user has elected NOT to recieve notifications for during the
  // current browser session
  this.mTempAccountsWithNotiesSuppressed = [];

  // mWSAccountListeners is the array of flockIWebServiceAccountListeners
  this.mWSAccountListeners = [];
  
  // mKeepAccounts is an associative array of service URNs to dates, used by
  // autoKeepAccountForService()
  this.mKeepAccounts = [];

  this.obs = Cc["@mozilla.org/observer-service;1"]
               .getService(Ci.nsIObserverService);
  this.obs.addObserver(this, OBS_TOPIC_QUIT_APP, false);
  // addObserver calls for FLOCKDOCREADY and FORMSUBMIT are in this.setup()

  this.mEnabled = true;
  this.mRegisteredEvents = false;

  this._metrics = Cc["@flock.com/metrics-service;1"]
                  .getService(Ci.flockIMetricsService);
  
  this._authMgr = Cc["@flock.com/authmgr;1"]
                  .getService(Ci.flockIAuthenticationManager);

  this.setup();
}


// BEGIN nsISupports interface
flockWebServiceManager.prototype.QueryInterface =
function flockWebServiceManager_QueryInterface(aIID)
{
  if ( !aIID.equals(Ci.nsISupports) &&
       !aIID.equals(Ci.nsIObserver) &&
       !aIID.equals(Ci.nsIFormSubmitObserver) &&
       !aIID.equals(Ci.flockIWebServiceManager) )
  {
    throw Cr.NS_ERROR_NO_INTERFACE;
  }
  return this;
}
// END nsISupports interface

// BEGIN nsIFormSubmitObserver interface
flockWebServiceManager.prototype.notify =
function flockWebServiceManager_notify(aFormElement, aWindow, aActionURI)
{
  this.onFormSubmit(aFormElement);
}

// BEGIN nsIObserver interface
flockWebServiceManager.prototype.observe =
function flockWebServiceManager_observe(aSubject, aTopic, aData)
{
  switch (aTopic) {
    //case OBS_TOPIC_FLOCKDATAREADY:
      //this.obs.removeObserver(this, OBS_TOPIC_FLOCKDATAREADY);
      //this.setup();
      //break;


    case OBS_TOPIC_FLOCKDOCREADY:
      this.onDocumentReady(aSubject, aData);
      break;

    case OBS_TOPIC_QUIT_APP:
      if (this.mRegisteredEvents) {
        this.obs.removeObserver(this, OBS_TOPIC_FLOCKDOCREADY);
        this.obs.removeObserver(this, OBS_TOPIC_FORMSUBMIT);
      }
      this.obs.removeObserver(this, OBS_TOPIC_QUIT_APP);
      this._deleteTransientAccounts();
      break;
  }
}
// END nsIObserver interface


// BEGIN flockIWebServiceManager interface
flockWebServiceManager.prototype.instantiateAll =
function flockWebServiceManager_instantiateAll()
{
  this.mAllServicesByContract = [];
  this._logger.info(".instantiateAll()");
  var catMgr = Cc["@mozilla.org/categorymanager;1"]
               .getService(Ci.nsICategoryManager);
  var svcEnum = catMgr.enumerateCategory("wsm-startup");
  var firstRun = ( !this.firstRunPrefs.getPrefType("webservicemanager") ||
                   !this.firstRunPrefs.getBoolPref("webservicemanager") );
  this._logger.info((firstRun) ? "First run" : "NOT first run");
  while (svcEnum.hasMoreElements()) {
    var entry = svcEnum.getNext().QueryInterface(Ci.nsISupportsCString);
    if (!entry) {
      continue;
    }
    var contractID = catMgr.getCategoryEntry("wsm-startup", entry.data);
    this._logger.info("checking "+contractID);
    // Always instantiate web services components on first run, or when an
    // account has already been configured
    var shouldInstantiate = ( firstRun ||
      this.coop.Account.find({serviceId: contractID}).length );
    var findSvc;
    if (!shouldInstantiate) {
      // If it doesn't exist in the RDF, then it was not properly initialized
      // last time, so make sure it gets instantiated now
      findSvc = this.coop.Service.find({serviceId: contractID});
      if (findSvc.length == 0) { shouldInstantiate = true; }
    }
    if (shouldInstantiate) {
      var service = this.instantiateWebService(contractID);
      if (service) {
        this.checkFirstRun(service);
      }
    } else {
      // There's still a possibility that this is the first run for this
      // service -- eg. if it's from an extension
      this._logger.debug("Decided NOT to instantiate "+contractID);
      this.mAllServicesByContract[contractID] = findSvc[0];
      this.checkFirstRun(null, contractID);
    }
  }
}

flockWebServiceManager.prototype.addListener = 
function flockWebServiceManager_addListener(aWSAccountListener)
{
  aWSAccountListener.QueryInterface(Ci.flockIWebServiceAccountListener);
  this.mWSAccountListeners.push(aWSAccountListener);
  this._logger.info(".addListener() ["+this.mWSAccountListeners.length+"]");
}

flockWebServiceManager.prototype.removeListener = 
function flockWebServiceManager_removeListener(aWSAccountListener)
{
  aWSAccountListener.QueryInterface(Ci.flockIWebServiceAccountListener);
  for (var i = 0; i < this.mWSAccountListeners.length; ++i) {
    if (aWSAccountListener == this.mWSAccountListeners[i]) {
      this.mWSAccountListeners.splice(i, 1);
      break;
    }
  }
}

flockWebServiceManager.prototype.autoKeepAccountForService =
function flockWebServiceManager_autoKeepAccountForService(aServiceURN)
{
  var stopTime = new Date();
  stopTime.setHours(stopTime.getHours() + 1);
  this.mKeepAccounts[aServiceURN] = stopTime.getTime();
}
// END flockIWebServiceManager interface


// BEGIN internal helper functions
flockWebServiceManager.prototype.setup =
function flockWebServiceManager_setup()
{
  this._logger.info(".setup()");

  this.coop = Cc["@flock.com/singleton;1"]
              .getService(Ci.flockISingleton)
              .getSingleton("chrome://flock/content/common/load-faves-coop.js")
              .wrappedJSObject;

  this.acUtils = Cc["@flock.com/account-utils;1"]
                   .getService(Ci.flockIAccountUtils);

  this._profiler = Cc["@flock.com/profiler;1"].getService(Ci.flockIProfiler);

  var prefSvc = Cc["@mozilla.org/preferences-service;1"]
                  .getService(Ci.nsIPrefService);
  var prefs = prefSvc.getBranch(null);
  this.firstRunPrefs = prefSvc.getBranch("flock.service.firstruncomplete.");
  this.svcConfPrefs = prefSvc.getBranch("flock.service.confirmation.");
  this.sessionPrefs = prefSvc.getBranch("flock.service.sessionvalue.");
  this.lastloginPrefs = prefSvc.getBranch("flock.service.lastlogin.");

  this.mEnabled = true;
  if (prefs.getPrefType("flock.service.webservicemanager.enabled")) {
    this.mEnabled = prefs.getBoolPref("flock.service.webservicemanager.enabled");
  }
  if (!this.mEnabled) { return; }

  this.obs.addObserver(this, OBS_TOPIC_FORMSUBMIT, false);
  this.obs.addObserver(this, OBS_TOPIC_FLOCKDOCREADY, false);
  this.mRegisteredEvents = true;

  this._deleteTransientAccounts();

  this.firstRunPrefs.setBoolPref("webservicemanager", true);
}

/**
 * Every web service component must get instantiated at least once - generally
 * on first-run - so that it can update the RDF, etc.
 */
flockWebServiceManager.prototype.checkFirstRun =
function flockWebServiceManager_checkFirstRun(aService, aServiceContractID)
{
  var serviceURN = (aService) ? aService.urn
    : this.coop.Service.find({serviceId: aServiceContractID})[0];

  this._logger.info(".checkFirstRun() "+serviceURN);
  if ( !this.firstRunPrefs.getPrefType(serviceURN) ||
       !this.firstRunPrefs.getBoolPref(serviceURN) )
  {
    // This is the first time we are running with this service, so ensure
    // that it is instantiated
    if (!aService) {
      aService = this.instantiateWebService(aServiceContractID);
    }
    this.firstRunPrefs.setBoolPref(serviceURN, true);
  }
}

flockWebServiceManager.prototype.instantiateWebService =
function flockWebServiceManager_instantiateWebService(aServiceContractID)
{
  this._logger.info(".instantiateWebService('"+aServiceContractID+"')");
  var service = Cc[aServiceContractID].getService(Ci.flockIWebService);
  if (!(service instanceof Ci.flockILoginWebService)) {
    this._logger.error("Cannot instantiate the service ("
                       + aServiceContractID
                       + ") because it doesn't implement "
                       + "flockILoginWebService");
    return null;
  }
  this.mAllServicesByContract[aServiceContractID] = service.urn;
  this.mInstantiatedServices[service.urn] = service;
  this.checkPasswordsForService(service);
  return service;
}

flockWebServiceManager.prototype.checkPasswordsForService =
function flockWebServiceManager_checkPasswordsForService(aService)
{
  if (aService.needPassword) {
    // This service requires that we have a password stored in order to have
    // a properly configured account.  Check if we have accounts, and if they
    // don't have passwords then log them out in order to force the user to
    // log in again.
    var accountUrns = this.coop.Account.find({serviceId: aService.contractId});
    var missingAPassword = false;
    for each (var urn in accountUrns) {
      var c_acct = this.coop.Account.get(urn);
      var pw = this.acUtils.getPassword(aService.urn+":"+c_acct.accountId);
      if (!pw) {
        missingAPassword = true;
        if (c_acct.isAuthenticated) {
          aService.logout();
          this.acUtils.markAllAccountsAsLoggedOut(aService);
          return;
        }
      }
    }
    if (missingAPassword) {
      // We are missing a password, but we don't know whether that account is
      // currently logged in or not.  If it's an account that the user never
      // uses, then we don't want to force logout here (because we could be
      // logging them out of the account they actually DO use)
      if (accountUrns.length == 1) {
        // But in this case there's only one account.  This must be the one
        // we're missing the password for.  So we want to log the service out,
        // regardless.  This will force the user to log in again, so we can
        // re-capture that password.
        aService.logout();
        this.acUtils.markAllAccountsAsLoggedOut(aService);
      }
    }
  }
}

flockWebServiceManager.prototype.onFormSubmit =
function flockWebServiceManager_onFormSubmit(aForm)
{
  this._logger.info(".onFormSubmit");

  // Make sure this is an HTML document
  try {
    var url = aForm.ownerDocument.QueryInterface(Ci.nsIDOMHTMLDocument).URL;
  } catch (ex) {
    // If this fails, we're not dealing with an HTML (or XHTML) document.  It
    // could be a newfangled XML thang, which we don't support yet.
    return;
  }

  if (!this.mIOS) {
    this.mIOS = Cc["@mozilla.org/network/io-service;1"]
                  .getService(Ci.nsIIOService);
  }
  var uri = this.mIOS.newURI(url, null, null);

  // Test to make sure this URI has a host
  try {
    var host = uri.host;
  } catch (ex) {
    // If this fails, it's because we're on a chrome page or a locally hosted
    // file, etc.  We just want to bail.
    return;
  }

  if (!this.mAllServicesByContract) {
    this.instantiateAll();
  }
  for (var contractID in this.mAllServicesByContract) {
    this._logger.info("try "+contractID);

    // Check the domain
    var serviceURN = this.mAllServicesByContract[contractID];
    this._logger.info(" - URN is "+serviceURN);
    if (!this.testDomains(serviceURN, uri.host)) {
      this._logger.info("NO MATCH on domains for "+serviceURN);
      continue;
    }
    this._logger.info("form domains MATCH for "+serviceURN);

    // The domain matched, so ensure this service is instantiated
    var service = this.mInstantiatedServices[serviceURN];
    if (!service) {
      service = this.instantiateWebService(contractID);
    }

    // Ask the service if it owns this form being submitted
    if (!service || !service.ownsLoginForm(aForm)) { continue; }
    this._logger.info(" service '"+service.urn+"' says it owns this form.");

    var pw = service.getCredentialsFromForm(aForm);
    if (pw) {
      // The user has just tried to log in, sign up for, or change their
      // password on this service.  We don't know which account it was for,
      // or if they were successful yet -- so for now, store the password
      // temporarily, associated with the service URN.
      this._logger.info(" saving TEMPORARY password for '"+service.urn+
        "' (formType="+pw.formType+")");

      this.acUtils.setTempPassword(service.urn,
                                   pw.username,
                                   pw.password,
                                   pw.formType);
      this._storeLastLoginName(service, pw.username);
    } else {
      this._logger.info(" service'"+service.urn+"' could not retrieve login info, nothing we can do :(");
    }
    // This form has been claimed by a service, but give the other services a
    // chance to claim it as multiple services may share the same page.  For
    // example: flickr.com and mail.yahoo.com share the same login.yahoo.com
    // page.
  }
}

flockWebServiceManager.prototype.onDocumentReady =
function flockWebServiceManager_onDocumentReady(aDocument, aURL)
{
  var profilerEvt = this._profiler.profileEventStart("onDocumentReady");
  this._logger.info(".onDocumentReady('"+aURL+"')");

  // flockILoginWebServices only know how to deal with HTML documents
  if (!(aDocument instanceof Ci.nsIDOMHTMLDocument)) {
    this._profiler.profileEventEnd(profilerEvt, aURL);
    return;
  }

  if (!this.mIOS) {
    this.mIOS = Cc["@mozilla.org/network/io-service;1"]
                  .getService(Ci.nsIIOService);
  }
  var uri = this.mIOS.newURI(aURL, null, null);
  var isOwned = false;

  // Function to alert user if there's a need to re-login
  var inst = this;
  var notifyPasswordMissing = function wsm_passwordMissing(aService) {
    for each (var ls in inst.mWSAccountListeners) {
      if (ls.onAccountPasswordMissing) {
        ls.onAccountPasswordMissing(aService, aDocument);
      }
    }
  };

  if (!this.mAllServicesByContract) {
    this.instantiateAll();
  }
  // Loop through all the services
  for (var contractID in this.mAllServicesByContract) {
    this._logger.info("try "+contractID);
    var authListener;

    // Check the domain
    var serviceURN = this.mAllServicesByContract[contractID];
    this._logger.info(" - URN is "+serviceURN);
    if (!this.testDomains(serviceURN, uri.host)) { continue; }
    this._logger.info("domains MATCH for "+serviceURN);

    // The domain matched, so ensure this service is instantiated
    var service = this.mInstantiatedServices[serviceURN];
    if (!service) {
      service = this.instantiateWebService(contractID);
    }

    // Ask the service if it actually cares about this document
    if (!service || !service.ownsDocument(aDocument)) { continue; }
    this._logger.info(" service '"+service.urn+"' says IT OWNS this document.");
    isOwned = true;
    var account = null;
    var acct_id = null;

    // We now know the service cares about this document, so next check if
    // it's a login landing page
    if (service.docRepresentsSuccessfulLogin(aDocument)) {
      this._logger.info(" service '"+service.urn+"' says this page represents a successful login!");

      // User is logged in -- see which account
      acct_id = service.getAccountIDFromDocument(aDocument);
      this._logger.debug(" account ID is " + acct_id);

      if (acct_id) {
        // We found an account ID on the document, so we're definitely logged
        // in to that account.  Store the session value for future reference.
        this._storeSessionValue(service);
      } else {
        // We could not deduce an account ID from the document.  In this case,
        // we might be able to use the username the user last logged in with
        // and see if we already know of an account with that as an account ID
        // or username.
        var lastLogin = this._getLastLoginName(service);
        if (lastLogin && lastLogin.length) {
          account = this.acUtils.getAccountByLogin(service, lastLogin);
          if (account && this._isSameSession(service)) {
            // An account exists for this login, AND we're in the same login
            // session as when we last saw the account ID on a document, so we
            // can safely assume the account ID associated with this account.
            this._logger.info("session cookie continuity!!");
            acct_id = account.getParam("accountId");
          }
        }
      }

      if (acct_id) {
        if (this.getServiceConfirmation(service.urn)) {
          // We have detected an account ID on this page, and furthermore the
          // service confirmation has not forbade us from trying to use this
          // service.  So let's see if we need to create an account.

          // If we have a temp password stored for this service (from the
          // onFormSubmit call), then we now know that we can associate it with
          // this account ID
          var pw = this.acUtils.getTempPassword(service.urn);
          var pwdKey = service.urn + ":" + acct_id.toLowerCase();
          if (!pw) {
            pw = this.acUtils.getTempPassword(pwdKey);
          }
          if (!pw) {
            // We didn't capture a password when the user logged in to this
            // service... but if the service is part of an auth-managed network
            // then we might be able to use the password from a related account
            pw = this._authMgr
                     .findPasswordFromRelatedAccount(service.contractId,
                                                     acct_id);
          }
          if (pw) {
            var origin = pw.QueryInterface(Ci.flockILoginInfo);
            if (origin.formType == "signup") {
              this._metrics.report("Account-Signup", service.shortName);
            }

            this.acUtils.setTempPassword(pwdKey,
                                         pw.username,
                                         pw.password,
                                         origin.formType);
            this.acUtils.clearTempPassword(service.urn);
          }

          // The service has told us the account ID represented by the
          // document, but that account might not actually exist in the RDF.
          if (!account) {
            account = this.acUtils.getAccountById(service, acct_id);
          }

          if (!account) {
            // Flock does not yet know about this account
            if (pw || !service.needPassword) {
              // We have a password, or else this service does not require a
              // password in order for us to configure an account.
              this._logger.info(" set up new temporary account: " + acct_id);
              var username = acct_id;
              if (pw && pw.username != acct_id) {
                username = pw.username;
              }
              account = this.setUpAccount(service, acct_id, username, aDocument);
              authListener = {
                onSuccess: function listener_onSuccess(aAccount, aTopic) {
                  inst.notifyListeners(aAccount, "accountAuthorized");
                },
                onError: function listener_onError(aFlockError, aTopic) {
                  inst._logger.debug("authListener.onError()");
                }
              };
            } else {
              // We don't have a stored password. That may be due to migrated
              // cookies (bug 6404).
              this._logger.info(" no password associated to the account");
              notifyPasswordMissing(service);
            }
          } else if (account.getParam("isTransient")) {
            // The account is set up, but transient.  We should give the user
            // the option to keep it, unless they have already explicitly
            // dismissed that notification.
            this._logger.info(" found existing temp account: " + acct_id);
            this.checkNotifyAccount(aDocument, account);
          } else {
            // The account is already set up and is non-transient.  The only
            // thing to do is make sure we don't have an out-of-date password.
            this._logger.info(" found existing account: "+acct_id);
            if (pw) {
              this.acUtils.makeTempPasswordPermanent(pwdKey);
            }
          }
        } else {
          this._logger.info(" user indicated to ignore this service");
        }
      } else {
        this._logger.info(" unable to find account ID on this page");
        // This is a somewhat broken state.  We know we're logged in, but we
        // can't tell which account.  Our only hope is to make the user log
        // out and log in again.  Using the "no password" notification to do
        // that.
        notifyPasswordMissing(service);
      }
    }

    // Give the service a chance to update the auth state of its account(s)
    this._logger.info(".updateAccountStatusFromDocument('"
                      + aDocument.URL + "', '" + acct_id + "', ...)");
    service.updateAccountStatusFromDocument(aDocument, account, authListener);

    // Give media services a chance to detect media
    var isMediaService = true;
    try {
      service.QueryInterface(Ci.flockIMediaWebService);
    } catch (ex) {
      isMediaService = false;
    }
    if (isMediaService) {
      service.decorateForMedia(aDocument, account);
    }
    this._profiler.profileEventEnd(profilerEvt, aURL);
    return;
  }
  // If no service owns the doc, notify explore button menu to redraw
  if (!isOwned) {
    this.obs.notifyObservers(aDocument, "media", "media:update");
  }
  this._profiler.profileEventEnd(profilerEvt, aURL);
}

flockWebServiceManager.prototype.testDomains =
function flockWebServiceManager_testDomains(aServiceURN, aHost)
{
  var domains = this.mDomains;
  if (!domains[aServiceURN]) {
    var svc = this.coop.get(aServiceURN);
    if (!svc) {
      // the service is disabled
      return false;
    }
    var domainList = svc.domains;
    if (domainList) {
      this._logger.info("got domains for '"+aServiceURN+"': "+domainList);
      domains[aServiceURN] = domainList.split(",");
    }
  }
  var domainArr = domains[aServiceURN];
  if (!domainArr || domainArr.length == 0) {
    return true;
  }
  for (var i = 0; i < domainArr.length; i++) {
    var domain = domainArr[i];
    if (aHost == domain) return true;
    var idx = aHost.indexOf("."+domain);
    if ((idx > 0) && ((idx + domain.length + 1) == aHost.length)) return true;
  }
  this._logger.info("domains did not match for "+aServiceURN);
  return false;
}

flockWebServiceManager.prototype.suppressNotiesForAccount =
function flockWebServiceManager_suppressNotiesForAccount(aURN) {
  this.mTempAccountsWithNotiesSuppressed[aURN] = true;
};

flockWebServiceManager.prototype.isShowingNotiesForAccount =
function flockWebServiceManager_isShowingNotiesForAccount(aURN) {
  return !this.mTempAccountsWithNotiesSuppressed[aURN];
};

flockWebServiceManager.prototype.getServiceConfirmation =
function flockWebServiceManager_getServiceConfirmation(aURN)
{
  if (this.mTempIgnore[aURN]) {
    return false;
  }
  var askUser = true;  // default value is to ask the user
  if (this.svcConfPrefs.getPrefType(aURN)) {
    askUser = this.svcConfPrefs.getBoolPref(aURN);
  }
  return askUser;
}

flockWebServiceManager.prototype.setServiceConfirmation =
function flockWebServiceManager_setServiceConfirmation(aUrn, aValue)
{
  this.svcConfPrefs.setBoolPref(aUrn, aValue);
}

flockWebServiceManager.prototype.setUpAccount =
function flockWebServiceManager_setUpAccount(aService,
                                             aNewAccountID,
                                             aNewUserName,
                                             aDocument)
{
  this._logger.info("setUpAccount('"+aService.shortName+"', '"+aNewAccountID+"')");
  var inst = this;

  var creationListener = {
    onSuccess: function (aAccount, aTopic) {
      aAccount.QueryInterface(Ci.flockIWebServiceAccount);
      inst.checkNotifyAccount(aDocument, aAccount);
    },
    onError: function (aFlockError, aTopic) {
      inst._logger.debug("setUpAccount() creationListener.onError()");
    }
  };

  // Accounts are created as 'transient' by default, unless
  // autoKeepAccountForService() has been called recently
  var setTransient = true;
  var stopTime = this.mKeepAccounts[aService.urn];
  if (stopTime) {
    var nowTime = (new Date()).getTime();
    if (nowTime < stopTime) {
      setTransient = false;
    } else {
      this.mKeepAccounts[aService.urn] = undefined;
    }
  }

  var acct = aService.addAccount(aNewAccountID, setTransient, creationListener);
  // loginName gets used for API authentication by some services
  acct.setCustomParam("loginName", aNewUserName);
  this._metrics.report("Account-Add", aService.shortName);
  if (!setTransient) {
    acct.keep();
  }
  this._logger.info("acct is: "+acct);
  return acct;
}

flockWebServiceManager.prototype.checkNotifyAccount =
function flockWebServiceManager_checkNotifyAccount(aDocument, aAccount)
{
  aDocument.QueryInterface(Ci.nsIDOMDocument);
  aAccount.QueryInterface(Ci.flockIWebServiceAccount);
  this._logger.info("checkNotifyAccount('"+aAccount.urn+"')");
  this.notifyListeners(aAccount, "accountCreated", aDocument);
}

flockWebServiceManager.prototype.notifyListeners =
function flockWebServiceManager_notifyListeners(aAccount, aEventName, aDocument)
{
  switch (aEventName) {
    case "accountCreated":
      for each (var ls in this.mWSAccountListeners) {
        ls.onAccountCreated(aAccount, aDocument);
      }
      break;
    case "accountPasswordMissing":
      for each (var ls in this.mWSAccountListeners) {
        if (ls.onAccountPasswordMissing) {
          ls.onAccountPasswordMissing(null, aDocument);
        }
      }
      break;
    case "accountAuthorized":
      for each (var ls in this.mWSAccountListeners) {
        ls.onAccountAuthChange(aAccount);
      }
      break;
    default:
      break;
  }
}

/**
 * Call flockILoginWebService#getSessionValue on aService and store the
 * result as a pref.  This is the last known session value for this service.
 */
flockWebServiceManager.prototype._storeSessionValue =
function flockWebServiceManager__storeSessionValue(aService)
{
  this.sessionPrefs.setCharPref(aService.urn, aService.getSessionValue());
}

/**
 * Test whether aService is currently in the "same session" as when we last
 * called _storeSessionValue().
 */
flockWebServiceManager.prototype._isSameSession =
function flockWebServiceManager__isSameSession(aService)
{
  this._logger.info("_isSameSession('" + aService.urn + "')");
  var oldSession = (this.sessionPrefs.getPrefType(aService.urn))
                 ? this.sessionPrefs.getCharPref(aService.urn)
                 : "";
  this._logger.debug(" OLD SESSION: " + oldSession);
  if (oldSession == "") {
    return false;
  }
  var newSession = aService.getSessionValue();
  this._logger.debug(" NEW SESSION: " + newSession);
  return (oldSession == newSession);
}

/**
 * Store aLoginName in a pref, as a record of the last known login ID used
 * to authenticate with aService.
 */
flockWebServiceManager.prototype._storeLastLoginName =
function flockWebServiceManager__storeLastLoginName(aService, aLoginName)
{
  this.lastloginPrefs.setCharPref(aService.urn, aLoginName);
}

/**
 * Returns the last known login ID for aService, or empty string if one hasn't
 * been recorded.
 */
flockWebServiceManager.prototype._getLastLoginName =
function flockWebServiceManager__getLastLoginName(aService)
{
  return (this.lastloginPrefs.getPrefType(aService.urn))
         ? this.lastloginPrefs.getCharPref(aService.urn)
         : "";
}

/**
 * Delete "transient" accounts: accounts that were automatically configured but
 * that the user didn't choose to remember (= make persistent)
 */
flockWebServiceManager.prototype._deleteTransientAccounts =
function flockWebServiceManager__deleteTransientAccounts() {
  var transientAccountUrns = this.coop.Account.find({isTransient: true});
  for each(var urn in transientAccountUrns) {
    var account = this.coop.Account.get(urn);
    this._logger.info("Removing transient Account: " + account.id());
    if (Cc[account.serviceId]) {
      Cc[account.serviceId].getService(Ci.flockIWebService)
                           .removeAccount(account.id());
    } else {
      this.acUtils.removeAccount(account.id());
    }
  }
}
// ========== END flockWebServiceManager class ==========



// ========== BEGIN XPCOM Module support ==========

// BEGIN flockWebServiceManagerModule object
var flockWebServiceManagerModule = {};

flockWebServiceManagerModule.registerSelf =
function flockWebServiceManagerModule_registerSelf(compMgr, fileSpec, location, type)
{
  compMgr = compMgr.QueryInterface(Ci.nsIComponentRegistrar);
  compMgr.registerFactoryLocation( WEBSVCMGR_CID, 
                                   "Flock Web Service Manager JS Component",
                                   WEBSVCMGR_CONTRACTID, 
                                   fileSpec, 
                                   location,
                                   type );
}

flockWebServiceManagerModule.getClassObject =
function flockWebServiceManagerModule_getClassObject(compMgr, cid, iid)
{
  if (!cid.equals(WEBSVCMGR_CID))
    throw Cr.NS_ERROR_NO_INTERFACE;
  if (!iid.equals(Ci.nsIFactory))
    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
  return flockWebServiceManagerFactory;
}

flockWebServiceManagerModule.canUnload =
function flockWebServiceManagerModule_canUnload(compMgr)
{
  return true;
}
// END flockWebServiceManagerModule object


// BEGIN flockWebServiceManagerFactory object
var flockWebServiceManagerFactory = new Object();

flockWebServiceManagerFactory.createInstance =
function flockWebServiceManagerFactory_createInstance(outer, iid)
{
  if (outer != null) {
    throw Cr.NS_ERROR_NO_AGGREGATION;
  }
  return (new flockWebServiceManager()).QueryInterface(iid);
}
// END flockWebServiceManagerFactory object


// NS module entry point
function NSGetModule(compMgr, fileSpec) {
  return flockWebServiceManagerModule;
}

// ========== END XPCOM module support ==========
