// 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 Cu = Components.utils;

Cu.import("resource:///modules/FlockXPCOMUtils.jsm");
FlockXPCOMUtils.debug = false;
Cu.import("resource:///modules/FlockCryptoHash.jsm");
Cu.import("resource:///modules/FlockScheduler.jsm");
Cu.import("resource:///modules/FlockStringBundleHelpers.jsm");
Cu.import("resource:///modules/FlockSvcUtils.jsm");
Cu.import("resource:///modules/FlockFlickrAPI.jsm");
Cu.import("resource:///modules/FlockMediaUtils.jsm");

const FLICKR_TITLE                  = "Flickr Web Service";
const FLICKR_FAVICON                = "chrome://flock/content/services/flickr/favicon.png";
const FLICKR_CID                    = Components.ID("{db720a5c-6315-49bf-a39f-b4d4aa5ed142}");
const FLICKR_CONTRACTID             = "@flock.com/?photo-api-flickr;1";
const SERVICE_ENABLED_PREF          = "flock.service.flickr.enabled";
const CLASS_NAME = "Flickr";
const CLASS_SHORT_NAME = "flickr";
const MODULE_NAME = "flickr";

const flockIError                   = Components.interfaces.flockIError;
const flockIPhotoAlbum              = Components.interfaces.flockIPhotoAlbum;

const FLOCK_PHOTO_CONTRACTID        = "@flock.com/photo;1";
const FLOCK_PHOTO_ALBUM_CONTRACTID  = "@flock.com/photo-album;1";
const FLOCK_ERROR_CONTRACTID        = "@flock.com/error;1";
const FLOCK_RDNDS_CONTRACTID        = "@flock.com/rich-dnd-service;1";

const PEOPLE_PROPERTIES = "chrome://flock/locale/people/people.properties";
const FLICKR_IDENTITY_URN_PREFIX = "urn:flock:identity:flickr:";
const XMLHTTPREQUEST_READYSTATE_COMPLETED = 4;

// The number of contacts to fetch in a call to contacts.getList
const FLICKR_GET_CONTACTS_PER_PAGE = 1000;

// The delay between two refreshes when the sidebar is closed (in seconds)
const FLICKR_REFRESH_INTERVAL = 3600; // 60 minutes
// The delay between two refreshes when the sidebar is open (in seconds)
const FLICKR_SHORT_INTERVAL = 3600; // 60 minutes
// The first time, only get photos not older than one week
const MEDIA_INITIAL_FETCH = 7 * 24 * 3600;
const MILLISEC = 1000;

var gFlickrAPI;

function Namespace(ns) { return function (arg) {
  return Cc["@mozilla.org/rdf/rdf-service;1"]
         .getService(Ci.nsIRDFService)
         .GetResource (ns + arg); };
}
const FLRDF = Namespace("http://flock.com/rdf#");

const FLICKR_STRING_BUNDLE = "chrome://flock/locale/services/flickr.properties";
const SERVICES_STRING_BUNDLE = "services/services";
const SERVICES_PHOTO_FEED_COMMENT_STRING = "flock.photo.feeds.comment.title";

// String defaults... may be updated later through Web Detective
var gStrings = {
  "domains": "flickr.com,yahoo.com",
  "userprofile": "http://www.flickr.com/people/%accountid%/",
  "photopage": "http://www.flickr.com/photos/%accountid%/%photoid%/",
  "commentsreceivedRSS": "http://api.flickr.com/services/feeds/activity.gne?id=%accountid%&format=rss_200",
  "staticimage": "http://static.flickr.com/%server%/%photoid%_%secret%%size%.jpg",
  "buddyicon": "http://www.flickr.com/images/buddyicon.jpg",
  "staticbuddyicon": "http://static.flickr.com/%iconserver%/buddyicons/%owner%.jpg",
  "nofriendavataricon": "http://static.flickr.com/0/buddyicons/%owner%.jpg",
  "nomeavataricon": "http://www.flickr.com/images/buddyicon.jpg?%owner%",
  "apikey": "92c2a562f0e9c2ed8dfe78f42a7734c7",
  "endpoint": "http://www.flickr.com/services/rest/",
  "uploadendpoint": "http://www.flickr.com/services/upload/",
  "authendpoint": "http://www.flickr.com/services/auth/"
};


/*************************************************************************
 * General Helpers
 *************************************************************************/

function loadSubScript(spec) {
  var loader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
              .getService(Components.interfaces.mozIJSSubScriptLoader);
  var context = {};
  loader.loadSubScript(spec, context);
  return context;
}

function _getIdentityUrn(aAccountId, aUid) {
  var result = FLICKR_IDENTITY_URN_PREFIX
             + aAccountId + ":"
             + aUid;
  return result;
}

/*************************************************************************
 * Component: flickrService
 *************************************************************************/

function flickrService() {
  loadSubScript("chrome://browser/content/utilityOverlay.js");
  this.obs = Cc["@mozilla.org/observer-service;1"]
             .getService(Ci.nsIObserverService);

  this._accountClassCtor = flickrAccount;

  FlockSvcUtils.getLogger(this).init("flickr");

  this._ppSvc = Cc["@flock.com/people-service;1"]
                .getService(Ci.flockIPeopleService);

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

  var sbs = Cc["@mozilla.org/intl/stringbundle;1"]
            .getService(Ci.nsIStringBundleService);
  var bundle = sbs.createBundle(FLICKR_STRING_BUNDLE);

  this._channels = {
    "special:recent": {
      title: bundle.GetStringFromName("flock.flickr.title.recent"),
      supportsSearch: false
    },
    "special:interestingness": {
      title: bundle.GetStringFromName("flock.flickr.title.interestingness"),
      supportsSearch: true
    }
  };
  this.albumPhotoList = null;
  this.init();

  var ws = FlockSvcUtils.flockIWebService;
  ws.addDefaultMethod(this, "url");
  ws.addDefaultMethod(this, "getStringBundle");
  ws.addDefaultMethod(this, "isHidden");

  var lws = FlockSvcUtils.flockILoginWebService;
  lws.addDefaultMethod(this, "loginURL");
  lws.addDefaultMethod(this, "tooltip");
  lws.addDefaultMethod(this, "docRepresentsSuccessfulLogin");
  lws.addDefaultMethod(this, "getAccountIDFromDocument");
  lws.addDefaultMethod(this, "getAccountUrns");
  lws.addDefaultMethod(this, "getAccount");
  lws.addDefaultMethod(this, "getAuthenticatedAccount");
  lws.addDefaultMethod(this, "getCredentialsFromForm");
  lws.addDefaultMethod(this, "getSessionValue");
  lws.addDefaultMethod(this, "ownsLoginForm");
  lws.addDefaultMethod(this, "removeAccount");

  FlockSvcUtils.flockIAuthenticateNewAccount
               .addDefaultMethod(this, "authenticateNewAccount");

  var sws = FlockSvcUtils.flockISocialWebService;
  sws.addDefaultMethod(this, "markAllMediaSeen");
  sws.addDefaultMethod(this, "parseStatus");

  var mws = FlockSvcUtils.flockIMediaWebService;
  mws.addDefaultMethod(this, "getChannel");
  mws.addDefaultMethod(this, "enumerateChannels");
  mws.addDefaultMethod(this, "getMediaItemFormatter");
  mws.addDefaultMethod(this, "getIconForQuery");

  var muws = FlockSvcUtils.flockIMediaUploadWebService;
  muws.addDefaultMethod(this, "getAlbumsForUpload");

  FlockSvcUtils.flockIMediaEmbedWebService
               .addDefaultMethod(this, "getSharingContent");

  FlockSvcUtils.flockIRichContentDropHandler
               .addDefaultMethod(this, "_handleTextareaDrop");
}

/*************************************************************************
 * XPCOM Component Creation
 *************************************************************************/

flickrService.prototype = new FlockXPCOMUtils.genericComponent(
  FLICKR_TITLE,
  FLICKR_CID,
  FLICKR_CONTRACTID,
  flickrService,
  Ci.nsIClassInfo.SINGLETON,
  [
    Ci.flockIWebService,
    Ci.flockILoginWebService,
    Ci.flockIAuthenticateNewAccount,
    Ci.flockISocialWebService,
    Ci.flockIMediaWebService,
    Ci.flockIMediaUploadWebService,
    Ci.flockIMediaEmbedWebService,
    Ci.flockIRichContentDropHandler,
    Ci.nsIObserver,
    Ci.flockIPollingService
  ]
);

// FlockXPCOMUtils.genericModule() categories
flickrService.prototype._xpcom_categories = [
  { category: "wsm-startup" },
  { category: "flockWebService", entry: CLASS_SHORT_NAME },
  { category: "flockMediaProvider", entry: CLASS_SHORT_NAME },
  { category: "flockRichContentHandler", entry: CLASS_SHORT_NAME }
];

/*************************************************************************
 * flockIWebService implementation
 *************************************************************************/

// readonly attribute AString title;
flickrService.prototype.title = CLASS_NAME;

// readonly attribute AString shortName;
flickrService.prototype.shortName = CLASS_SHORT_NAME;

// readonly attribute AString icon;
flickrService.prototype.icon = FLICKR_FAVICON;

// DEFAULT: readonly attribute AString url;

// readonly attribute AString contractId;
flickrService.prototype.contractId = FLICKR_CONTRACTID;

