// BEGIN FLOCK GPL
//
// Copyright Flock Inc. 2005-2008
// 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/FlockSvcUtils.jsm");
CU.import("resource:///modules/FlockCryptoHash.jsm");

const MODULE_NAME = "Photobucket";

const CLASS_NAME = "Flock Photobucket Service";
const CLASS_SHORT_NAME = "photobucket";
const CLASS_TITLE = "Photobucket";
const CLASS_ID = Components.ID("{d3b147b0-a321-11da-a746-0800200c9a66}");
const CONTRACT_ID = "@flock.com/?photo-api-photobucket;1";

const SERVICE_ENABLED_PREF = "flock.service.photobucket.enabled";

const PHOTOBUCKET_FAVICON = "chrome://flock/content/services/photobucket/favicon.png";
const PHOTOBUCKET_URL = "http://www.photobucket.com/";

const FLOCK_PHOTO_ALBUM_CONTRACTID = "@flock.com/photo-album;1";
const FLOCK_PHOTOPERSON_CONTRACTID = "@flock.com/photo-person;1";
const FLOCK_ERROR_CONTRACTID = "@flock.com/error;1";

const PHOTOBUCKET_API_VERSION = "1.0";
const PHOTOBUCKET_TOKEN_PREF = "flock.photo.photobucket.token";

const PHOTOBUCKET_PK = "e57335b0847da8b1105b6c8bc27d217a";
const PHOTOBUCKET_SCID = "149825788";
const PHOTOBUCKET_PROPERTIES = "chrome://flock/locale/services/photobucket.properties";

// Refresh the session token every 6 hours
const PHOTOBUCKET_SESSION_REFRESH_INTERVAL = 21600000;

// String defaults... may be updated at runtime through Web Detective.
var gStrings = {
  "domains": "photobucket.com",
  "userloginURL": "http://photobucket.com/login.php",
  "userprofileURL": "http://photobucket.com/images/search/%accountid%",
  "serviceloginURL": "http://photobucket.com/svc/servicelogin.php",
  "apiURL": "http://photobucket.com/svc/api.php"
};

/**************************************************************************
 * General Helper Functions
 **************************************************************************/

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

function loadLibraryFromSpec(aSpec) {
  var loader = CC["@mozilla.org/moz/jssubscript-loader;1"]
               .getService(CI.mozIJSSubScriptLoader);
  loader.loadSubScript(aSpec);
}

loadLibraryFromSpec("chrome://flock/content/common/flocksafe.js");
loadLibraryFromSpec("chrome://browser/content/flock/photo/photoAPI.js");

var flockMediaItemFormatter = {
  canBuildTooltip: false,
  canBuildHTML: false,
  canBuildLargeHTML: false,
  canBuildBBCode: true,
  canBuildMiniPage: true,
  buildBBCode: function fmif_buildBBCode(aMediaItem) {
    var bbCode = "";
    if (aMediaItem.is_video) {
      bbCode = "[photob]" + aMediaItem.largeSizePhoto + "[photob]";
    } else {
      bbCode = "[url=" + aMediaItem.webPageUrl
             + "][img]" + aMediaItem.largeSizePhoto
             + "[/img][/url]";
    }
    return bbCode;
  },
  buildMiniPage: function fmif_buildMiniPage(aMediaItem) {
    var html = '<html><head><title>'
             + aMediaItem.title
             + ' (' + aMediaItem.username + ')</title></head>'
             + '<body><center><object width="425" height="350">'
             + '<param name="movie" value="'
             + aMediaItem.largeSizePhoto
             + '"/>'
             + '<embed src="'
             + aMediaItem.largeSizePhoto
             + '&autoplay=1" type="application/x-shockwave-flash"'
             + ' width="425" height="350"/></object></center></body></html>';
    return html;
  }
};


/**************************************************************************
 * Photobucket API
 **************************************************************************/

var gPhotobucketAPI;

function PhotobucketAPI(aSvc) {
  FlockSvcUtils.getLogger(this);
  this._logger.init("photobucketAPI");
  this._logger.info("Created Photobucket API Object");

  FlockSvcUtils.getACUtils(this);
  FlockSvcUtils.getCoop(this);

  this._svc = aSvc;
  this._WebDetective = this._acUtils.useWebDetective("photobucket.xml");
  this._apiUrl = gStrings["apiURL"];
  this._authUser = null;
  this._sessionRefreshTimer = null;
}

PhotobucketAPI.prototype.isLoggedIn =
function pbApi_isLoggedIn() {
  return (this._authUser && this._authUser.sessionkey) ? true : false;
};

PhotobucketAPI.prototype.getToken =
function pbApi_getToken(aListener) {
  var inst = this;
  var tokenListener = {
    onResult: function pbApi_getToken_onResult(aXML) {
      try {
        var resp = aXML.getElementsByTagName("response");
        if (resp[0].getAttribute("stat") == "ok") {
          //rval = resp[0].firstChild.nodeValue;
          aListener.onResult(aXML);
        } else {
          aListener.onError(null);  //TODO - figure out errors
          //rval = -1;
        }
      } catch (ex) {
        inst._logger.error(ex);
      }
    },

    onError: function pbApi_getToken_onError(aError) {
      inst._logger.error(aError.errorString);
      aListener.onError(aError);
    }
  };
  var params = {};
  this.call(tokenListener, "getservicetoken", params2Dictionary(params));
};

PhotobucketAPI.prototype.call =
function pbApi_call(aListener, aMethod, aDictionary, aAuth)
{
  var inst = this;
  var params = this.dictionary2Params(aDictionary);
  var url = this.buildUrl(this._apiUrl, aMethod, params, aAuth);
  this._logger.debug(url + " < call");
  this.req = CC["@mozilla.org/xmlextras/xmlhttprequest;1"]
             .createInstance(CI.nsIXMLHttpRequest)
             .QueryInterface(CI.nsIJSXMLHttpRequest);

  this.req.open("GET", url, true);
  var req = this.req;
  this.req.onreadystatechange = function pbApi_call_onreadystatechange(aEvt) {
    if (req.readyState == 4) {
      try {
        inst._logger.debug("doCall[onreadystatechange] req.status = "
                           + req.status);
        if (req.status/100 == 2) {
          // XXX .DUMP inst._logger.debug(req.responseText);
          inst._logger.debug("response text length: " + req.responseText.length);
          var rsp = req.responseXML.getElementsByTagName("response")[0];
          var stat = rsp.getAttribute("stat");
          if (stat != "ok") {
            aListener.onError(inst.getError("SERVICE_ERROR",
                                            req.responseXML,
                                            null));
          } else {
            aListener.onResult(req.responseXML);
          }
        } else {
          aListener.onError(inst.getError("HTTP_ERROR", null, req.status));
        }
      } catch (ex) {
        inst._logger.error(ex);
        // We need to convert to a string because that's what getError expects
        var errorCode = String(CI.flockIError.XML_HTTP_REQUEST_ERROR);
        aListener.onError(inst.getError("HTTP_ERROR", null, errorCode));
      }
    }
  };
  this.req.send(null);
};

PhotobucketAPI.prototype.dictionary2Params =
function pbApi_dictionary2Params(aDictionary) {
  var params = {};
  var obj = {};
  var count = {};
  var keys = aDictionary.getKeys(count, obj);
  for (var i = 0; i < keys.length; i++) {
    var supports = aDictionary.getValue(keys[i]);
    var supportsString = supports.QueryInterface(CI.nsISupportsString);
    var val = supportsString.toString();
    params[keys[i]] = val;
  }
  return params;
};

PhotobucketAPI.prototype.authenticatedCall =
function pbApi_authenticatedCall(aListener, aMethod, aDictionary) {
  this._logger.debug("authenticatedCall " + aMethod);
  if (!this.isLoggedIn()) {
    this._logger.debug("not logged in, so trying unauth call for " + aMethod);
    return this.call(aListener, aMethod, aDictionary);
  }
  this._logger.debug("+++++" + this._authUser.sessionkey);
  return this.call(aListener, aMethod, aDictionary, true);
};