// readonly attribute AString urn;
flickrService.prototype.urn = "urn:" + CLASS_SHORT_NAME + ":service";

// DEFAULT: nsIStringBundle getStringBundle();
// DEFAULT: boolean isHidden();

/*************************************************************************
 * flockILoginWebService implementation
 *************************************************************************/

// readonly attribute boolean needPassword;
flickrService.prototype.needPassword = true;

// DEFAULT: readonly attribute AString loginURL;

// flockIWebServiceAccount addAccount(in AString aAccountId, ...
flickrService.prototype.addAccount =
function flickrService_addAccount(aAccountId, aIsTransient,
                                  aFlockListener, aUsername, aToken)
{
  this._logger.info("{flockIWebService}.addAccount('"+aAccountId+"', "+aIsTransient+")");

  var name;
  var token;
  var pollable;
  var auth;
  if (!aUsername) {
    // Get the password associated with this account
    var pw = this._acUtils.getPassword(this.urn+':'+aAccountId);
    // FIX ME - name shouldn't be email address (pw.username) - ja
    name = (pw) ? pw.username : aAccountId;
    token = "";
    pollable = false;
    auth = false;
  } else {
    name = aUsername;
    token = aToken;
    pollable = true;
    auth = true;
  }
  // Account
  var url = gStrings["userprofile"].replace("%accountid%", aAccountId);
  var accountUrn = this._acUtils.createAccount(this,
                                              aAccountId,
                                              name,
                                              url,
                                              aIsTransient);
  // Instanciate account component
  var account = this.getAccount(accountUrn);
  account.setParam("service", this._coopObj);
  account.setParam("refreshInterval", FLICKR_REFRESH_INTERVAL);
  account.setCustomParam("flickr_username", "");
  account.setCustomParam("flickr_fullname", "");
  account.setCustomParam("flickr_nsid", "");
  account.setCustomParam("flickr_comments", 0);
  account.setCustomParam("flickr_comments_timespan", 0);
  if (aFlockListener) {
    aFlockListener.onSuccess(account, "addAccount");
  }
  return account;
};

// DEFAULT: boolean docRepresentsSuccessfulLogin( ...
// DEFAULT: AString getAccountIDFromDocument( ...
// DEFAULT: nsIStringEnumerator getAccountUrns();
// DEFAULT: flockIWebServiceAccount getAccount(in AString aUrn);
// DEFAULT: flockIWebServiceAccount getAuthenticatedAccount();
// DEFAULT: flockILoginInfo getCredentialsFromForm( ...
// DEFAULT: AString getSessionValue();

// void logout();
flickrService.prototype.logout =
function flickrService_logout() {
  this.api.logout();
  this._acUtils.markAllAccountsAsLoggedOut(this);
  this._acUtils.removeCookies(this._WebDetective.getSessionCookies("flickr"));
};

// boolean ownsDocument(in nsIDOMHTMLDocument aDocument);
flickrService.prototype.ownsDocument =
function flickrService_ownsDocument(aDocument) {
  this._logger.debug("domain: " + aDocument.domain);

  // We need to override the default ownsDocument in this case because
  // multiple services share the same login page (yahoo.com in this case)
  return this._WebDetective.detect("flickr", "ownsDocument", aDocument, null);
};

// DEFAULT: boolean ownsLoginForm(in nsIDOMHTMLFormElement aForm);
// DEFAULT: void removeAccount(in AString aUrn);

// void updateAccountStatusFromDocument(in nsIDOMHTMLDocument ...
flickrService.prototype.updateAccountStatusFromDocument =
function flickrService_updateAccountStatusFromDocument(aDocument,
                                                       aAccount,
                                                       aFlockListener)
{
  if (aAccount) {
    // We're logged in to this account
    if (!aAccount.isAuthenticated()) {
      var inst = this;
      var flockListener = {
        onSuccess: function UA_onSuccess(aSubject, aTopic) {
          inst._logger.info(".updateAccountStatusFromDocument(): loginListener: onSuccess()");
          inst._authMgr.reportCompleteLoginToNetwork(aAccount);
          if (aFlockListener) {
            aFlockListener.onSuccess(aAccount, aTopic);
          }
        },
        onError: function UA_onError(aFlockError, aTopic) {
          inst._logger.info(".updateAccountStatusFromDocument(): loginListener: onError()");
          inst._logger.info(aFlockError ? (aFlockError.errorString) : "No details");
        }
      };
      aAccount.login(flockListener);
    }
  } else {
    // No login was detected.  So let's see if we can detect a logged out
    // condition.
    var flickrLogout =
      this._WebDetective.detect("flickr", "loggedout", aDocument, null);
    var yahooNetworkLogout =
      this._WebDetective.detect("flickr", "completeLogout", aDocument, null);
    if (flickrLogout || yahooNetworkLogout) {
      // Yes, we're logged out.  We do slightly different things depending on
      // whether we've just logged out of Flickr, or the whole Yahoo network.
      var loggedoutAccount = null;
      if (flickrLogout) {
        loggedoutAccount = this.getAccount(this.getAuthenticatedAccount());
      }
      this._acUtils.markAllAccountsAsLoggedOut(this);
      this.api.logout();
      this.api.refreshMediabarIfHasPrivate();
      if (flickrLogout && loggedoutAccount) {
        this._authMgr.reportAccountAuthState(loggedoutAccount, false);
      }
      if (yahooNetworkLogout) {
        this._authMgr.reportCompleteLogoutFromNetwork(FLICKR_CONTRACTID);
      }
    }
  }
};

/*************************************************************************
 * flockIAuthenticateNewAccount implementation
 *************************************************************************/

// DEFAULT: void authenticateNewAccount();
flickrService.prototype.authenticateNewAccount =
function flickrService_authenticateNewAccount() {
  this._logger.info("authenticateNewAccount()");
  var inst = this;
  var frobListener = {
    onResult: function (aXML) {
      inst._logger.info(".authenticateNewAccount(): frobListener: onResult()");
      var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
                         .getService(Components.interfaces.nsIWindowMediator);
      var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
                         .getService(Components.interfaces.nsIWindowWatcher);
      var topWin = wm.getMostRecentWindow(null);
      var frob = aXML.getElementsByTagName("frob")[0].firstChild.nodeValue;
      var authURL = inst.api.getAuthUrl(frob, "write");
      var url = "chrome://flock/content/photo/photoLoginWindow.xul?"
              + "url="+escape(authURL)+"&finalString=logout.gne";
      var chrome = "chrome,close,titlebar,resizable=yes,toolbar,dialog=no,"
                 + "scrollbars=yes,modal,centerscreen";
      topWin.open(url, "_blank", chrome);

      var account = inst.getAuthenticatedAccount();
      if (!account) {
        inst._logger.error(".authenticateNewAccount(): frobListener: "
                           + "onResult(): ERROR: account was not created");
      }
    },
    onError: function (aError) {
      inst._logger.info(".authenticateNewAccount(): frobListener: onError()");
    }
  };
  this.api.call(frobListener, "flickr.auth.getFrob", null);
};

/*************************************************************************
 * flockISocialWebService implementation
 *************************************************************************/

// DEFAULT: void markAllMediaSeen(in AString aPersonUrn);

// readonly attribute long maxStatusLength;
flickrService.prototype.maxStatusLength = 0;

/*************************************************************************
 * flockIMediaWebService implementation
 *************************************************************************/

// void decorateForMedia(in nsIDOMHTMLDocument aDocument,
//                       in flockIWebServiceAccount aAccount);
flickrService.prototype.decorateForMedia =
function flickrService_decorateForMedia(aDocument, aAccount) {
  this._logger.info("{flockIMediaWebService}.decorateForMedia(aDocument)");
  var results = Components.classes["@mozilla.org/hash-property-bag;1"]
                          .createInstance(Components.interfaces.nsIWritablePropertyBag2);
  var mediaArr = [];
  var media;
  if (this._WebDetective.detect("flickr", "person", aDocument, results)) {
    media = {
      name: results.getPropertyAsAString("ownername"),
      query: "user:" + results.getPropertyAsAString("userid")
                     + "|username:"
                     + results.getPropertyAsAString("ownername"),
      label: results.getPropertyAsAString("ownername"),
      favicon: this.icon,
      service: this.shortName
    };
    mediaArr.push(media);
  }

  if (this._WebDetective.detect("flickr", "pool", aDocument, results)) {
    media = {
      name: results.getPropertyAsAString("groupname"),
      query: "pool:" + results.getPropertyAsAString("groupid") + "|albumlabel:" + results.getPropertyAsAString("groupname"),
      label: results.getPropertyAsAString("groupname"),
      favicon: this.icon,
      service: this.shortName
    };
    mediaArr.push(media);
  }

  if (mediaArr.length > 0) {
    if (!aDocument._flock_decorations) {
      aDocument._flock_decorations = {};
    }

    if (aDocument._flock_decorations.mediaArr) {
      aDocument._flock_decorations.mediaArr =
        aDocument._flock_decorations.mediaArr.concat(mediaArr);
    } else {
      aDocument._flock_decorations.mediaArr = mediaArr;
    }

    this.obs.notifyObservers(aDocument, "media", "media:update");
  }
};

// void findByUsername(in flockIListener aFlockListener, in AString aUsername);
flickrService.prototype.findByUsername =
function flickrService_findByUsername(aFlockListener, aUsername) {
  var inst = this;
  var userListener =
  function fls_findByUsername_ul(aFlockListener) {
    this.mFlockListener = aFlockListener;
  };
  userListener.prototype.onGetInfo =
  function fls_findByUsername_ul_onGetInfo(aPerson) {
    var wrappedUser = {
      wrappedJSObject: { newUserObj: aPerson }
    };
    this.mFlockListener.onSuccess(wrappedUser, "");
  };
  userListener.prototype.onError =
  function fls_findByUsername_ul_onError(aError) {
    inst._logger.info(aError);
    this.mFlockListener.onError(null, null);
  };
  var myListener = {
    onResult: function fls_findByUsername_ml_onResult(aXML) {
      var user = aXML.getElementsByTagName("user")[0];
      var id = user.getAttribute("id");
      inst.people_getInfo(new userListener(aFlockListener), id);
    },
    onError: function fls_findByUsername_ml_onError(aFlockError) {
      aFlockListener.onError(aFlockError, null);
    }
  };
  var params = {};
  params.username = aUsername;
  this.api.call(myListener, "flickr.people.findByUsername", params);
};

// void getAlbums(in flockIListener aFlockListener, in AString aUsername);
flickrService.prototype.getAlbums =
function flickrService_getAlbums(aFlockListener, aUsername) {
  var inst = this;
  var myListener = {
    onResult: function (aXML) {
      try {
        var rval = inst.handleAlbumsResult(aXML);
        var albumsEnum = {
          hasMoreElements: function albumsEnum_hasMoreElements() {
            return (rval.length > 0);
          },
          getNext: function albumsEnum_getNext() {
            return rval.shift();
          },
          QueryInterface: function albumsEnum_QI(aIID) {
            if (aIID.equals(Ci.nsISimpleEnumerator)) {
              return this;
            }
            throw Cr.NS_ERROR_NO_INTERFACE;
          }
        };
        aFlockListener.onSuccess(albumsEnum, "");
      } catch(e) {
        aFlockListener.onError(null, null);
      }
    },
    onError: function (aFlockError) {
      aFlockListener.onError(aFlockError, null);
    }
  };
  var params = {};
  if (aUsername) {
    params.user_id  = aUsername;
  }
  // Force authentication if the account is authenticated (for upload)
  var authOptional = (this.api.authAccount == null);
  this.api.authenticatedCall(myListener,
                             "flickr.photosets.getList", params, authOptional);
};

// DEFAULT: flockIMediaChannel getChannel(in AString AChannelId);
// DEFAULT: nsISimpleEnumerator enumerateChannels();
// DEFAULT: flockIMediaItemFormatter getMediaItemFormatter();

// void migrateAccount(in AString aId, in AString aUsername);
flickrService.prototype.migrateAccount =
function flickrService_migrateAccount( aId, aUsername ) {
  this.init();

  var token = '';
  /*
  try {
    token = flock_getCharPref('flock.photo.flickr.token');
  } catch (e) { }
  */

  this.addAccount( aId, false, null, aUsername, token);
};

// void search(in flockIListener aFlockListener, ...
flickrService.prototype.search =
function flickrService_search(aFlockListener,
                              aQueryString,
                              aCount,
                              aPage,
                              aRequestId)
{
  this._logger.info(".search(" + aQueryString + ")");

  var aQuery = new QueryHelper(aQueryString);
  if (aQuery.pool) {
    this.poolSearch(aFlockListener, aQueryString, aCount, aPage, aRequestId);
    return;
  }
  if (!aQuery.user) {
    this.queryChannel(aFlockListener, aQueryString, aCount, aPage, aRequestId);
    return;
  }
  var aUserid = aQuery.user;
  var inst = this;
  var myListener = {
    onResult: function myListener_onResult(aXML) {
      var list = aXML.getElementsByTagName("photo");
      var resultsEnum = inst.handlePhotosResult(list, 0, list.length, aUserid);
      aFlockListener.onSuccess(resultsEnum, aRequestId);
    },
    onError: function myListener_onError(aError) {
      aFlockListener.onError(aError, aRequestId);
    }
  };
  // Flickr returns the oldest photos first when calling
  // flickr.photosets.getPhotos!
  // So we need to do a pseudo pagination and enumerate backward :-/
  var myPhotosetListener = {
    onResult: function myPhotosetListener_onResult(aXML) {
      var photoList = inst.albumPhotoList;
      if (aXML) {
        photoList = aXML.getElementsByTagName("photo");
        if (aRequestId) {
          // request comes from the media bar not the flockPhotoAPIManager
          inst.albumPhotoList = photoList;
        }
      }
      var start = -1;
      if (photoList) {
        start = (photoList.length - 1) - aCount * (aPage - 1);
      }
      var end = start - aCount;
      if (end < 0) {
        end = -1;
        inst.albumPhotoList = null;
      }
      var resultsEnum = inst.handlePhotosResult(photoList, start, end, aUserid);
      aFlockListener.onSuccess(resultsEnum, aRequestId);
    },
    onError: function myPhotosetListener_onError(aError) {
      aFlockListener.onError(aError, aRequestId);
    }
  };

  var params = {};
  if (aUserid && aUserid.length) {
    params.user_id = aUserid;
  }

  if (aQuery.search) {
    params.text = aQuery.search;
  }

  params.tag_mode = "all";
  params.extras = "owner_name,license,date_upload,icon_server,media";
  if (aQuery.album) {
    params.photoset_id = aQuery.album;
  }

  if (params.photoset_id) {
    if (aPage > 1) {
      myPhotosetListener.onResult();
      return;
    }
    // get all the photos of the album (max 500)
    this.api.authenticatedCall(myPhotosetListener,
                               "flickr.photosets.getPhotos", params, true);
  } else if (params.user_id || params.text) {
    params.per_page = aCount;
    params.page = aPage;
    this.api.authenticatedCall(myListener,
                               "flickr.photos.search", params, true);
  }
};

// boolean supportsSearch(in AString aQuery);
flickrService.prototype.supportsSearch =
function flickrService_supportsSearch( aQueryString ) {
  var aQuery = new QueryHelper(aQueryString);

  if (aQuery.special) {
    var channel = this._channels["special:" + aQuery.special];
    if (channel) {
      return channel.supportsSearch;
    }
  }

  if (aQuery.album) {
    return false;
  }
  if (aQuery.user) {
    return true;
  }
  if (aQuery.search) {
    return false;
  }
  return false;
};

// DEFAULT: AString getIconForQuery(in AString aQuery);

// readonly attribute boolean supportsUsers;
flickrService.prototype.supportsUsers = true;

/*************************************************************************
 * flockIMediaUploadWebService implementation
 *************************************************************************/

// void getAccountStatus(in flockIListener aFlockListener);
flickrService.prototype.getAccountStatus =
function flickrService_getAccountStatus(aFlockListener) {
  var inst = this;
  var myListener = {
    onResult:function (aXML) {
      try {
        inst._logger.info('we got a result for account status...');
        var result = Cc["@mozilla.org/hash-property-bag;1"]
                     .createInstance(Ci.nsIWritablePropertyBag2);
        var username = aXML.getElementsByTagName('username')[0].firstChild.nodeValue;
        var bandwidth = aXML.getElementsByTagName('bandwidth')[0];
        var maxFileSize = aXML.getElementsByTagName('filesize')[0];
        var isPro = aXML.getElementsByTagName('user')[0].getAttribute('ispro');
        var maxSpace = bandwidth.getAttribute("max");
        var usedSpace = bandwidth.getAttribute("used");
        result.setPropertyAsAString("username", username);
        result.setPropertyAsAString("maxSpace", maxSpace);
        result.setPropertyAsAString("usedSpace", usedSpace);
        result.setPropertyAsAString("maxFileSize", maxFileSize);
        result.setPropertyAsAString("usageUnits", "bytes");
        result.setPropertyAsBool("isPremium", (isPro == "1"));
        aFlockListener.onSuccess(result, "");
      } catch (ex) {
        inst._logger.info('getaccountstatus error >>>>>>>>>>>>>>>>>>>>' + ex);
      }
    },
    onError: function (aError) {
      aFlockListener.onError(aError, "");
    }
  };
  this.api.authenticatedCall(myListener, "flickr.people.getUploadStatus");
};

// DEFAULT: void getAlbumsForUpload(in flockIListener aFlockListener, ...

// void createAlbum(in flockIListener aFlockListener, ...
flickrService.prototype.createAlbum =
function flickrService_createAlbum(aFlockListener, aAccountId, aAlbumName) {
  // trim white space from front and end of string
  aAlbumName = aAlbumName.replace(/^\s+|\s+$/g, "");
  if (aAlbumName) {
    var newAlbum = Components.classes[FLOCK_PHOTO_ALBUM_CONTRACTID]
                             .createInstance(flockIPhotoAlbum);
    newAlbum.title = aAlbumName;
    var date = new Date();  //hopefully this won't collide with an actual id!
    newAlbum.id = date.getTime();
    this.api.fakeAlbums[newAlbum.id] = newAlbum;
    aFlockListener.onSuccess(newAlbum, "");
  } else {
    var error = Cc[FLOCK_ERROR_CONTRACTID].createInstance(Ci.flockIError);
    error.errorCode = error.PHOTOSERVICE_EMPTY_ALBUMNAME;
    aFlockListener.onError(error, null);
  }
};