PhotobucketAPI.prototype.doLogin =
function pbApi_doLogin(aAccountURN, aListener) {
  this._logger.debug(".doLogin('" + aAccountURN + "')");
  var inst = this;
  var tokenListener = {
    onResult: function pbApi_doLogin_onResult(aXML) {
      inst._logger.debug(".doLogin('"
                         + aAccountURN
                         + "'): tokenListener: onResult()");
      // Time to authenticate...
      var params = {
        service_token: aXML.getElementsByTagName("service_token")[0]
                           .firstChild
                           .nodeValue
      };
      // persist the token
      var acctCoopObj = inst._coop.get(aAccountURN);
      acctCoopObj.authToken = params.service_token;

      // listener for the upcoming "getsession" api call
      var getSessListener = {
        onResult: function pbApi_doLogin_gsl_onResult(aXML) {
          inst._logger.debug(".doLogin('"
                             + aAccountURN
                             + "'): tokenListener:"
                             + "getSessListener: onResult()");
          var username = aXML.getElementsByTagName("username")[0]
                             .firstChild
                             .nodeValue;
          var sessionkey = aXML.getElementsByTagName("session_key")[0]
                               .firstChild
                               .nodeValue;
          inst._apiUrl = aXML.getElementsByTagName("url")[0]
                             .firstChild
                             .nodeValue;
          inst._authUser.id = username;
          inst._authUser.username = username;
          inst._authUser.fullname = username;
          inst._authUser.sessionkey = sessionkey;
          inst._acUtils.ensureOnlyAuthenticatedAccount(aAccountURN);

          // Notify that api auth has changed
          inst._svc._obsSvc
              .notifyObservers(null, "apiAuthChange", inst._svc.shortName);

          if (!inst._sessionRefreshTimer) {
            inst.startSessionRefreshTimer();
          }
          aListener.onSuccess(null, "");
        },

        onError: function pbApi_doLogin_gsl_onError(aErr) {
          inst._logger.error(".doLogin('"
                             + aAccountURN
                             + "'): tokenListener: getSessListener: onError()");
          inst._logger.error(">>>>>> " + aErr.errorString);
          aListener.onError(aErr);
        }
      };

      // If we have the user's un/pw, then we can do this silently...
      var pw = inst._acUtils.getPassword("urn:photobucket:service:"
                                        + acctCoopObj.accountId);
      if (pw) {
        inst._logger.debug(".doLogin('"
                           + aAccountURN
                           + "'): tokenListener: onResult(): found password");

        var postBody = "action=login"
                     + "&service_token=" + params.service_token
                     + "&sig=" + inst.buildSig("login", params)
                     + "&scid=" + PHOTOBUCKET_SCID
                     + "&method=login"
                     + "&callback_verify="
                     + "&username=" + pw.user
                     + "&password=" + pw.password
                     + "&login=Login";
        var hr = CC["@mozilla.org/xmlextras/xmlhttprequest;1"]
                 .createInstance(CI.nsIXMLHttpRequest)
                 .QueryInterface(CI.nsIJSXMLHttpRequest);

        var onReadyStateFunc =
        function pbApi_doLogin_onReadyStateFunc(eEvt) {
          if (hr.readyState == 4) {
            inst._logger.debug(".doLogin('"
                               + aAccountURN
                               + "'): tokenListener:"
                               + "onResult(): hr.onreadystatechange()");
            var results = CC["@mozilla.org/hash-property-bag;1"]
                          .createInstance(CI.nsIWritablePropertyBag2);
            if (inst._WebDetective.detectNoDOM("photobucket",
                                               "apiauth",
                                               "",
                                               hr.responseText,
                                               results))
            {
              var hr2 = CC["@mozilla.org/xmlextras/xmlhttprequest;1"]
                        .createInstance(CI.nsIXMLHttpRequest)
                        .QueryInterface(CI.nsIJSXMLHttpRequest);

              hr2.onreadystatechange =
              function pbApi_doLogin_onreadystatechange(eEvt2) {
                if (hr2.readyState == 4) {
                  inst.call(getSessListener,
                            "getsession",
                            params2Dictionary(params));
                }
              };

              var postBody2 = "action=complete"
                            + "&authorized=yes"
                            + "&service_token=" + params.service_token
                            + "&sig=" + inst.buildSig("login", params)
                            + "&scid=" + PHOTOBUCKET_SCID
                            + "&method=login"
                            + "&callback_verify="
                            + "&user_id="
                            + results.getPropertyAsAString("user_id");

              hr2.backgroundRequest = true;
              hr2.open("POST", gStrings["serviceloginURL"],true);
              hr2.setRequestHeader("Content-Type",
                                   "application/x-www-form-urlencoded");
              hr2.send(postBody2);
            } else {
              inst.call(getSessListener,
                        "getsession",
                        params2Dictionary(params));
            }
          }
        };

        hr.onreadystatechange = onReadyStateFunc;
        hr.backgroundRequest = true;
        hr.open("POST", gStrings["serviceloginURL"],true);
        hr.setRequestHeader("Content-Type",
                            "application/x-www-form-urlencoded");
        hr.send(postBody);

      } else {
        inst._logger.debug(".doLogin('"
                           + aAccountURN
                           + "'): tokenListener: onResult(): NO password!");

        // No password stored, so we have to prompt the user
        var windowManager = CC["@mozilla.org/appshell/window-mediator;1"]
                            .getService(CI.nsIWindowMediator);
        var topWindow = windowManager.getMostRecentWindow(null);

        var loginUrl = inst.buildUrl(gStrings["serviceloginURL"],
                                     "login",
                                     params);
        var url = "chrome://browser/content/flock/photo/photoLoginWindow.xul?"
                + "url=" + escape(loginUrl)
                + "&finalString=" + escape("You have granted");
        var options = "chrome,"
                    + "close,"
                    + "titlebar,"
                    + "resizable=yes,"
                    + "toolbar,"
                    + "dialog=no,"
                    + "scrollbars=yes,"
                    + "modal,"
                    + "centerscreen";
        topWindow.open(url, "_blank", options);
        // we are waiting till window closes

        inst.call(getSessListener, "getsession", params2Dictionary(params));
      }
    },

    onError: function pbApi_doLogin_onError(aErr) {
      inst._logger.error(".doLogin('"
                         + aAccountURN
                         + "'): tokenListener: onError()");
      aListener.onError(aErr);
    }
  };

  if (!this._authUser) {
    this._authUser = {};
  }
  this.getToken(tokenListener);
};

PhotobucketAPI.prototype.startSessionRefreshTimer =
function pbApi_startSessionRefreshTimer() {
  // Keep the API authentication alive at the designated interval.
  this._sessionRefreshTimer = CC["@mozilla.org/timer;1"]
                              .createInstance(CI.nsITimer);
  this._sessionRefreshTimer.initWithCallback(this._svc,
                                             PHOTOBUCKET_SESSION_REFRESH_INTERVAL,
                                             CI.nsITimer.TYPE_REPEATING_PRECISE);
};