// boolean supportsFeature(in AString aFeature);
flickrService.prototype.supportsFeature =
function flickrService_supportsFeature(aFeature) {
  var supports = {};
  supports.tags = true;
  supports.title = true;
  supports.fileName = false;
  supports.contacts = true;
  supports.description = true;
  supports.privacy = true;
  supports.albumCreation = true;
  return (supports[aFeature] == true);
};

// void upload(in flockIPhotoUploadAPIListener aUploadListener, ...
flickrService.prototype.upload =
function flickrService_upload(aUploadListener, aUpload, aFile) {
  var params = {};
  params.title = aUpload.title;
  params.description = aUpload.description;
  var prefService = Cc["@mozilla.org/preferences-service;1"]
                    .getService(Ci.nsIPrefService);
  var prefBranch = prefService.getBranch("flock.photo.uploader.");
  if (prefBranch.getPrefType("breadcrumb.enabled") &&
      prefBranch.getBoolPref("breadcrumb.enabled"))
  {
    var sbs = Cc["@mozilla.org/intl/stringbundle;1"]
              .getService(Ci.nsIStringBundleService);
    var sb = sbs.createBundle(FLICKR_STRING_BUNDLE);
    params.description += sb.GetStringFromName("flock.flickr.uploader.breadcrumb");
  }

  params.is_family = aUpload.is_family;
  params.is_friend = aUpload.is_friend;
  params.is_public = aUpload.is_public;
  params.async = "1";
  params.tags = aUpload.tags;
  this.api.upload(aUploadListener, aFile, params, aUpload);
};

/*************************************************************************
 * flockIMediaEmbedWebService implementation
 *************************************************************************/

// boolean checkIsStreamUrl(in AString aUrl);
flickrService.prototype.checkIsStreamUrl =
function flickrService_checkIsStreamUrl(aUrl) {
  if (this._WebDetective.detectNoDOM("flickr", "isStreamUrl", "", aUrl, null)) {
    this._logger.debug("Checking if url is flickr stream: YES: " + aUrl);
    return true;
  }
  this._logger.debug("Checking if url is flickr stream: NO: " + aUrl);
  return false;
};

// void getMediaQueryFromURL(in AString aUrl, ...
flickrService.prototype.getMediaQueryFromURL =
function flickrService_getMediaQueryFromURL(aUrl, aFlockListener) {
  var myListener = {
    onResult:function (aXML) {
      try {
        var results = Components.classes["@mozilla.org/hash-property-bag;1"]
                                .createInstance(Components.interfaces.nsIWritablePropertyBag2);
        var userID = aXML.getElementsByTagName('user')[0].getAttribute('id');
        var userName = aXML.getElementsByTagName('username')[0].firstChild.nodeValue;

        results.setPropertyAsAString("query", "user:" + userID + "|username:" + userName);
        results.setPropertyAsAString("title", userName);
        aFlockListener.onSuccess(results, "query");
      } catch (ex) {
        aFlockListener.onError(null, "Unable to get user.");
      }
    },
    onError: function (aError) {
      aFlockListener.onError(aError, null);
    }
  };

  this._logger.debug("Finding User ID from Url: " + aUrl);
  var params = {};
  params.url = aUrl;
  this.api.call(myListener, "flickr.urls.lookupUser", params);
};

// DEFAULT: boolean getSharingContent(in nsIDOMHTMLElement aSrc, ...

/*************************************************************************
 * flockIRichContentDropHandler implementation
 *************************************************************************/

// boolean handleDrop(in nsITransferable aFlavours, ...
flickrService.prototype.handleDrop =
function flickrService_handleDrop(aFlavours, aTargetElement) {
  this._logger.info(".handleDrop()");

  // Handle textarea drops
  if (aTargetElement instanceof Ci.nsIDOMHTMLTextAreaElement) {
    var dropCallback = function flickr_dropCallback(aFlav) {
      var data = {};
      var len = {};
      aFlavours.getTransferData(aFlav, data, len);
      var caretPos = aTargetElement.selectionEnd;
      var currentValue = aTargetElement.value;

      // Add a trailing space so that we don't mangle the url
      var nextChar = currentValue.charAt(caretPos);
      var trailingSpace = ((nextChar == "") ||
                           (nextChar == " ") ||
                           (nextChar == "\n"))
                        ? ""
                        : " ";

      // We'll only add a breadcrumb if there isn't one already present
      var breadcrumb = "";
      var richDnDSvc = Cc[FLOCK_RDNDS_CONTRACTID]
                       .getService(Ci.flockIRichDNDService);
      if (!richDnDSvc.hasBreadcrumb(aTargetElement)) {
        breadcrumb = richDnDSvc.getBreadcrumb("plain");
      }

      aTargetElement.value = currentValue.substring(0, caretPos)
                           + data.value.QueryInterface(Ci.nsISupportsString)
                                 .data.replace(/: /, ":\n")
                           + trailingSpace
                           + currentValue.substring(caretPos)
                           + breadcrumb;
    };

    return this._handleTextareaDrop(CLASS_SHORT_NAME,
                                    this._coopObj.domains,
                                    aTargetElement,
                                    dropCallback);
  }

  // Default handling otherwise
  return false;
};

/*************************************************************************
 * flockIPollingService implementation
 *************************************************************************/

// void refresh(in AString aUrn, in flockIPollingListener ...
flickrService.prototype.refresh =
function flickrService_refresh(aURN, aPollingListener) {
  this._logger.info("{flockIPollingService}.refresh('"+aURN+"', ...)");
  var account = this.getAccount(aURN);
  var refreshItem = account.coopObj;
  var inst = this;
  if (refreshItem instanceof this._coop.Account) {
    var peopleHash;

    if (!refreshItem.isAuthenticated) {
      this._logger.debug("account not logged in - skipping refresh");
      aPollingListener.onResult();
      return;
    }
    var recentPhotosListener = {
      onResult: function recentPhotosListener_onResult(aXML) {
        var photosList = aXML.getElementsByTagName("photo");
        for (var i = 0; i < photosList.length; i++) {
          var photo = photosList[i];
          var personId = photo.getAttribute("owner");
          var latest = photo.getAttribute("dateupload");

          // Add media info to people hash
          peopleHash[personId].media = {
            count: 1,
            latest: latest
          };
        }

        // Now that we have all we need, update the RDF
        function myWorker(aShouldYield) {
          if (refreshItem.isAuthenticated) {
            // REMOVE locally people removed on the server
            var localChildren = refreshItem.friendsList.children;
            var localEnum = localChildren.enumerate();
            while (localEnum.hasMoreElements()) {
              var identity = localEnum.getNext();
              if (!peopleHash[identity.accountId]) {
                inst._logger.info("Friend " + identity.accountId
                                            + " has been deleted on the server");
                localChildren.remove(identity);
                identity.destroy();
              }
            }

            // ADD or update existing people
            for (var uid in peopleHash) {
              inst.addCoopPerson(peopleHash[uid], refreshItem);
              if (aShouldYield()) {
                yield;
                // XXXX mrr - this looks wrong here
                if (!refreshItem.isAuthenticated) {
                  // Account has just been deleted or logged out
                  break;
                }
              }
            }
          }

          if (inst._acUtils.peopleSidebarOpen() &&
              refreshItem.isAuthenticated)
          {
            refreshItem.nextRefresh = new Date(Date.now()
                                               + FLICKR_SHORT_INTERVAL * 1000);
          }
          aPollingListener.onResult();
        } // end myWorker()
        FlockScheduler.schedule(null, 0.05, 10, myWorker);
      },
      onError: function recentPhotosListener_onError(aFlockError) {
        aPollingListener.onError(aFlockError);
        inst._logger.error("recentPhotosListener error");
      }
    };

    this._getContacts({
      onGetContactsResult: function getContacts_onGetContactsResult(aContacts) {
        inst._updateUserAccount(account);
        peopleHash = aContacts.wrappedJS;

        var api_key = inst.api_key;
        var params = {
          api_key: api_key,
          single_photo: 1,
          count: 50,
          extras: "date_upload"
        };

        inst.api.authenticatedCall(recentPhotosListener,
                                   "flickr.photos.getContactsPhotos", params);
      },
      onError: function getContacts_onError(aFlockError) {
        aPollingListener.onError(aFlockError);
      }
    });

  } else {
    this._logger.error("can't refresh " + aURN + " (unsupported type)");
    aPollingListener.onError(null);
    throw Cr.NS_ERROR_ABORT;
  }
};

/*************************************************************************
 * nsIObserver implementation
 *************************************************************************/

// void observe( in nsISupports aSubject, ...

/*************************************************************************
 * Private Functions
 *************************************************************************/

flickrService.prototype.init =
function flickrService_init() {
  // Prevent re-entry
  if (this.mIsInitialized) {
    return;
  }
  this.mIsInitialized = true;

  this._logger.info(".init()");
  var evtID = this._profiler.profileEventStart("flickr-init");

  // Determine whether this service has been disabled, and unregister if so.
  this.prefService = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
  if ( this.prefService.getPrefType(SERVICE_ENABLED_PREF) &&
       !this.prefService.getBoolPref(SERVICE_ENABLED_PREF) )
  {
    this._logger.info("Pref "+SERVICE_ENABLED_PREF+" set to FALSE... not initializing.");
    var catMgr = Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager);
    catMgr.deleteCategoryEntry("wsm-startup", CLASS_SHORT_NAME, true);
    catMgr.deleteCategoryEntry("flockWebService", CLASS_SHORT_NAME, true);
    catMgr.deleteCategoryEntry("flockMediaProvider", CLASS_SHORT_NAME, true);
    return;
  }

  var inst = this;

  // Initialize member that specifies path for localized string bundle
  this._stringBundlePath = FLICKR_STRING_BUNDLE;

  // getWD() also adds the "_acUtils" property.
  FlockSvcUtils.getWD(this);
  for (var s in gStrings) {
    gStrings[s] = this._WebDetective.getString("flickr", s, gStrings[s]);
  }
  this.domains = gStrings["domains"];
  var authenticatedUrn = FlockSvcUtils.initService(this, true);

  gFlickrAPI = new FlickrAPI();
  this.api = gFlickrAPI;

  // Load auth manager
  this._authMgr = Cc["@flock.com/authmgr;1"]
                  .getService(Ci.flockIAuthenticationManager);
  this._authMgr.joinNetwork(FLICKR_CONTRACTID, "yahoo");

  // On browser restart if a Flickr coop account is still marked as
  // authenticated we will also need to reauth the api. We can't use
  // getAuthenticatedAccount because this service is not yet registered.
  if (authenticatedUrn) {
    this.api.setAuthAccount(authenticatedUrn);
  }

  var timerFunc = {
    inProgress: false,
    count: 0,
    observe: function(subject, topic, state) {
      //if (topic == 'nsPref:changed' && state == FLICKR_TOKEN_PREF_ID) {
      //  var oldToken = flock_getCharPref(FLICKR_TOKEN_PREF_ID);
      //  if (oldToken && oldToken.length && !inst.api.isLoggedIn()) {
      //    this.inProgress = true;
      //    inst.api.checkToken(this, oldToken);
      //  }
      //}
    },
    notify: function(aTimer) {
      if (this.inProgress) {
        return;
      }
      inst.api.processPendingUploadTickets();
      inst.api.processPendingAlbumAdditions();
    },
    onResult: function(aXML) {
      this.inProgress = false;
    },
    onSuccess: function(aXML) {
      this.inProgress = false;
    },
    onError: function(aError) {
      inst.logout();
      this.inProgress = false;
    }
  };
  var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
  timer.initWithCallback(timerFunc, 5 * 1 * 1000, 1);  //re-check token

  this._profiler.profileEventEnd(evtID, "");
};

flickrService.prototype.handlePhotosResult =
function flickrService_handlePhotosResult(aPhotoList, aStart, aEnd, aUserid) {
  var inst = this;
  var resultsEnum = {
    hasMoreElements: function resultsEnum_hasMoreElements() {
      return aStart != aEnd;
    },
    getNext: function resultsEnum_getNext() {
      var photo = aPhotoList[aStart < aEnd ? aStart++ : aStart--];

      var id = photo.getAttribute("id");
      var server = photo.getAttribute("server");
      var secret = photo.getAttribute("secret");
      var title = photo.getAttribute("title");
      var owner = photo.getAttribute("owner");
      if (!owner) {
        owner = aUserid;
      }
      var owner_name = photo.getAttribute("ownername");
      var date_upload = photo.getAttribute("dateupload");
      var date_added = photo.getAttribute("dateadded");

      var icon_server = photo.getAttribute("iconserver");
      var square_url = inst.api.buildPhotoUrl(server, id, secret, "square");
      var small_url = inst.api.buildPhotoUrl(server, id, secret, "small");
      var med_url = inst.api.buildPhotoUrl(server, id, secret, "medium");
      var page_url = inst.api.buildPageUrl(owner, id);
      var media = photo.getAttribute("media");
      var newMediaItem = Cc["@flock.com/photo;1"]
                         .createInstance(Ci.flockIMediaItem);
      newMediaItem.init(inst.shortName, null);
      newMediaItem.webPageUrl = page_url;
      newMediaItem.thumbnail = square_url;
      newMediaItem.midSizePhoto = small_url;
      newMediaItem.largeSizePhoto = med_url;
      newMediaItem.username = owner_name;
      newMediaItem.userid = owner;
      newMediaItem.title = title;
      newMediaItem.id = id;
      if (icon_server == "1") {
        newMediaItem.icon = gStrings["buddyicon"];
      }
      else {
        newMediaItem.icon = gStrings["staticbuddyicon"]
                            .replace("%iconserver%", icon_server)
                            .replace("%owner%", owner);
      }
      newMediaItem.uploadDate = 
                     parseInt(date_added ? date_added : date_upload, 10) * 1000;

      // if the photo result set doesn't return back the ispublic
      // attribute, we assume that the server is returning only
      // public photos back in the response
      var ispublic = "true";
      if (photo.hasAttribute("ispublic")) {
        ispublic = (photo.getAttribute("ispublic") == "1") ? "true" : "false";
      }

      newMediaItem.is_public = ispublic;

      if (media == "video") {
        newMediaItem.is_video = true;
        newMediaItem.mediaType = Ci.flockIMediaItem.MEDIA_TYPE_VIDEO;
      } else {
        newMediaItem.mediaType = Ci.flockIMediaItem.MEDIA_TYPE_IMAGE;
      }
      return newMediaItem;
    },
    QueryInterface: function resultsEnum_QueryInterface(aIID) {
      if (aIID.equals(Ci.nsISimpleEnumerator)) {
        return this;
      }
      throw Cr.NS_ERROR_NO_INTERFACE;
    }
  };
  return resultsEnum;
};

flickrService.prototype.handleContactsResult =
function flickrService__handleContactsResult(aXML, aResults) {
  var headerList = aXML.getElementsByTagName("contacts");
  if (headerList && headerList[0]) {
    aResults.page = parseInt(headerList[0].getAttribute("page"), 10);
    aResults.pages = parseInt(headerList[0].getAttribute("pages"), 10);
  } else {
    // If we can't get the header info, then assume we have them all 
    aResults.page = 1;
    aResults.pages = 1;
  }
  
  var contactList = aXML.getElementsByTagName("contact");
  for (var i = 0; i < contactList.length; i++) {
    var contact = contactList[i];
    var nsid = contact.getAttribute("nsid");
    var username = contact.getAttribute("username");
    var fullname = contact.getAttribute("fullname");
    var family = contact.getAttribute("family");
    var friend = contact.getAttribute("friend");
    var icon_server = contact.getAttribute("iconserver");

    if (true || family == "1" || friend == "1") {
      this._logger.debug(".handleContactsResult(): id=" + nsid
                        + " username=" + username
                        + " family=" + family
                        + " friend=" + friend);

      var newContact = {};
      newContact.id = nsid;
      newContact.username = username;
      newContact.fullname = username;

      // If friend has no avatar, set it to null here and people sidebar
      // code will fill it with the standard Flock no avatar image.
      var noAvatarUrl = gStrings["nofriendavataricon"].replace("%owner%", nsid);
      var avatarUrl = gStrings["staticbuddyicon"]
                             .replace("%iconserver%", icon_server)
                             .replace("%owner%", nsid);
      if (noAvatarUrl == avatarUrl) {
        newContact.avatarUrl = null;
      } else {
        newContact.avatarUrl = avatarUrl;
      }

      aResults.contacts[nsid] = newContact;
    }
  }
};

flickrService.prototype.handleAlbumsResult =
function (aXML) {
  var rval = [];
  var photosetList = aXML.getElementsByTagName("photoset");
  var titleList = aXML.getElementsByTagName("title");
  for (var i = 0; i < photosetList.length; i++) {
    var id = photosetList[i].getAttribute("id");
    var title = titleList[i].firstChild.nodeValue;

    var newAlbum = Components.classes[FLOCK_PHOTO_ALBUM_CONTRACTID]
                             .createInstance(flockIPhotoAlbum);
    newAlbum.id = id;
    newAlbum.title = title;

    rval.push(newAlbum);
  }
  return rval;
};

flickrService.prototype._getContacts =
function flickrService__getContacts(aListener) {
  this._logger.info("._getContacts(...)");
  var inst = this;

  var results = {
    contacts: {},
    page: 1,
    pages: 1
  };

  var params = {
    page: 1,
    per_page: FLICKR_GET_CONTACTS_PER_PAGE
  };
  
  var myListener = {
    onResult: function fs_getContactsOnResult(aXML) {
      inst.handleContactsResult(aXML, results);

      // Get more contacts if necessary
      if (results.page == params.page && results.page < results.pages) {
        params = {
          page: results.page + 1,
          per_page: FLICKR_GET_CONTACTS_PER_PAGE
        };
        inst.api.authenticatedCall(this, "flickr.contacts.getList", params);
      } else {
        // All done so move on to next step
        var contacts = {
          wrappedJS: results.contacts
        };
        aListener.onGetContactsResult(contacts);
      }
    },
    onError: aListener.onError
  };

  // Get the first set of contacts
  this.api.authenticatedCall(myListener, "flickr.contacts.getList", params);
};