PhotobucketAPI.prototype.getError =
function pbApi_getError(aErrorType, aXML, aHTTPErrorCode) {
  var errorNode;
  var code;
  var serverErrorMessage;
  var error = CC[FLOCK_ERROR_CONTRACTID]
              .createInstance(CI.flockIError);

  this._logger.debug("getError aErrorType="
                     + aErrorType
                     + "; aHTTPErrorCode="
                     + aHTTPErrorCode);

  if (aErrorType == "HTTP_ERROR") {
    error.errorCode = aHTTPErrorCode;
    return error;
  } else if (aErrorType == "SERVICE_ERROR") {
    try {
      errorNode = aXML.getElementsByTagName("error")[0];
      code = errorNode.getAttribute("code");
      serverErrorMessage = errorNode.getAttribute("msg");
      error.serviceErrorCode = code;
      error.serviceErrorString = serverErrorMessage;
      this._logger.debug("Found error code = "
                         + code
                         + " and message = '"
                         + serverErrorMessage
                         + "'");
    } catch (ex) {
      code = "-1";
    }

    if (!code) {
      code = "-1";
    }

    switch (code) {
      case "-1":
        error.errorCode = error.PHOTOSERVICE_INVALID_RESPONSE;
        break;
      case "007":
      case "008":
        error.errorCode = error.PHOTOSERVICE_LOGIN_FAILED;
        break;
      case "012":
        error.errorCode = error.PHOTOSERVICE_SESSION_KEY_EXPIRED;
        break;
      case "101":
        error.errorCode = error.PHOTOSERVICE_EMPTY_ALBUMNAME;
        break;
      case "102":
        error.errorCode = error.PHOTOSERVICE_INVALID_USER;
        break;
      case "103":
        error.errorCode = error.PHOTOSERVICE_INVALID_SEARCH_QUERY;
        break;
      case "104":
        error.errorCode = error.PHOTOSERVICE_INVALID_SEARCH_QUERY;
        break;
      case "105":
        error.errorCode = error.PHOTOSERVICE_INVALID_UPLOAD_FILE;
        break;
      case "106":
        error.errorCode = error.PHOTOSERVICE_INVALID_SEARCH_QUERY;
        break;
      case "107":
        error.errorCode = error.PHOTOSERVICE_INVALID_ALBUM;
        break;
      case "108":
        error.errorCode = error.PHOTOSERVICE_PRIVATE_ALBUM;
        break;
      case "109":
        error.errorCode = error.PHOTOSERVICE_PHOTOS_IN_ALBUM_LIMIT_REACHED;
        break;
      case "110":
        error.errorCode = error.PHOTOSERVICE_NO_FILE_UPLOADED;
        break;
      case "111":
        error.errorCode = error.PHOTOSERVICE_ALBUM_IS_OVER_SIZE_LIMIT;
        break;
      case "112":
        error.errorCode = error.PHOTOSERVICE_INVALID_UPLOAD_FILE;
        break;
      case "113":
        error.errorCode = error.PHOTOSERVICE_DUPLICATE_FILENAME;
        break;
      case "115":
        error.errorCode = error.PHOTOSERVICE_INVALID_ALBUMNAME;
        break;
      case "116":
        error.errorCode = error.PHOTOSERVICE_DUPLICATE_ALBUMNAME;
        break;
      case "314":
        error.errorCode = error.PHOTOSERVICE_FILE_IS_OVER_SIZE_LIMIT;
        break;
      default:
        error.errorCode = error.PHOTOSERVICE_UNKNOWN_ERROR;
        if (serverErrorMessage && serverErrorMessage.length) {
          error.serviceErrorString = serverErrorMessage;
        }
        break;
    }
    return error;
  }
  return null;
};

PhotobucketAPI.prototype.buildUrl =
function pbApi_buildUrl(aUrl, aMethod, aParams, aAuth) {
  var qs = "";
  var strConcat = "";
  for (var p in aParams) {
    qs += "&" + escape(p) + "=" + escape(aParams[p]);
    strConcat += escape(aParams[p]);
  }
  var rval = aUrl
           + "?method=" + aMethod
           + "&version=" + PHOTOBUCKET_API_VERSION;
  if (aAuth) {
    rval += "&session_key=" + this._authUser.sessionkey;
  }
  rval += "&scid=" + PHOTOBUCKET_SCID
       + qs
       + "&sig=" + this.buildSig(aMethod, aParams, aAuth);
  this._logger.debug("this is your buildUrl: " + rval + "\n");
  return rval;
};

PhotobucketAPI.prototype.buildSig =
function pbApi_buildSig(aMethod, aParams, aAuth) {
  var qs = "";
  var strConcat = "";
  for (var p in aParams) {
    qs += "&" + escape(p) + "=" + escape(aParams[p]);
    if (p == "service_token") {
      strConcat += escape(aParams[p]);
    }
  }

  this._logger.debug(strConcat + "<<<<<<<<strconcat\n");
  var preHash = aMethod
                + PHOTOBUCKET_PK
                + PHOTOBUCKET_SCID
                + strConcat
                + (aAuth ? this._authUser.sessionkey : "");
  this._logger.debug("going to sig::: " + preHash);

  var converter = CC["@mozilla.org/intl/scriptableunicodeconverter"]
                  .createInstance(CI.nsIScriptableUnicodeConverter);
  converter.charset = "UTF-8";

  var inputStream = converter.convertToInputStream(preHash);
  return FlockCryptoHash.md5Stream(inputStream);
};


/**************************************************************************
 * Component: Flock Photobucket Account
 **************************************************************************/

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

  if (!gPhotobucketAPI) {
    this._logger.error("ERROR; why did the Account get created first?");
  }
  this._api = gPhotobucketAPI;

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

/**************************************************************************
 * Flock Photobucket Account: XPCOM Component Creation
 **************************************************************************/

// Use genericComponent() for the goodness it provides (QI, nsIClassInfo, etc),
// but do NOT add PhotobucketAccount to the list of constructors passed
// to FlockXPCOMUtils.genericModule().
PhotobucketAccount.prototype = new FlockXPCOMUtils.genericComponent(
  CLASS_NAME + " Account",
  "",
  "",
  PhotobucketAccount,
  0,
  [
    CI.flockIWebServiceAccount,
    CI.flockIMediaWebServiceAccount,
    CI.flockIMediaUploadAccount
  ]
);

/**************************************************************************
 * Flock Photobucket Account: flockIWebServiceAccount Implementation
 **************************************************************************/

// readonly attribute AString urn;
// This gets set in the call to flockIWebService::getAccount()
PhotobucketAccount.prototype.urn = null;

// void activate(in flockIListener aListener);
PhotobucketAccount.prototype.activate =
function pbAcct_activate(aListener) {
  this._logger.debug("{flockIWebServiceAccount}.activate() urn=" + this.urn);
  var acctCoopObj = this._coop.get(this.urn);
  var inst = this;
  var listener = {
    onSuccess: function pbAcct_activate_onSuccess() {
      inst._logger.debug("activate() listener onSuccess()");
      acctCoopObj.isPollable = true;
      aListener.onSuccess(inst, "accountAuthorized");
      try {
        var authUser = inst._api._authUser;
        acctCoopObj.name = authUser.fullname;
        var photostreamURN = "urn:flock:stream:photo:photobucket:"
                           + acctCoopObj.accountId;
        var photostream = inst._coop.get(photostreamURN);
        photostream.name = authUser.fullname + " Photostream";
      } catch (ex) {
        inst._logger.error(ex + "::" + ex.lineNumber);
      }
    },

    onError: function pbAcct_activate_onError(aError) {
      inst._logger.debug("activate() listener onError()");
      try {
        var authUser = inst._getAuthUser();
        var acctCoopObj = inst._coop.get(inst.urn);
        acctCoopObj.isAuthenticated = false;
      } catch (ex) {
        // Don't really need to do anything, we were just trying to be nice
        // anyway...
      }
    }
  };
  this.login(listener);
  // Pre-emptively setting isAuthenticated to TRUE in order to avoid further
  // auth attemps while this one is completing.
  acctCoopObj.isAuthenticated = true;
};

// void deactivate(in flockIListener aListener);
PhotobucketAccount.prototype.deactivate =
function pbAcct_deactivate() {
  this._logger.debug("{flockIWebServiceAccount}.deactivate()");
  var acctCoopObj = this._coop.get(this.urn);
  acctCoopObj.isPollable = false;
  acctCoopObj.isAuthenticated = false;
};