flickrService.prototype.people_getInfo =
function (aListener, aUserId) {
  var inst = this;
  var myListener = {
    onResult: function (aXML) {
      var newUserObj = {};
      var person = aXML.getElementsByTagName("person")[0];
      var icon_server = person.getAttribute("iconserver");
      var count = person.getElementsByTagName("photos")[0]
                        .getElementsByTagName("count")[0]
                        .firstChild
                        .nodeValue;
      newUserObj.serviceId = inst.contractId;
      newUserObj.accountId = person.getAttribute("id");
      newUserObj.name = person.getElementsByTagName('username')[0]
                              .firstChild
                              .nodeValue;
      newUserObj.avatar = gStrings["staticbuddyicon"]
                          .replace("%iconserver%", icon_server)
                          .replace("%owner%", newUserObj.accountId);
      newUserObj.totalMedia = parseInt(count);
      aListener.onGetInfo(newUserObj);
    },
    onError: function (aXML) {
      aListener.onError(aXML);
    }
  };
  var params = {};
  params.user_id = aUserId;
  this.api.call(myListener, "flickr.people.getInfo", params);
};

flickrService.prototype.queryChannel =
function flickrService_queryChannel(aFlockListener,
                                    aQueryString,
                                    aCount,
                                    aPage,
                                    aRequestId)
{
  var aQuery = new QueryHelper(aQueryString);
  var inst = this;
  var myListener = {
    onResult: function myListener_onResult(aXML) {
      var list = aXML.getElementsByTagName("photo");
      var resultsEnum = inst.handlePhotosResult(list, 0, list.length);
      aFlockListener.onSuccess(resultsEnum, aRequestId);
    },
    onError: function myListener_onError(aError) {
      aFlockListener.onError(aError, aRequestId);
    }
  };

  var params = {
    per_page: aCount,
    page: aPage,
    extras: "owner_name,icon_server,date_upload,license,media"
  };

  if (aQuery.getKey("special") == "recent") {
    if (aQuery.hasKey("search")) {
      params.text = aQuery.getKey("search");
      this.api.call(myListener, "flickr.photos.search", params);
    } else {
      this.api.call(myListener, "flickr.photos.getRecent", params);
    }
  } else if (aQuery.getKey("special") == "interestingness") {
    if (aQuery.hasKey("search")) {
      params.text = aQuery.getKey("search");
      params.sort = "interestingness-desc";
      this.api.call(myListener, "flickr.photos.search", params);
    } else {
      this.api.call(myListener, "flickr.interestingness.getList", params);
    }
  } else if (aQuery.hasKey("search")) {
    var aText = aQuery.getKey("search");
    // If search string only contains whitespace, consider it blank
    if (aText.replace(/\s/g, "") == "") {
      aText = "";
    }
    if (aText && aText.length) {
      if (aQuery.sort) {
        params.sort = aQuery.sort;
      }
      params.text = aText;
      this.api.call(myListener, "flickr.photos.search", params);
    } else {
      // No search string so just return empty result
      var rval = [];
      var emptyEnum = {
        hasMoreElements: function emptyEnum_hasMoreElements() {
          return (rval.length>0);
        },
        getNext: function emptyEnum_getNext() {
          return rval.shift();
        },
        QueryInterface: function emptyEnum_QueryInterface(aIID) {
          if (aIID.equals(Ci.nsISimpleEnumerator)) {
            return this;
          }
          throw Cr.NS_ERROR_NO_INTERFACE;
        }
      };
      aFlockListener.onSuccess(emptyEnum, aRequestId);
    }
  }
};

flickrService.prototype.poolSearch =
function flickrService_poolSearch(aFlockListener,
                                  aQueryString,
                                  aCount,
                                  aPage,
                                  aRequestId)
{
  var aQuery = new QueryHelper(aQueryString);
  var inst = this;
  var myListener = {
    onResult: function myListener_onResult(aXML) {
      var list = aXML.getElementsByTagName("photo");
      var resultsEnum = inst.handlePhotosResult(list, 0, list.length);
      aFlockListener.onSuccess(resultsEnum, aRequestId);
    },
    onError: function myListener_onError(aError) {
      aFlockListener.onError(aError, aRequestId);
    }
  };

  /*
  var aText;

  if (aQuery.split('?').length > 1) {
    aText = aQuery.split('?')[1];
    aQuery = aQuery.split('?')[0];
  }

  aQuery = aQuery.split(':')[1]
  */

  var params = {
    group_id: aQuery.pool,
    per_page: aCount,
    page: aPage,
    extras: "owner_name,icon_server,date_upload,license,media"
  };

  if (aQuery.search) {
    params.tags = aQuery.search;
    this.api.call(myListener, "flickr.groups.pools.getPhotos", params);
  } else {
    this.api.call(myListener, "flickr.groups.pools.getPhotos", params);
  }
};

flickrService.prototype.addCoopPerson =
function flickrService_addCoopPerson(aPhotoPerson, aCoopAccount) {
  var person = aPhotoPerson;
  var profileURL = gStrings["userprofile"].replace("%accountid%", person.id);
  // We include the current accountId in the identity urn to prevent friend
  // collisions if multiple Facebook accounts have the same friend.
  var identityUrn = _getIdentityUrn(aCoopAccount.accountId,
                                    person.id);
  var updating = this._coop.Identity.exists(identityUrn);
  var identity = this._coop.get(identityUrn);
  if (identity) {
    if (this._personUpdateRequired (aCoopAccount,person)) {
      // update data of the identity coop obj here
      identity.name = person.username;
      identity.URL = profileURL;
    }
  } else {
    identity = new this._coop.Identity(
      identityUrn,
      {
        name: person.username,
        serviceId: FLICKR_CONTRACTID,
        accountId: person.id,
        statusMessage: '',
        lastUpdateType: "media",
        URL: profileURL
      }
    );
    aCoopAccount.friendsList.children.add(identity);
    identity.avatar = person.avatarUrl;
    identity.lastUpdate = 0; // triggers the RDF observers
  }

  if (person.media) {
    this._incrementMedia(person.id, person.media, aCoopAccount);
  }
};

flickrService.prototype._incrementMedia =
function flickrService__incrementMedia(aUid, aMedia, aCoopAccount) {
  this._logger.info("._incrementMedia('" + aUid + "')");
  var currAcctId = aCoopAccount.accountId;

  // Update the count on the identity...
  var identityUrn = _getIdentityUrn(currAcctId, aUid);
  var identity = this._coop.get(identityUrn);

  if (!identity.lastUpdate) {
    identity.lastUpdate = aMedia.latest;
    var lastweek = parseInt(Date.now() / 1000) - MEDIA_INITIAL_FETCH;
    // Friend uploaded pictures in the last week!
    if (aMedia.latest > lastweek) {
      identity.unseenMedia += aMedia.count;
    }
  } else if (aMedia.latest > identity.lastUpdate) {
    identity.unseenMedia += aMedia.count;
    identity.lastUpdate = aMedia.latest;
  }
};

flickrService.prototype._personUpdateRequired =
function flickrService__personUpdateRequired(aCoopPerson, aPerson) {
  return (aCoopPerson.name != aPerson.fullname)
      || (aCoopPerson.avatar != aPerson.pic_square);
};

flickrService.prototype._updateUserCommentCount =
function flickrService__updateUserCommentCount(aCoopAccount) {
  this._logger.info("._updateUserCommentCount('" + aCoopAccount.id() + "')");
  var timespanMS = 0;
  var lastClearedDateSec = 0;
  var account = this.getAccount(aCoopAccount.id());
  var updateTimeMS;
  if (!aCoopAccount.lastUpdateDate) {
    // First time refreshing this account, so notify on all photo comments for
    // the past week.
    aCoopAccount.lastUpdateDate = new Date();
    updateTimeMS =  aCoopAccount.lastUpdateDate.getTime();
    var lastweek = updateTimeMS - (MEDIA_INITIAL_FETCH * MILLISEC);
    timespanMS = updateTimeMS - lastweek;
    account.setCustomParam("flickr_comments", 0);
    account.setCustomParam("flickr_comments_timespan", 0);
    lastClearedDateSec = parseInt(lastweek / MILLISEC);
  } else {
    var today = new Date();
    updateTimeMS =  aCoopAccount.lastUpdateDate.getTime();
    timespanMS = today.getTime() - updateTimeMS;
    lastClearedDateSec = parseInt(updateTimeMS / MILLISEC);
  }
  // This controls the time span (in hours) that photo comments will be shown
  // for when the user chooses to view comment noties.
  var timespanHrs = timespanMS / (60 * 60 * MILLISEC);
  var acc_timespanHrs = account.getCustomParam("flickr_comments");
  var timespan = acc_timespanHrs + timespanHrs;
  account.setCustomParam("flickr_comments_timespan", timespan);

  var api_key = this.api_key;
  var params = {
    api_key: api_key,
    timeframe: timespanHrs + "h"
  };
  var inst = this;
  var commentListener = {
    onResult: function commentListener_onResult(aXML) {
      var items = aXML.getElementsByTagName("item");
      var lightUp = false;
      for (var i = 0; i< items.length; i++) {
        var events = items[i].getElementsByTagName("event");
        for (var j = 0; j< events.length; j++) {
          var dateadded = events[j].getAttribute("dateadded");
          // Dateadded greater than last checked time,
          // we have new comment to see! Light up!
          dateadded = parseInt(dateadded);
          if (lastClearedDateSec < dateadded) {
            var newdate = new Date();
            newdate.setTime(dateadded * MILLISEC);
            aCoopAccount.lastUpdateDate = newdate;
            var comments = account.getCustomParam("flickr_comments");
            account.setCustomParam("flickr_comments", ++comments);
            lightUp = true;
          }
        }
      }
      if (lightUp) {
        inst._ppSvc.togglePeopleIcon(true);
      }
    },
    onError: function commentListener_onError(aFlockError) {
    }
  };
  this.api.authenticatedCall(commentListener,
                             "flickr.activity.userPhotos", params);
};

flickrService.prototype._updateUserAccount =
function flickrService__updateUserAccount(aAccount) {
  this._logger.info("._updateUserAccount('" + aAccount.urn + "')");
  var inst = this;
  var hr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
           .createInstance(Ci.nsIXMLHttpRequest);
  hr.mozBackgroundRequest = true;
  hr.onreadystatechange = function updateAccount_onreadystatechange(aEvent) {
    if (hr.readyState == XMLHTTPREQUEST_READYSTATE_COMPLETED) {
      var results = Cc["@mozilla.org/hash-property-bag;1"]
                    .createInstance(Ci.nsIWritablePropertyBag2);
      if (inst._WebDetective.detectNoDOM("flickr", "accountinfo", "",
                                        hr.responseText, results))
      {
        inst._updateUserStatusFromResults(aAccount.coopObj, results);
        inst._updateUserCommentCount(aAccount.coopObj);
        inst.obs.notifyObservers(aAccount, "flock-acct-refresh", "user-info");
      }
    }
  };
  hr.open("GET", this._WebDetective.getString("flickr", "scrapeAccountInfo",
                                             "http://www.flickr.com/"));
  hr.send(null);
};

flickrService.prototype._updateUserStatusFromResults =
function flickrService__updateUserStatusFromResults(aCoopAccount, aResults) {
  this._logger.info("._updateUserStatusFromResults('" + aCoopAccount.id() + "')");
  var username = aResults.getPropertyAsAString("username");
  if (username && username.length) {
    aCoopAccount.name = username;
  }
  var avatarURL = aResults.getPropertyAsAString("avatarURL");
  if (avatarURL && avatarURL.length) {
    // If user account has no avatar, set it to null here and people sidebar
    // code will fill it with the standard Flock no avatar image.
    var accountid = aResults.getPropertyAsAString("accountid");
    var noAvatarUrl = gStrings["nomeavataricon"].replace("%owner%", accountid);
    if (noAvatarUrl == avatarURL) {
      aCoopAccount.avatar = null;
    } else {
      aCoopAccount.avatar = avatarURL;
    }
  }
  var messages = "0";
  try {
    messages = aResults.getPropertyAsAString("messages");
    if (messages && messages.length) {
      if (aCoopAccount.accountMessages < messages) {
        this._ppSvc.togglePeopleIcon(true);
      }
    }
    else {
      messages = "0";
    }
  } catch(ex) {}
  aCoopAccount.accountMessages = messages;
};

// ===== BEGIN probably-unused cruft =====

flickrService.prototype.__defineGetter__('isUploading', function () { return this.running; });

flickrService.prototype.cancelUpload =
function flickrService_cancelUpload() {
  try {
    this.api.uploader.req.abort();
  } catch(ex) {
    // Ignore error unless this becomes non-cruft in the next assessment.
  }
};

// ===== END probably-unused cruft =======

/**************************************************************************
 * flickrAccount
 **************************************************************************/

function flickrAccount() {
  FlockSvcUtils.getLogger(this).init("flickrAccount");
  FlockSvcUtils.getACUtils(this);
  FlockSvcUtils.getCoop(this);

  this.svc = Cc[FLICKR_CONTRACTID]
             .getService(Ci.flockIMediaWebService)
             .QueryInterface(Ci.flockIWebService);
  if (!gFlickrAPI) {
    gFlickrAPI = new FlickrAPI();
  }
  this.api = gFlickrAPI;

  this._WebDetective = Cc["@flock.com/web-detective;1"]
                      .getService(Ci.flockIWebDetective);

  var wsa = FlockSvcUtils.flockIWebServiceAccount;
  wsa.addDefaultMethod(this, "getService");
  wsa.addDefaultMethod(this, "isAuthenticated");
  wsa.addDefaultMethod(this, "getParam");
  wsa.addDefaultMethod(this, "setParam");
  wsa.addDefaultMethod(this, "getCustomParam");
  wsa.addDefaultMethod(this, "getAllCustomParams");
  wsa.addDefaultMethod(this, "setCustomParam");
  wsa.addDefaultMethod(this, "logout");

  var sa = FlockSvcUtils.flockISocialAccount;
  sa.addDefaultMethod(this, "formatFriendActivityForDisplay");
  sa.addDefaultMethod(this, "getFormattedFriendUpdateType");
  sa.addDefaultMethod(this, "getFriendCount");
  sa.addDefaultMethod(this, "enumerateFriends");
  sa.addDefaultMethod(this, "getInviteFriendsURL");
}

/**************************************************************************
 * flickrAccount: XPCOM Component Creation
 **************************************************************************/

flickrAccount.prototype = new FlockXPCOMUtils.genericComponent(
  CLASS_NAME + " Account",
  "",
  "",
  flickrAccount,
  0,
  [
    Ci.flockIWebServiceAccount,
    Ci.flockISocialAccount,
    Ci.flockIMediaAccount,
    Ci.flockIMediaUploadAccount
  ]
);


/*************************************************************************
 * flockIWebServiceAccount implementation
 *************************************************************************/

// readonly attribute AString urn;
flickrAccount.prototype.urn = null;

// DEFAULT: flockILoginWebService getService();

// void login(in flockIListener aFlockListener);
flickrAccount.prototype.login =
function flickrAccount_login(aFlockListener) {
  this._logger.info("{flockIWebServiceAccount}.login()");
  this.api.setAuthAccount(this.urn);
  this._acUtils.ensureOnlyAuthenticatedAccount(this);
  // Force refresh on login is done by the people sidebar
  if (aFlockListener) {
    aFlockListener.onSuccess(this, "login");
  }
};

// DEFAULT: void logout(in flockIListener aFlockListener);

// void keep();
flickrAccount.prototype.keep =
function flickrAccount_keep() {
  this._logger.info(".keep(): "+this.urn);
  var accountId = this.coopObj.accountId;
  this.coopObj.isTransient = false;
  this._acUtils.makeTempPasswordPermanent(this.svc.urn + ":" + accountId);

  // Subscribe to the recent activity RSS feed for this account
  var feedMgr = Cc["@flock.com/feed-manager;1"]
                .getService(Ci.flockIFeedManager);
  var inst = this;
  var feedListener = {
    onGetFeedComplete: function keep_feed_complete(aFeed) {
      feedMgr.getFeedContext("news").getRoot().subscribeFeed(aFeed);
      Cc["@flock.com/metrics-service;1"]
        .getService(Ci.flockIMetricsService)
        .report("FeedsSidebar-AddFeed",  "FlickrAccountKeep");

      var coop = FlockSvcUtils.getCoop(this);

      var title = flockGetString(SERVICES_STRING_BUNDLE,
                                 SERVICES_PHOTO_FEED_COMMENT_STRING);
      var username = inst.coopObj.name;

      var feedTitle = title.replace("%s", username);

      coop.get(aFeed.id()).name = feedTitle;
    },
    onError: function keep_feed_error(aError) {
      inst._logger.debug("There was a problem subscribing to the recent "
                         + "activity feed for Flickr account " + accountId);
    }
  };
  try {
    var feedURI = Cc["@mozilla.org/network/standard-url;1"]
                  .createInstance(Ci.nsIURI);
    feedURI.spec = gStrings["commentsreceivedRSS"]
                   .replace("%accountid%", accountId);
    feedMgr.getFeed(feedURI, feedListener);
  } catch (ex) {
    this._logger.debug("There was an error subscribing to the recent "
                       + "activity feed for Flickr account " + accountId);
  }
};

// DEFAULT: boolean isAuthenticated();
// DEFAULT: nsIVariant getParam(in AString aParamName);
// DEFAULT: void setParam(in AString aParamName, ...
// DEFAULT: nsIVariant getCustomParam(in AString aParamName);
// DEFAULT: nsIPropertyBag getAllCustomParams();
// DEFAULT: void setCustomParam(in AString aParamName, ...

/*************************************************************************
 * flockISocialAccount implementation
 *************************************************************************/

// readonly attribute boolean hasFriendActions;
flickrAccount.prototype.hasFriendActions = true;

// readonly attribute boolean isMeStatusSupported;
flickrAccount.prototype.isMeStatusSupported = false;

// readonly attribute boolean isFriendStatusSupported;
flickrAccount.prototype.isFriendStatusSupported = false;

// readonly attribute boolean isStatusEditable;
flickrAccount.prototype.isStatusEditable  = false;

// readonly attribute boolean isPostLinkSupported;
flickrAccount.prototype.isPostLinkSupported = false;

// readonly attribute boolean isMyMediaFavoritesSupported;
flickrAccount.prototype.isMyMediaFavoritesSupported = false;

// void setStatus(in AString aStatusMessage, ...
flickrAccount.prototype.setStatus =
function flickrAccount_setStatus(aStatusMessage, aFlockListener) {
  this._logger.info(".setStatus('" + aStatusMessage + "', aFlockListener)");
};

// AString getEditableStatus();
flickrAccount.prototype.getEditableStatus =
function flickrAccount_getEditableStatus() {
  this._logger.info("{flockISocialAccount}.getEditableStatus()");
  return "";
};