// void login(in flockIListener aListener);
PhotobucketAccount.prototype.login =
function pbAcct_login(aListener) {
  this._logger.debug("{flockIWebServiceAccount}.login() urn=" + this.urn);

  var timer = CC["@mozilla.org/timer;1"].createInstance(CI.nsITimer);
  var inst = this;
  var timerFunc = {
    notify: function pbAcct_login_notify(aTimer) {
      inst._api.doLogin(inst.urn, aListener);
    }
  };
  //re-check token
  timer.initWithCallback(timerFunc, 0, CI.nsITimer.TYPE_ONE_SHOT); 
};

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

// void keep();
PhotobucketAccount.prototype.keep =
function pbAcct_keep() {
  var c_acct = this._coop.get(this.urn);
  c_acct.isTransient = false;
  var key = this._api._svc.urn + ":" + c_acct.accountId;
  this._acUtils.makeTempPasswordPermanent(key);
};

/**************************************************************************
 * Flock Photobucket Account: flockIMediaWebServiceAccount Implementation
 **************************************************************************/

/* No methods or properties on this interface! */

/**************************************************************************
 * Flock Photobucket Account: flockIMediaUploadAccount Implementation
 **************************************************************************/

/* No methods or properties on this interface! */


/**************************************************************************
 * Component: Flock Photobucket Service
 **************************************************************************/