// AString formatStatusForDisplay(in AString aStatusMessage);
flickrAccount.prototype.formatStatusForDisplay =
function flickrAccount_formatStatusForDisplay(aStatusMessage) {
  return "";
};

// DEFAULT: AString formatFriendActivityForDisplay(in AString ...

// AString getMeNotifications();
flickrAccount.prototype.getMeNotifications =
function flickrAccount_getMeNotifications() {
  this._logger.info(".getMeNotifications()");

  var sbs = Cc["@mozilla.org/intl/stringbundle;1"]
            .getService(Ci.nsIStringBundleService);
  var bundle = sbs.createBundle(FLICKR_STRING_BUNDLE);

  var noties = [];
  var inst = this;
  function _addNotie(aType, aCount, aUrl) {
    var stringName = "flock.flickr.noties."
                   + aType + "."
                   + ((parseInt(aCount) <= 0) ? "none" : "some");
    noties.push({
      class: aType,
      tooltip: bundle.GetStringFromName(stringName),
      metricsName: aType,
      count: aCount,
      URL: aUrl
    });
  }
  var url = this._WebDetective.getString("flickr", "meMessages_URL", "");
  _addNotie("meMessages", this.coopObj.accountMessages, url);

  var timespan = this.getCustomParam("flickr_comments_timespan");
  timespan = (timespan) ? timespan : "";
  url = this._WebDetective.getString("flickr", "meCommentActivity_URL", "")
            .replace("%timespan%", timespan);

  var comments = this.getCustomParam("flickr_comments");
  comments = (comments) ? comments : 0;
  _addNotie("meCommentActivity", comments, url);
  var nsJSON = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
  return nsJSON.encode(noties);
};

// DEFAULT: AString getFormattedFriendUpdateType(in AString ...

// AString getFriendActions(in AString aFriendURN);
flickrAccount.prototype.getFriendActions =
function flickrAccount_getFriendActions(aFriendURN) {
  this._logger.info(".getFriendActions('" + aFriendURN + "')");

  var actionNames = ["friendMessage",
                     "friendViewProfile",
                     "friendShareFlock"];

  var sbs = Cc["@mozilla.org/intl/stringbundle;1"]
            .getService(Ci.nsIStringBundleService);
  var bundle = sbs.createBundle(FLICKR_STRING_BUNDLE);

  var actions = [];
  var c_friend = this._coop.get(aFriendURN);
  if (c_friend) {
    for each (var i in actionNames) {
      actions.push({
        label: bundle.GetStringFromName("flock.flickr.actions." + i),
        class: i,
        spec: this._WebDetective.getString("flickr", i, "")
                  .replace("%accountid%", this.coopObj.accountId)
                  .replace("%friendid%", c_friend.accountId)
      });
    }
  }
  var nsJSON = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
  return nsJSON.encode(actions);
};

// DEFAULT: long getFriendCount();
// DEFAULT: nsISimpleEnumerator enumerateFriends();
// DEFAULT: AString getInviteFriendsURL();

// void shareWithFriend(in AString aFriendURN, ...
flickrAccount.prototype.shareWithFriend =
function flickrAccount_shareWithFriend(aFriendURN, aTransferable) {
  this._logger.info(".shareWithFriend('" + aFriendURN + "')");

  var sharingAction = "";
  var c_friend = this._coop.get(aFriendURN);
  if (c_friend) {
    // Flavors we want to support, in order of preference
    var flavors = ["text/x-flock-media",
                   "text/x-moz-url",
                   "text/unicode",
                   "text/html"];

    var message = Cc[FLOCK_RDNDS_CONTRACTID]
                  .getService(Ci.flockIRichDNDService)
                  .getMessageFromTransferable(aTransferable,
                                              flavors.length,
                                              flavors);
    if (message.body) {
      this._composeMessage(aFriendURN, message.subject, message.body, true);
    }
  }
};

// AString getProfileURLForFriend(in AString aFriendURN);
flickrAccount.prototype.getProfileURLForFriend =
function flickrAccount_getProfileURLForFriend(aFriendURN) {
  this._logger.info(".getProfileURLForFriend('" + aFriendURN + "')");

  var url = "";
  var c_friend = this._coop.get(aFriendURN);
  if (c_friend) {
    url = this._WebDetective.getString("flickr", "friendprofile", "")
                           .replace("%accountid%", c_friend.accountId);
  }
  return url;
};

// void postLink(in nsITransferable aTransferable);
flickrAccount.prototype.postLink =
function flickrAccount_postLink(aTransferable) {
  // Not needed for Flickr.
};

// void markAllMeNotificationsSeen(in AString aType);
flickrAccount.prototype.markAllMeNotificationsSeen =
function flickrAccount_markAllMeNotificationsSeen(aType) {
  this._logger.debug(".markAllMeNotificationsSeen('" + aType + "')");
  switch (aType) {
    case "meMessages":
      this.coopObj.accountMessages = 0;
      break;
    case "meCommentActivity":
      var comments = this.getCustomParam("flickr_comments");
      if (comments > 0) {
        this.coopObj.lastUpdateDate = new Date();
        this.setCustomParam("flickr_comments", 0);
        this.setCustomParam("flickr_comments_timespan", 0);
      }
      break;

    default:
      break;
  }
};

// void shareFlock(in AString aFriendURN);
flickrAccount.prototype.shareFlock =
function flickrAccount_shareFlock(aFriendURN) {
  this._logger.info(".shareFlock('" + aFriendURN + "')");
  var sbs = Cc["@mozilla.org/intl/stringbundle;1"]
              .getService(Ci.nsIStringBundleService);
  var bundle = sbs.createBundle(FLICKR_STRING_BUNDLE);
  var body = bundle.GetStringFromName("flock.flickr.friendShareFlock.message");
  var subj = bundle.GetStringFromName("flock.flickr.friendShareFlock.subject");
  this._composeMessage(aFriendURN, subj, body, false);
};

// void friendAction(in AString aAction, in AString aFriendURN, in AString..
flickrAccount.prototype.friendAction =
function flickrAccount_friendAction(aAction, aFriendURN, aActivityURN) {
  this._logger.error(".friendAction(): NS_ERROR_NOT_IMPLEMENTED");
  throw CR.NS_ERROR_NOT_IMPLEMENTED;
};

/*************************************************************************
 * flockIMediaAccount implementation
 *************************************************************************/

// NO METHODS OR PROPERTIES

/*************************************************************************
 * flockIMediaUploadAccount implementation
 *************************************************************************/

// NO METHODS OR PROPERTIES

/*************************************************************************
 * Private Functions
 *************************************************************************/

flickrAccount.prototype._composeMessage =
function flickrAccount__composeMessage(aFriendURN,
                                       aSubject,
                                       aBody,
                                       aAddBreadCrumb)
{
  var body = aBody;
  var subject = aSubject;
  var c_friend = this._coop.get(aFriendURN);
  var url = this._WebDetective.getString("flickr", "flickrmail_URL", "")
                              .replace("%friendid%", c_friend.accountId);
  var win = Cc["@mozilla.org/appshell/window-mediator;1"]
            .getService(Ci.nsIWindowMediator)
            .getMostRecentWindow("navigator:browser");
  if (win) {
    var browser = win.getBrowser();
    var newTab = browser.loadOneTab(url, null, null, null, false, false);
    if (newTab && newTab.linkedBrowser && newTab.linkedBrowser.docShell) {
      var obs = Cc["@mozilla.org/observer-service;1"]
                .getService(Ci.nsIObserverService);
      var inst = this;
      var observer = {
        observe: function openSendMessageTabForFill_observer(aContent,
                                                             aTopic,
                                                             aContextUrl)
        {
          var contentWindow = newTab.linkedBrowser.docShell
                                    .QueryInterface(Ci.nsIInterfaceRequestor)
                                    .getInterface(Ci.nsIDOMWindow);
          function insertContent(aWebDicString, aMessage) {
            var xpathquery = inst._WebDetective
                                 .getString("flickr", aWebDicString, "");
            var doc = contentWindow.document;
            var formItems = doc.evaluate(xpathquery, doc, null,
                                         Ci.nsIDOMXPathResult.ANY_TYPE, null);
            if (formItems) {
              var formItem = formItems.iterateNext();
              if (formItem.hasAttribute("value")) {
                formItem.setAttribute("value", aMessage);
              } else {
                var textNode = doc.createTextNode(aMessage);
                formItem.appendChild(textNode);
                inst._logger.info("aMessage " + aMessage);
              }
            }
          }
          if (contentWindow == aContent) {
            obs.removeObserver(this, "EndDocumentLoad");
            insertContent("flickrmail_subjectXPath", subject);
            if (aAddBreadCrumb) {
              // Add breadcrumb to message body
              var breadcrumb = Cc[FLOCK_RDNDS_CONTRACTID]
                               .getService(Ci.flockIRichDNDService)
                               .getBreadcrumb("plain");
              if (breadcrumb) {
                body += breadcrumb;
              }
            }
            insertContent("flickrmail_bodyXPath", body);
          }
        }
      };
    }
    obs.addObserver(observer, "EndDocumentLoad", false);
  }
};

/*************************************************************************
 * XPCOM Support - Module Construction
 *************************************************************************/

// Create array of components.
var componentsArray = [flickrService];

// Generate a module for XPCOM to find.
var NSGetModule = FlockXPCOMUtils.generateNSGetModule(MODULE_NAME,
                                                      componentsArray);