// Constructor.
function PhotobucketService() {

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

  FlockSvcUtils.flockIWebService.addDefaultMethod(this, "getAccount");
  FlockSvcUtils.flockIWebService.addDefaultMethod(this, "isHidden");
  FlockSvcUtils.flockIWebService.addDefaultMethod(this, "getAuthenticatedAccount");
  FlockSvcUtils.flockIWebService.addDefaultMethod(this, "getAccounts");
  FlockSvcUtils.flockIWebService.addDefaultMethod(this, "getStringBundle");

  FlockSvcUtils.flockIManageableWebService
               .addDefaultMethod(this, "docRepresentsSuccessfulLogin");
  FlockSvcUtils.flockIManageableWebService
               .addDefaultMethod(this, "ownsDocument");
  FlockSvcUtils.flockIManageableWebService
               .addDefaultMethod(this, "ownsLoginForm");
  FlockSvcUtils.flockIManageableWebService
               .addDefaultMethod(this, "getSessionValue");

  FlockSvcUtils.flockIMediaWebService
               .addDefaultMethod(this, "getChannel");
  FlockSvcUtils.flockIMediaWebService
               .addDefaultMethod(this, "enumerateChannels");

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

/**************************************************************************
 * Flock Photobucket Service: XPCOM Component Creation
 **************************************************************************/

PhotobucketService.prototype = new FlockXPCOMUtils.genericComponent(
  CLASS_NAME,
  CLASS_ID,
  CONTRACT_ID,
  PhotobucketService,
  CI.nsIClassInfo.SINGLETON,
  [
    CI.nsIObserver,
    CI.flockIWebService,
    CI.flockIManageableWebService,
    CI.flockIMediaWebService,
    CI.flockIMediaUploadWebService,
    CI.flockIMediaEmbedWebService,
    CI.flockIAuthenticateNewAccount,
    CI.flockIPollingService
  ]
);

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

/**************************************************************************
 * Flock Photobucket Service: Private Data and Functions
 **************************************************************************/

PhotobucketService.prototype._init =
function pbSvc__init() {
  FlockSvcUtils.getLogger(this).info(".init()");

  // Determine whether this service has been disabled, and unregister if so.
  var prefService = CC["@mozilla.org/preferences-service;1"]
                    .getService(CI.nsIPrefBranch);
  if (prefService.getPrefType(SERVICE_ENABLED_PREF) &&
     !prefService.getBoolPref(SERVICE_ENABLED_PREF))
  {
    this._logger.info("Pref " + SERVICE_ENABLED_PREF
                              + " set to FALSE... not initializing.");
    this.deleteCategories();
    return;
  }

  FlockSvcUtils.getACUtils(this);
  FlockSvcUtils.getCoop(this);
  FlockSvcUtils.getWD(this);
  
  // Initialize member that specifies path for localized string bundle
  this._stringBundlePath = PHOTOBUCKET_PROPERTIES;

  this._accountClassCtor = PhotobucketAccount;

  // Update the strings from Web Detective.
  for (var s in gStrings) {
    gStrings[s] = this._WebDetective.getString("photobucket", s, gStrings[s]);
  }

  // This property is part of our public interface.
  this.domains = gStrings["domains"];

  this._photoUploader = null;
  if (!gPhotobucketAPI) {
    // Note: Constructor expects gStrings to be up-to-date.
    gPhotobucketAPI = new PhotobucketAPI(this);
  }
  this._api = gPhotobucketAPI;

  this._obsSvc = CC["@mozilla.org/observer-service;1"]
                 .getService(CI.nsIObserverService);

  var bundle = this.getStringBundle();

  this._channels = {
    "special:recent": {
      title: bundle.GetStringFromName("flock.photobucket.title.recent"),
      supportsSearch: false
    }
  };

  this._c_svc = new this._coop.Service(
    this.urn,
    {
      name: "photobucket",
      desc: "The Photobucket Service",
      contactLabel: "People"
    }
  );
  this._c_svc.serviceId = CONTRACT_ID;
  this._c_svc.domains = gStrings["domains"];
  this._c_svc.loginURL = gStrings["userloginURL"];

  // Update auth states
  try {
    if (this._WebDetective.detectCookies("photobucket", "loggedout", null)) {
      this._acUtils.markAllAccountsAsLoggedOut(CONTRACT_ID);
      this._deauthenticateAPI();
    } else {
      this._checkPersistedToken();
    }
  } catch (ex) {
    this._logger.error("ERROR updating auth states for Photobucket: " + ex);
  }

  // On browser restart if a Photobucket coop account is still marked as
  // authenticated we also need to reauth the api. We do this by calling
  // login() on the xpcom account obj. However, we have to wait while
  // the Photobucket service finishes constructing itself.
  var inst = this;
  var reloginCb = {
    notify: function reloginCallback_notify(aTimer) {
      inst._logger.info("reloginCallback.notify()")
      inst._reloginTimer = null;
      var account = inst.getAuthenticatedAccount();
      if (account) {
        var reloginListener = {
          onSuccess: function reloginListener_onSuccess(aSubject, aTopic) {
            inst._logger.info(".init(): reloginListener: onSuccess()");
          },
          onError: function reloginListener_onError(aError) {
            inst._logger.info(".init(): reloginListener: onError()");
            inst._coop.get(account.urn).isAuthenticated = false;
          }
        };
        account.login(reloginListener);
      }
    }
  };
  this._reloginTimer = CC["@mozilla.org/timer;1"].createInstance(CI.nsITimer);
  this._reloginTimer.initWithCallback(reloginCb, 100,
                                      CI.nsITimer.TYPE_ONE_SHOT);
};

PhotobucketService.prototype._checkPersistedToken =
function pbSvc__checkPersistedToken() {
  var acctURN = this._acUtils.getFirstAuthenticatedAccountForService(CONTRACT_ID);
  if (!acctURN) {
    return;
  }
  var token = this._coop.get(acctURN).authToken;
  if (token) {
    this._logger.debug("Is session still valid for token: " + token);
    // try to get the session from the persisted token
    var inst = this;
    var myListener = {
      onResult: function pbSvc__checkPersistedToken_onResult(aXML) {
        try {
          var resp = aXML.getElementsByTagName("response");
          if (resp[0].getAttribute("stat") == "ok") {
            var username = aXML.getElementsByTagName("username")[0]
                               .firstChild.nodeValue;
            var sessionkey = aXML.getElementsByTagName("session_key")[0]
                                 .firstChild.nodeValue;
            inst._api._apiUrl = aXML.getElementsByTagName("url")[0]
                                    .firstChild.nodeValue;
            inst._getAuthUser().id = username;
            inst._getAuthUser().username = username;
            inst._getAuthUser().fullname = username;
            inst._getAuthUser().sessionkey = sessionkey;
            if (!inst._api._sessionRefreshTimer) {
              inst._api.startSessionRefreshTimer();
            }
          } else {
            inst._api._authUser = null;
            inst._photoUploader = null;
            inst._acUtils.markAllAccountsAsLoggedOut(CONTRACT_ID);
          }
        } catch (ex) {
          inst._logger.error(ex);
        }
      },

      onError: function pbSvc__checkPersistedToken_onError(aErr) {
        return false;
      }
    };
    var params = {};
    params.service_token = token;
    inst.call(myListener, "getsession", params2Dictionary(params));
  }
};

PhotobucketService.prototype._deauthenticateAPI =
function pbSvc__deauthenticateAPI() {
  if (this._api.isLoggedIn()) {
    this._api._authUser = null;
    // Notify that api auth has changed
    this._obsSvc.notifyObservers(null, "apiAuthChange", this.shortName);
  }

  this._photoUploader = null;
  if (this._api._sessionRefreshTimer) {
    this._api._sessionRefreshTimer.cancel();
    this._api._sessionRefreshTimer = null;
  }
};

PhotobucketService.prototype._handlePhotosResult =
function pbSvc__handlePhotosResult(aXML) {
  var inst = this;
  function createItem(media, is_video) {
    var uploadDate = media.getAttribute("uploaddate");
    var title = media.getAttribute("name");
    var username = media.getAttribute("username");
    var is_public = media.getAttribute("public");
    var page_url = "";
    var thumb_url = media.getElementsByTagName("thumb")[0].firstChild.nodeValue;
    var small_url = media.getElementsByTagName("url")[0].firstChild.nodeValue;
    // this is a photobucket api issue - need a fix -
    try {
      page_url = media.getElementsByTagName("browseurl")[0].firstChild.nodeValue;
      page_url = page_url.replace("&amp;", "&");
    } catch (ex) {
      page_url = small_url;
    }
    var newMediaItem = CC["@flock.com/photo;1"]
                       .createInstance(CI.flockIMediaItem);
    newMediaItem.init(inst.shortName, flockMediaItemFormatter);
    newMediaItem.webPageUrl = page_url;
    newMediaItem.thumbnail = thumb_url;
    newMediaItem.midSizePhoto = thumb_url;
    newMediaItem.largeSizePhoto = small_url;
    newMediaItem.title = title;
    newMediaItem.username = username;
    newMediaItem.userid = username;
    if (uploadDate) {
      newMediaItem.id = uploadDate;
      newMediaItem.uploadDate = parseInt(uploadDate) * 1000;
    } else {
      newMediaItem.id = username + ":" + title;
    }
    newMediaItem.is_video = is_video;
    newMediaItem.has_miniView = is_video;
    newMediaItem.is_public = (is_public == "1") ? "true" : "false";
    return newMediaItem;
  }

  var rval = [];
  try {
    var i;
    var videoList = aXML.getElementsByTagName("video");
    for (i = 0; i < videoList.length; i++) {
      rval.push(createItem(videoList[i], true));
    }

    var photoList = aXML.getElementsByTagName("photo");
    for (i = 0; i < photoList.length; i++) {
      rval.push(createItem(photoList[i], false));
    }
  } catch (ex) {
    inst._logger.error(ex);
  }

  /*
   * do not sort anymore since PB no longer passes back the uploadDate.
   * instead we just present the photos in the order PB gives them to
   * us (upload order)
   * see https://bugzilla.flock.com/show_bug.cgi?id=9084

  // sort the photos and videos together by date
  // the follow sorts based on ID which we are using the uploadDate to be the id
  // it is ugly -- and probably not as efficient as it could be --- but it
  // is only sorting 20 items at most so it is good enough for now - ja
  rval.sort(function(a,b) {
    return (b.id-a.id);
  });
  */

  return rval;
};

PhotobucketService.prototype._queryChannel =
function pbSvc__queryChannel(aListener,
                             aQueryString,
                             aCount,
                             aPage,
                             aRequestId)
{
  var aQuery = new queryHelper(aQueryString);
  var inst = this;
  var dict = {};

  var myListener = {
    onResult: function pbSvc__queryChannel_onResult(aXML) {
      var rval = inst._handlePhotosResult(aXML);
      var resultsEnum = {
        hasMoreElements: function pbslenum_hasMoreElements() {
          return (rval.length > 0);
        },
        getNext: function pbslenum_getNext() {
          return rval.shift();
        },
        QueryInterface: function pbslenum_QueryInterface(aIID) {
          if (aIID.equals(CI.nsISimpleEnumerator)) {
            return this;
          }
          throw Cr.NS_ERROR_NO_INTERFACE;
        }
      };
      aListener.onSuccess(resultsEnum, aRequestId);
    },

    onError: function pbSvc__queryChannel_onError(aError) {
      aListener.onError(null, aRequestId, aError);
    }
  };

  var params = {
    page: aPage,
    num: aCount
  };

  if (aQuery.getKey("special") == "recent") {
    dict = params2Dictionary(params);
    this._api.call(myListener, "getrecentphotos", dict);
  }

  if (aQuery.hasKey("search")) {
    // don't pass in encoded text for PB
    // if an illegal char is used, we will return no results
    var aText = aQuery.search;
    if (aText && aText.length) {
      params.query = aText.split(".")[0];
      dict = params2Dictionary(params);
      this._api.call(myListener, "search", dict);
    }
  }
};

PhotobucketService.prototype._handleAlbumsResult =
function pbSvc__handleAlbumsResult(aXML) {
  var rval = [];
  var albumList = aXML.getElementsByTagName("photoalbum");
  for (var i = 0; i < albumList.length; i++) {
    var album = albumList[i];
    var title = album.getAttribute("name");

    // the first album node has no name attr, so skip it
    if (!title) {
      continue;
    }

    var username = album.getAttribute("username");
    var id;
    // parse out the prefix /username/
    if (title.match(album.getAttribute("username")) && title != username) {
      id = title.substring(username.length + 1);
      var newAlbum = CC[FLOCK_PHOTO_ALBUM_CONTRACTID]
                     .createInstance(CI.flockIPhotoAlbum);
      newAlbum.id = id;
      newAlbum.title = id;
      rval.push(newAlbum);
    }
  }
  return rval;
};

PhotobucketService.prototype._getAuthUser =
function pbSvc__getAuthUser() {
  return this._api._authUser;
};

PhotobucketService.prototype._getSessionKey =
function pbSvc__getSessionKey(aListener) {
  var acctURN =
    this._acUtils.getFirstAuthenticatedAccountForService(CONTRACT_ID);
  if (!acctURN) {
    aListener.onError(null);
  }
  var token = this._coop.get(acctURN).authToken;
  this._logger.debug(">>>_getSessionKey called with the token " + token);

  var inst = this;
  var mySessionListener = {
    onResult: function pbSvc__getSessionKey_onResult(aXML) {
      var username = aXML.getElementsByTagName("username")[0]
                         .firstChild.nodeValue;
      var sessionkey = aXML.getElementsByTagName("session_key")[0]
                           .firstChild.nodeValue;
      inst._api._apiUrl = aXML.getElementsByTagName("url")[0]
                              .firstChild.nodeValue;
      inst._getAuthUser().id = username;
      inst._getAuthUser().username = username;
      inst._getAuthUser().fullname = username;
      inst._getAuthUser().sessionkey = sessionkey;
      aListener.onAuth();
    },

    onError: function pbSvc__getSessionKey_onError(aErr) {
      inst._logger.debug(">>>>>> " + aErr.errorString);
      aListener.onError(aErr);
    }
  };
  var params = {};
  params.service_token = token;
  this._api.call(mySessionListener, "getsession", params2Dictionary(params));
};

/**************************************************************************
 * Flock Photobucket Service: nsIObserver Implementation
 **************************************************************************/

PhotobucketService.prototype.observe =
function pbSvc_observe(aSubject, aTopic, aData) {
};


/**************************************************************************
 * Flock Photobucket Service: flockIWebService Implementation
 **************************************************************************/

// readonly attribute AString title;
PhotobucketService.prototype.title = CLASS_TITLE;

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

// readonly attribute AString icon;
PhotobucketService.prototype.icon = PHOTOBUCKET_FAVICON;

// readonly attribute AString url;
PhotobucketService.prototype.url = PHOTOBUCKET_URL;

// readonly attribute AString contractId;
// ALMOST duplicated by nsIClassInfo::contractID
// with different case: contractId != contractID
// FIXME: File a bug for this as IDL is case-insensitive.
PhotobucketService.prototype.contractId = CONTRACT_ID;

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

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

// readonly attribute long status;
PhotobucketService.prototype.status = CI.flockIWebService.STATUS_UNKNOWN;

// flockIWebServiceAccount addAccount(in AString aAccountID,
//                                    in boolean aIsTransient,
//                                    in flockIListener aListener);
PhotobucketService.prototype.addAccount =
function pbSvc_addAccount(aAccountID, aIsTransient, aListener,
                          aUsername, aToken)
{
  this._logger.debug(".addAccount('" + aAccountID
                     + "', " + aIsTransient + ", aListener)");

  var name = "";
  var token = "";
  var pollable = false;
  var auth = false;

  if (aUsername) {
    name = aUsername;
    token = aToken;
    pollable = true;
    auth = true;
  } else {
    // Get the password associated with this account
    var pw = this._acUtils.getPassword(this.urn + ":" + aAccountID);
    name = pw.user;
  }

  var accountURN = "urn:flock:photobucket:account:" + aAccountID;
  var acctCoopObj = this._coop.get(accountURN);
  if (!acctCoopObj) {
    acctCoopObj = new this._coop.Account(
      accountURN,
      {
        accountId: aAccountID,
        // This gets changed to the user's full name once logged in
        name: name,
        serviceId: CONTRACT_ID,
        service: this._c_svc,
        URL: this.url,
        favicon: this.icon,
        isTransient: aIsTransient,
        isPollable: pollable,
        authToken: token,
        isAuthenticated: auth
      }
    );
    this._coop.accounts_root.children.add(acctCoopObj);
  }

  // Perhaps we should do a checktoken and if valid go ahead and set
  // the account as logged in?

  var acct = this.getAccount(acctCoopObj.id());
  if (aListener) {
    aListener.onSuccess(acct, "addAccount");
  }
  return acct;
};

// void removeAccount(in AString aUrn);
PhotobucketService.prototype.removeAccount =
function pbSvc_removeAccount(aURN) {
  this._acUtils.removeAccount(aURN);
};

// DEFAULT: flockIWebServiceAccount getAccount(in AString aUrn);
// DEFAULT: nsISimpleEnumerator getAccounts();

// void logout();
PhotobucketService.prototype.logout =
function pbSvc_logout() {
  this._deauthenticateAPI();
  this._acUtils.removeCookies(this._WebDetective
                                  .getSessionCookies("photobucket"));
};


/**************************************************************************
 * Flock Photobucket Service: flockIManageableWebService Implementation
 **************************************************************************/

// readonly attribute AString domains;
// This gets set up during construction, from Web Detective.
PhotobucketService.prototype.domains = {};

// readonly attribute AString loginURL;
PhotobucketService.prototype.loginURL = "";

// DEFAULT: boolean docRepresentsSuccessfulLogin(in nsIDOMHTMLDocument aDocument);

// AString getAccountIDFromDocument(in nsIDOMHTMLDocument aDocument);
PhotobucketService.prototype.getAccountIDFromDocument =
function pbSvc_getAccountIDFromDocument(aDocument) {
  this._logger.debug(".getAccountIDFromDocument(aDocument)");
  aDocument.QueryInterface(CI.nsIDOMHTMLDocument);
  var results = CC["@mozilla.org/hash-property-bag;1"]
                .createInstance(CI.nsIWritablePropertyBag2);
  if (this._WebDetective.detect("photobucket",
                                "accountinfo",
                                aDocument,
                                results))
  {
    return results.getPropertyAsAString("userid");
  } else {
    // We could not detect any account info on the login landing page.  This
    // may mean that it's the API authentication login landing page, which
    // doesn't have any account info.  If this is the case, then we should
    // have a temp password entry associated with the session cookie.
    // XXX Use Web Detective
    var sessionValue = this._acUtils.getCookie("http://photobucket.com",
                                              "PHPSESSID");
    var pw =
      this._acUtils.getTempPassword("photobucket:session:" + sessionValue);
    if (pw) {
      this._logger.debug("Got acctID from temp password in session cookie!");
      return pw.user;
    }
  }
  return null;
};

// nsIPassword getCredentialsFromForm(in nsIDOMHTMLFormElement aForm);
PhotobucketService.prototype.getCredentialsFromForm =
function pbSvc_getCredentialsFromForm(aForm) {
  var inst = this;
  var detectForm = function pbsgc_detectForm(aType, aResults) {
    return inst._WebDetective.detectForm("photobucket", aType, aForm, aResults);
  };

  var formType = "login";
  var results = FlockSvcUtils.newResults();
  if (!detectForm(formType, results)) {
    formType = "signup";
    results = FlockSvcUtils.newResults();
    if (!detectForm(formType, results)) {
      formType = "changepassword";
      results = FlockSvcUtils.newResults();
      if (!detectForm(formType, results)) {
        results = null;
      }
    }
  }

  if (results) {
    var pw = {
      QueryInterface: function pbsgcpw_QueryInterface(aIID) {
        if (!aIID.equals(CI.nsISupports) &&
            !aIID.equals(CI.nsIPassword) &&
            !aIID.equals(CI.flockIPassword))
        {
          throw CI.NS_ERROR_NO_INTERFACE;
        }
        return this;
      },
      user: results.getPropertyAsAString("username"),
      password: results.getPropertyAsAString("password"),
      host: null,
      formType: formType
    };

    // Doing a bit of a hack here...
    // Since the Photobucket login landing page doesn't always reveal which
    // account is logged in, we will need to look at the session cookie and
    // see if its the same as what it was when the user last logged in.  So
    // at this point we're just storing the account username as a temp
    // password entry associated with the session cookie token.
    // XXX Use Web Detective
    var sessionValue = this._acUtils.getCookie("http://photobucket.com",
                                              "PHPSESSID");
    this._acUtils.clearTempPassword("photobucket:session:" + sessionValue);
    this._acUtils.setTempPassword("photobucket:session:" + sessionValue,
                                  pw.user, "", formType);
    return pw;
  }
  return null;
};

// DEFAULT: boolean ownsDocument(in nsIDOMHTMLDocument aDocument);
// DEFAULT: boolean ownsLoginForm(in nsIDOMHTMLFormElement aForm);

// void updateAccountStatusFromDocument(in nsIDOMHTMLDocument aDocument);
PhotobucketService.prototype.updateAccountStatusFromDocument =
function pbSvc_updateAccountStatusFromDocument(aDocument, aAcctURN) {
  this._logger.debug("updateAccountStatusFromDocument('" + aAcctURN + "')");
  if (aAcctURN) {
    // We're logged in to this account
    var c_acct = this._coop.get(aAcctURN);
    if (!c_acct.isAuthenticated) {
      var inst = this;
      // Re-authenticate the API
      var reauthListener = {
        onSuccess: function pbSvc_updateASFD_onSuccess(aSubject, aTopic) {
          inst._logger.debug("reauthListener: onSuccess()");
        },
        onError: function pbSvc_updateASFD_onError(aSubject, aTopic, aError) {
          inst._logger.debug("reauthListener: onError()");
          c_acct.isAuthenticated = false;
        }
      };
      this.getAccount(aAcctURN).login(reauthListener);
      // Prematurely set isAuthenticated to TRUE in order to avoid other
      // asyncronous auth attempts before this one completes.
      c_acct.isAuthenticated = true;
    }
  } else if (this._WebDetective
                 .detect("photobucket", "loggedout", aDocument, null))
  {
    // We're logged out (of all accounts)
    this._acUtils.markAllAccountsAsLoggedOut(CONTRACT_ID);
    this._deauthenticateAPI();
  }
};


/**************************************************************************
 * Flock Photobucket Service: flockIMediaWebService Implementation
 **************************************************************************/
// readonly attribute boolean supportsUsers;
PhotobucketService.prototype.supportsUsers = true;

// void decorateForMedia(in nsIDOMHTMLDocument aDocument);
PhotobucketService.prototype.decorateForMedia =
function pbSvc_decorateForMedia(aDocument) {
  this._logger.debug(".decorateForMedia(aDocument)");
  var results = CC["@mozilla.org/hash-property-bag;1"]
                .createInstance(CI.nsIWritablePropertyBag2);
  if (this._WebDetective.detect("photobucket", "media", aDocument, results)) {
    var userid = results.getPropertyAsAString("userid");
    var mediaArr = [];

    if (!aDocument._flock_decorations) {
      aDocument._flock_decorations = {};
    }
    var media = {
      name: userid,
      query: "user:" + userid + "|username:" + userid,
      label: userid,
      favicon: this.icon,
      service: this.shortName
    };
    mediaArr.push(media);

    aDocument._flock_decorations.mediaArr = mediaArr;
    this._obsSvc.notifyObservers(aDocument, "media", "media:update");
  }
};

// void createAlbum(in flockIListener aListener, in AString aAlbumName);
PhotobucketService.prototype.createAlbum =
function pbSvc_createAlbum(aListener, aAlbumName) {
  this._logger.debug("createAlbum - aAlbumName = " + aAlbumName);

  var parentDir = "";
  var pathName = aAlbumName.split("/");
  var params = {};
  params.new_album_name = pathName.pop();

  for (var i = 0; i < pathName.length; i++) {
    parentDir += pathName[i] + "/";
  }

  if (parentDir.length) {
    params.parent_album_name = parentDir;
  }

  var svc = this;
  var albumCreationListener = {
    onResult: function pbSvc_createAlbum_onResult(aXML) {
      svc._logger.info(aAlbumName + " album successfully created");
      var newAlbum = CC[FLOCK_PHOTO_ALBUM_CONTRACTID]
                     .createInstance(CI.flockIPhotoAlbum);
      var newAlbumPath;
      if (params.parent_album_name) {
        newAlbumPath = params.parent_album_name + params.new_album_name;
      } else {
        newAlbumPath = params.new_album_name;
      }
      newAlbum.title = newAlbumPath;
      newAlbum.id = newAlbumPath;
      aListener.onSuccess(newAlbum, "");
    },

    onError: function pbSvc_createAlbum_onError(aError) {
      svc._logger.error("Error in album creation: "
                        + aError.errorCode
                        + " Service: ["
                        + aError.serviceErrorCode
                        + "] "
                        + aError.serviceErrorString
                        );
      aListener.onError(null, null, aError);
    }
  };

  var dict = params2Dictionary(params);
  this._api.authenticatedCall(albumCreationListener, "createalbum", dict);
};

// void findByUsername(in flockIListener aListener, in AString aUsername);
PhotobucketService.prototype.findByUsername =
function pbSvc_findByUsername(aListener, aUsername) {
  // XXX ERWAN: why are we making a call to the server, if we don't
  // use the xml we get as result??
  var inst = this;
  var myListener = {
    onResult: function pbSvc_findByUsername_onResult(aXML) {
      //var user = aXML.getElementsByTagName("photo")[0];
      // We should be getting all the user's details from the response xml, but
      // the pbucket response has all pertinent info with the photo tag.  If
      // there are no photos, then we have no user info
      var newUserObj = CC["@flock.com/person;1"]
                       .createInstance(CI.flockIPerson);
      newUserObj.accountId = aUsername;
      newUserObj.serviceId = inst.contractID;
      newUserObj.name = aUsername;
      newUserObj.totalMedia = -1; // See mediabar.js
      aListener.onSuccess(newUserObj, "");
    },

    onError: function pbSvc_findByUsername_onError(aXML) {
      aListener.onError(null, null, null);
    }
  };
  var params = {};
  params.username = aUsername;
  var dict = params2Dictionary(params);
  this._api.authenticatedCall(myListener, "getrecentusermedia", dict);
};

// void getAccountStatus(in flockIListener aListener);
PhotobucketService.prototype.getAccountStatus =
function pbSvc_getAccountStatus(aListener) {
  var inst = this;
  var myListener = {
    onResult: function pbSvc_getAccountStatus_onResult(aXML) {
      try {
        var result = CC["@mozilla.org/hash-property-bag;1"]
                     .createInstance(CI.nsIWritablePropertyBag2);
        var username = aXML.getElementsByTagName("username")[0]
                           .firstChild
                           .nodeValue;
        var maxSpace = aXML.getElementsByTagName("megabytes_allowed")[0]
                           .firstChild
                           .nodeValue;
        var usedSpace = aXML.getElementsByTagName("megabytes_used")[0]
                            .firstChild
                            .nodeValue;
        var isPremium = aXML.getElementsByTagName("premium")[0]
                            .firstChild
                            .nodeValue;
        result.setPropertyAsAString("username", username);
        result.setPropertyAsAString("maxSpace", maxSpace);
        result.setPropertyAsAString("usedSpace", usedSpace);
        result.setPropertyAsAString("maxFileSize", "");
        result.setPropertyAsAString("usageUnits", "megabytes");
        result.setPropertyAsBool("isPremium", (isPremium == "1"));
        aListener.onSuccess(result, "");
      } catch (ex) {
        inst._logger.error("getaccountstatus error :" + ex);
      }
    },

    onError: function pbSvc_getAccountStatus_onError(aError) {
      aListener.onError(aError);
    }
  };
  var params = {};
  var dict = params2Dictionary(params);
  this._api.authenticatedCall(myListener, "getuserinfo", dict);
};

// void getAlbums(in flockIListener aListener, in AString aUsername);
PhotobucketService.prototype.getAlbums =
function pbSvc_getAlbums(aListener, aUsername) {
  var inst = this;
  var myListener = {
    onResult: function pbSvc_getAlbums_onResult(aXML) {
      try {
        var rval = inst._handleAlbumsResult(aXML);
        var albumsEnum = {
          hasMoreElements: function pbslenum_hasMoreElements() {
            return (rval.length > 0);
          },
          getNext: function pbslenum_getNext() {
            return rval.shift();
          },
          QueryInterface: function pbslenum_QI(aIID) {
            if (aIID.equals(CI.nsISimpleEnumerator)) {
              return this;
            }
            throw Cr.NS_ERROR_NO_INTERFACE;
          }
        };
        aListener.onSuccess(albumsEnum, "");
      } catch (ex) {
        inst._logger.error(ex);
        aListener.onError(null, null, null);
      }
    },

    onError: function pbSvc_getAlbums_onError(aXML) {
      aListener.onError(null, null, null);
    }
  };
  var params = {};
  params.recurse = "true";
  params.view = "flat";
  params.media = "none";
  if (aUsername) {
    params.username = aUsername;
  }
  var dict = params2Dictionary(params);
  this._api.authenticatedCall(myListener, "getuseralbum", dict);
};

// void getAlbumsForUpload(in flockIListener aListener, in AString aUsername);
PhotobucketService.prototype.getAlbumsForUpload = 
function pbSvc_getAlbumsForUpload(aListener, aUsername) {
  this.getAlbums(aListener, aUsername);
};

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

// flockIMediaItemFormatter getMediaItemFormatter();
PhotobucketService.prototype.getMediaItemFormatter =
function pbSvc_getMediaItemFormatter() {
  return flockMediaItemFormatter;
};

// void migrateAccount(in AString aId, in AString aUsername);
PhotobucketService.prototype.migrateAccount =
function pbSvc_migrateAccount( aId, aUsername) {
  var token = "";
  try {
    token = flock_getCharPref(PHOTOBUCKET_TOKEN_PREF);
  } catch (ex) { }

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

// void search(in flockIListener aListener,
//             in AString aQuery,
//             in long aCount,
//             in long aPage);
PhotobucketService.prototype.search =
function pbSvc_search(aListener, aQueryString, aCount, aPage, aRequestId) {
  var aQuery = new queryHelper(aQueryString);
  if (!aQuery.user) {
    this._queryChannel(aListener, aQueryString, aCount, aPage, aRequestId);
    return;
  }
  var aUserid = aQuery.user;

  var inst = this;

  var myListener = {
    onResult: function pbSvc_search_onResult(aXML) {
      var rval = inst._handlePhotosResult(aXML);
      var resultsEnum = {
        hasMoreElements: function pbslenum_hasMoreElements() {
          return (rval.length > 0);
        },
        getNext: function pbslenum_getNext() {
          return rval.shift();
        },
        QueryInterface: function pbslenum_QueryInterface(aIID) {
          if (aIID.equals(CI.nsISimpleEnumerator)) {
            return this;
          }
          throw Cr.NS_ERROR_NO_INTERFACE;
        }
      };
      aListener.onSuccess(resultsEnum, aRequestId);
    },

    onError: function pbSvc_search_onError(aError) {
      aListener.onError(null, aRequestId, aError);
    }
  };

  var params = {};

  if (aUserid) {
    params.username = aUserid;
    params.perpage = aCount;
  }

  if (aQuery.search) {
    params.query = aQuery.search.split(".")[0];
  }
  if (aPage) {
    params.page = aPage;
  }
  if (aQuery.album) {
    params.album_name = aQuery.album;
  }

  params.media = "all";

  var dict = params2Dictionary(params);
  if (params.query) {
    this._api.call(myListener, "search", dict);
  } else if (params.username) {
    if (params.album_name) {
      this._api.authenticatedCall(myListener, "getuseralbumpaginated", dict);
    } else {
      this._api.authenticatedCall(myListener, "getrecentusermedia", dict);
    }
  }
};

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

// boolean supportsSearch(in AString aQuery);
PhotobucketService.prototype.supportsSearch =
function pbSvc_supportsSearch( aQueryString ) {

  var aQuery = new queryHelper(aQueryString);

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

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

// void upload(in flockIPhotoUploadAPIListener aListener,
//             in flockIPhotoUpload aUpload,
//             in AString aFilename);
PhotobucketService.prototype.upload =
function pbSvc_upload(aListener, aUpload, aFile) {
  var inst = this;
  var sigParams = {};
  var myParams = {};

  this._logger.debug("uploading to album " + aUpload.album);
  myParams.version = PHOTOBUCKET_API_VERSION;
  myParams.scid = PHOTOBUCKET_SCID;
  myParams.method = "upload";
  myParams.description = aUpload.description;
  myParams.title = aUpload.title;
  myParams.album_name = aUpload.album;
  myParams.session_key = this._getAuthUser().sessionkey;
  myParams.sig = this._api.buildSig("upload", sigParams, true);

  sigParams.session_key = this._getAuthUser().sessionkey;

  this._photoUploader = new PhotoUploader();
  this._photoUploader.setEndpoint(inst._api._apiUrl);
  this._photoUploader.photoParam = "uploadfile";

  var myListener = {
    onResult: function pbSvc_upload_onResult(aXML) {
      var photo = aXML.getElementsByTagName("photo")[0];
      if (photo) {
        inst._logger.debug("uploaded pic named " + photo.getAttribute("name"));
        var rval = inst._handlePhotosResult(aXML);
        aListener.onUploadComplete(aUpload);
        aListener.onUploadFinalized(aUpload, rval[0]);
      } else {
        aListener.onError(inst._api.getError("SERVICE_ERROR", aXML, null));
      }
    },

    onError: function pbSvc_upload_onError(aErrorCode) {
      if (aErrorCode) {
        aListener.onError(inst._api.getError("HTTP_ERROR", null, aErrorCode));
      } else {
        aListener.onError(inst._api.getError("SERVICE_ERROR", null, null));
      }
    },

    onProgress: function pbSvc_upload_onProgress(aCurrentProgress) {
        aListener.onProgress(aCurrentProgress);
    }
  };
  this._photoUploader.upload(myListener, aFile, myParams, aUpload);
};

/**************************************************************************
 * Flock Photobucket Service: flockIMediaEmbedWebService Implementation
 **************************************************************************/

// boolean checkIsStreamUrl(in AString aUrl);
PhotobucketService.prototype.checkIsStreamUrl =
function pbSvc_checkIsStreamUrl(aUrl) {
  // Videos:
  // /player.swf?file=http://vid166.photobucket.com/albums/u113/h671226/Smart_Dog.flv
  // Images:
  // http://i166.photobucket.com/albums/u105/such-a-craka/thfhg.jpg
  if (this._WebDetective.detectNoDOM("photobucket",
                                     "isStreamUrl",
                                     "",
                                     aUrl,
                                     null))
  {
    this._logger.debug("Checking if url is photobucket stream: YES: " + aUrl);
    return true;
  }
  this._logger.debug("Checking if url is photobucket stream: NO: " + aUrl);
  return false;
};

// void getMediaQueryFromURL(in AString aUrl, in flockIListener aListener);
PhotobucketService.prototype.getMediaQueryFromURL =
function pbSvc_getMediaQueryFromURL(aUrl, aListener) {
  var userName = null;
  var detectResults = CC["@mozilla.org/hash-property-bag;1"]
                      .createInstance(CI.nsIWritablePropertyBag2);
  if (this._WebDetective.detectNoDOM("photobucket",
                                     "person",
                                     "",
                                     aUrl,
                                     detectResults))
  {
    userName = detectResults.getPropertyAsAString("userid");
  }

  if (userName) {
    var results = CC["@mozilla.org/hash-property-bag;1"]
                  .createInstance(CI.nsIWritablePropertyBag2);
    results.setPropertyAsAString("query",
                                 "user:" + userName + "|username:" + userName);
    results.setPropertyAsAString("title", this.title + " user: " + userName);
    aListener.onSuccess(results, "query");
  } else {
    aListener.onError(null, "Unable to get user.", null);
  }
};

// DEFAULT: boolean getSharingContent(in nsIDOMHTMLElement aSrc,
//                                    in nsIWritablePropertyBag2 aProp);

/**************************************************************************
 * Flock Photobucket Service: flockIAuthenticateNewAccount Implementation
 **************************************************************************/

// DEFAULT: void authenticateNewAccount(in flockIListener aListener);

/**************************************************************************
 * Flock Photobucket Service: flockIPollingService Implementation
 **************************************************************************/

// void refresh(in AString aUrn, in flockIPollingListener aListener);
PhotobucketService.prototype.refresh =
function pbSvc_refresh(aURN, aListener) {
  var refreshItem = this._coop.get(aURN);
  if (refreshItem.isInstanceOf(this._coop.Account)) {
    aListener.onResult();
  }
};


/**************************************************************************
 * Flock Photobucket Service: nsITimerCallback Implementation
 **************************************************************************/

// void notify(in nsITimer timer);
PhotobucketService.prototype.notify =
function pbSvc_notify(aTimer) {
  var inst = this;
  var sessionRefreshListener = {
    onAuth: function pbSvc_notify_onAuth() {
      inst._logger.debug("refreshed session key with the timer");
    },

    onError: function pbSvc_notify_onError(aError) {
      inst._logger.error("error refreshing session key on the timer");
    }
  };
  this._getSessionKey(sessionRefreshListener);
};

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

// Create array of components.
var gComponentsArray = [PhotobucketService];

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

/**************************************************************************
 * END XPCOM Support
 **************************************************************************/
