// 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

// CONTENTS OF THIS FILE:
// This file contains the implementation of the LiveJournal service. This
// involves the following components:
//   flockLJService - the service class itself
//   flockLJController - the controller class
//   flockLJServiceFactory - factory object for flockLJService
//   flockLJModule - module object for XPCOM registration
//   flockLJAccount - the account class

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");

const LIVEJOURNAL_CID = Components.ID("{e642271b-1b0d-4d5d-ab51-34de23f75fc7}");
const LIVEJOURNAL_CONTRACTID = "@flock.com/people/livejournal;1";
const LIVEJOURNAL_TITLE = "LiveJournal Web Service";
const LIVEJOURNAL_FAVICON = "chrome://flock/content/services/livejournal/favicon.png";
const SERVICE_ENABLED_PREF = "flock.service.livejournal.enabled";
const CATEGORY_COMPONENT_NAME = "LiveJournal JS Component";
const CATEGORY_ENTRY_NAME = "livejournal";

const CLASS_SHORT_NAME = "livejournal";
const CLASS_NAME = "LiveJournal";
const MODULE_NAME = "livejournal";

const METADATA_OVERLAY = "chrome://flock/content/services/livejournal/ljBlogOverlay.xul";
const APILINK_BLOGGER = "https://www.livejournal.com/interface/blogger";
const APILINK_XMLRPC = "https://www.livejournal.com/interface/xmlrpc";

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

// String defaults... may be updated later through Web Detective
var gStrings = {
  "domains": "livejournal.com",
  "userprofile": "http://%accountid%.livejournal.com/profile",
  "editprofile": "http://www.livejournal.com/manage/profile/",
  "userblog": "http://%accountid%.livejournal.com/",
  "userfriends": "http://%accountid%.livejournal.com/friends",
  "userphotos": "http://pics.livejournal.com/%accountid%",
  "userpics": "http://www.livejournal.com/allpics.bml?user=%accountid%",
  "postcomment": "http://fixme.com",
  "userfoaf": "http://www.livejournal.com/users/%accountid%/data/foaf"
};

// Helper function for LJ URLs - needs to be updated periodically
function makeLiveJournalURL(aType, aFriendID) {
  switch (aType) {
    case "openProfile":
      return gStrings["userprofile"].replace("%accountid%", aFriendID);
    case "openBlog":
      return gStrings["userblog"].replace("%accountid%", aFriendID);
    case "viewFriends":
      return gStrings["userfriends"].replace("%accountid%", aFriendID);
    case "postComment":
      return gStrings["postcomment"].replace("%accountid%", aFriendID);
    case "openPhotos":
      return gStrings["userphotos"].replace("%accountid%", aFriendID);
    case "userpics":
      return gStrings["userpics"].replace("%accountid%", aFriendID);
    default:
      return gStrings[aType].replace("%accountid%", aFriendID);
  }
}



/*************************************************************************
 * Component: Flock Livejournal Service
 *************************************************************************/

function flockLJService() {
  var loader = CC["@mozilla.org/moz/jssubscript-loader;1"]
               .getService(CI.mozIJSSubScriptLoader);
  loader.loadSubScript("chrome://browser/content/utilityOverlay.js");
  loader.loadSubScript("chrome://flock/content/xmlrpc/xmlrpchelper.js");
  loader.loadSubScript("chrome://flock/content/blog/blogBackendLib.js");

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

  this._accountClassCtor = flockLJAccount;

  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");
}

/*************************************************************************
 * flockLJService: XPCOM Component Creation
 *************************************************************************/

flockLJService.prototype = new FlockXPCOMUtils.genericComponent(
  LIVEJOURNAL_TITLE,
  LIVEJOURNAL_CID,
  LIVEJOURNAL_CONTRACTID,
  flockLJService,
  CI.nsIClassInfo.SINGLETON,
  [
    CI.flockIWebService,
    CI.flockIAuthenticateNewAccount,
    CI.flockILoginWebService,
    CI.flockIBlogWebService
  ]
);

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

/*************************************************************************
 * flockLJService: flockIWebService implementation
 *************************************************************************/

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

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

// readonly attribute AString icon;
flockLJService.prototype.icon = LIVEJOURNAL_FAVICON;

// readonly attribute AString contractId;
// ALMOST duplicated by nsIClassInfo::contractID
// with different case: contractId != contractID
flockLJService.prototype.contractId = LIVEJOURNAL_CONTRACTID;

// readonly attribute AString urn;
// Defined in ._init()

/*************************************************************************
 * flockLJService: flockILoginWebService implementation
 *************************************************************************/

// readonly attribute AString domains;
// TODO: Where is this defined?

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

// flockIWebServiceAccount addAccount(in AString aAccountId, ...
// readonly attribute AString urn;
flockLJService.prototype.urn = "urn:" + CLASS_SHORT_NAME + ":service";

flockLJService.prototype.addAccount =
function flockLJService_addAccount(aAccountId, aIsTransient, aFlockListener) {
  this._logger.debug("addAccount('" + aAccountId + "', "
                     + aIsTransient + ", aFlockListener)");

  // Bug 13151: Livejournal is not case-sensitive, but Flock is and
  // would consider fooBar and foobaR as two different accounts.
  // To mitigate that, we normalize the accountId to lowercase.
  var accountID = aAccountId.toLowerCase();
  var ljAccountURN = "urn:flock:lj" + accountID;
  var ljAccount = new this._coop.Account(ljAccountURN, {
    name: accountID,
    serviceId: LIVEJOURNAL_CONTRACTID,
    service: this._coopObj,
    accountId: accountID,
    favicon: LIVEJOURNAL_FAVICON,
    URL: "http://www.livejournal.com/portal",
    isTransient: aIsTransient
  });
  this.account_root.children.addOnce(ljAccount);
  var acct = this.getAccount(ljAccountURN);

  // Add the blog(s)
  var ljsvc = this;
  var blogListener = {
    onResult: function addAcc_listener_onResult(aSimpleEnum) {
      ljsvc._logger.info("addAccount: got the blog list from the server");
      var theBlog;
      while (aSimpleEnum.hasMoreElements()) {
        theBlog = aSimpleEnum.getNext();
        theBlog.QueryInterface(CI.nsIPropertyBag2);
        var blogID = theBlog.getPropertyAsAString("blogID");
        var title = theBlog.getPropertyAsAString("title");
        var URL = theBlog.getPropertyAsAString("URL");
        var blogURN = ljAccountURN + ":" + title;
        var theCoopBlog = new ljsvc._coop.Blog(blogURN, {
          name: title,
          title: title,
          blogid: blogID,
          URL: URL,
          apiLink: APILINK_XMLRPC
        });
        ljAccount.children.addOnce(theCoopBlog);
      }
      if (aFlockListener) {
        aFlockListener.onSuccess(acct, "addAccount");
      }
    },
    onFault: function addAcc_listener_onFault(aFlockError) {
      ljsvc._logger.error("addAcc_listener_onFault");
      this.onError(aFlockError);
    },
    onError: function addAcc_listener_onError(aFlockError) {
      ljsvc._logger.error("addAcc_listener_onError");
      ljAccount.destroy();
      if (aFlockListener) {
        aFlockListener.onError(aFlockError, null);
      }
    }
  };

  var apiLink = APILINK_BLOGGER;
  var pw = this._acUtils.getPassword(this.urn + ":" + accountID);
  this.getUsersBlogs(blogListener, apiLink, accountID, pw.password);

  return acct;
};

// void logout();
flockLJService.prototype.logout =
function flockLJService_logout() {
  this._acUtils.markAllAccountsAsLoggedOut(this);
  this._acUtils.removeCookies(this._WebDetective
                                  .getSessionCookies("livejournal"));
};

// boolean ownsDocument(in nsIDOMHTMLDocument aDocument);
flockLJService.prototype.ownsDocument =
function flockLJService_ownsDocument(aDocument) {
  this._logger.debug("ownsDocument()");
  aDocument.QueryInterface(CI.nsIDOMHTMLDocument);
  var ios = CC["@mozilla.org/network/io-service;1"]
            .getService(CI.nsIIOService);
  var uri = ios.newURI(aDocument.URL, null, null);
  if ((uri.host.indexOf("livejournal.com") == 0) ||
      (uri.host.indexOf(".livejournal.com") > 0))
  {
    if (uri.host.indexOf("adserver.livejournal.com") >= 0) {
      return false;
    } else {
      return true;
    }
  }
  return false;
};

// void updateAccountStatusFromDocument(in nsIDOMHTMLDocument aDocument, ...
flockLJService.prototype.updateAccountStatusFromDocument =
function flockLJService_updateAccountStatusFromDocument(aDocument,
                                                        aAccount,
                                                        aFlockListener)
{
  if (aAccount) {
    // We're logged in to this account
    this._acUtils.ensureOnlyAuthenticatedAccount(aAccount);
  } else if (this._WebDetective
                 .detect("livejournal", "loggedout", aDocument, null))
  {
    // We're logged out (of all accounts)
    this._acUtils.markAllAccountsAsLoggedOut(this);
  }
};


/*************************************************************************
 * flockLJService: flockIBlogWebService implementation
 *************************************************************************/

// attribute long supportsCategories;
flockLJService.prototype.supportsCategories = CI.flockIBlogWebService.CATEGORIES_NO;

// attribute boolean supportsPostReplace;
flockLJService.prototype.supportsPostReplace = true;

// attribute AString metadataOverlay;
flockLJService.prototype.metadataOverlay = METADATA_OVERLAY;

// attribute AString authtoken;
flockLJService.prototype.authtoken = null;

// void getUsersBlogs(in flockIBlogListener aBlogListener, ...
flockLJService.prototype.getUsersBlogs =
function flockLJService_getUsersBlogs(aBlogListener,
                                      aAPILink,
                                      aUsername,
                                      aPassword)
{
  this._logger.debug("getUsersBlogs('" + aUsername + "')");
  var svc = this;

  var gubListener = {
    // aResult is an Array (simpleEnumerator) of struct objects
    onResult: function gubListener_onResult(aResult) {
      svc._logger.debug("gubListener_onResult");
      var result = [];
      for each (var entry in aResult){
        var blog = CC["@mozilla.org/hash-property-bag;1"]
                   .createInstance(CI.nsIWritablePropertyBag2);
        blog.setPropertyAsAString("title", entry.blogName);
        blog.setPropertyAsAString("blogID", entry.blogid);
        blog.setPropertyAsAString("APILink", "");
        blog.setPropertyAsAString("URL", entry.url);
        result.push(blog);
      }
      aBlogListener.onResult(new simpleEnumerator(result));
    },
    onFault: function gubListener_onFault(aFlockError) {
      svc._logger.error("gubListener_onFault: " + aFlockError.serviceErrorCode
                        + " " + aFlockError.serviceErrorString);
      switch (aFlockError.serviceErrorCode) {
        case 0:
          // Invalid login/pass
          aFlockError.errorCode = CI.flockIError.BLOG_INVALID_AUTH;
          break;
        default:
          // Unknown error code
          aFlockError.errorCode = CI.flockIError.BLOG_UNKNOWN_ERROR;
      }
      aBlogListener.onFault(aFlockError);
    },
    onError: function gubListener_onError(aFlockError) {
      aBlogListener.onError(aFlockError);
    }
  };

  var xmlrpcServer = new flockXmlRpcServer(aAPILink);
  var args = ["flockbrowser", aUsername, aPassword];
  xmlrpcServer.call("blogger.getUsersBlogs", args, gubListener);
};

// void getRecentPosts(in flockIBlogListener aBlogListener, ...
flockLJService.prototype.getRecentPosts =
function flockLJService_getRecentPosts(aBlogListener, aBlogId, aNumber) {
  this._logger.debug("getRecentPosts('" + aBlogId + "')");
  var svc = this;

  var coopBlog = this._coop.get(aBlogId);
  var coopAccount = coopBlog.getParent();

  var pw = this._acUtils.getPassword(this.urn + ":" + coopAccount.accountId);

  var grpListener = {
    onResult: function grpListener_onResult(aResult) {
      svc._logger.debug("grpListener_onResult");
      var result = [];
      for (var i=0; i<aResult.length; i++) {
        var post = new BlogPost();
        var content = aResult[i].content;
        if (content.match(/<title.*>(.+?)</)) {
          post.title = RegExp.$1;
        }
        post.issued = aResult[i].dateCreated.getTime();
        post.postid = aResult[i].postId.split(":")[1];
        result.push(post);
      }
      aBlogListener.onResult(new simpleEnumerator(result));
    },
    onFault: function grpListener_onFault(aFlockError) {
      svc._logger.error("grpListener fault: " + aFlockError.serviceErrorCode
                        + " " + aFlockError.serviceErrorString);
      aFlockError.errorCode = CI.flockIError.BLOG_UNKNOWN_ERROR;
      aBlogListener.onFault(aFlockError);
    },
    onError: function grpListener_onError(aFlockError) {
      svc._logger.error("grpListener error " + aFlockError.serviceErrorString);
      aBlogListener.onError(aFlockError);
    }
  };

  var xmlrpcServer = new flockXmlRpcServer(APILINK_BLOGGER);
  // The username is used as the blogid for this call
  var args = [
    "flockblog",
    coopAccount.accountId,
    coopAccount.accountId,
    pw.password,
    aNumber
  ];
  xmlrpcServer.call("blogger.getRecentPosts", args, grpListener);
};

// void getCategoryList(in flockIBlogListener aBlogListener, ...
flockLJService.prototype.getCategoryList =
function flockLJService_getCategoryList(aBlogListener, aAccount) {
  this._logger.debug("getCategoryList('" + aAccount + "')");
  // supportsCategories is 0; provide an empty enumerator for any callers.
  aBlogListener.onResult(new simpleEnumerator([]));
};

// void newPost(in flockIBlogPublishListener aPublishListener, ...
flockLJService.prototype.newPost =
function flockLJService_newPost(aPublishListener,
                                aBlogId,
                                aPost,
                                aPublish,
                                aNotifications)
{
  this._logger.debug("newPost('" + aBlogId + "')");
  var svc = this;

  var coopBlog = this._coop.get(aBlogId);
  var coopAccount = coopBlog.getParent();

  var pw = this._acUtils.getPassword(this.urn + ":" + coopAccount.accountId);

  var extra = [];
  if (aPost.extra) {
    while (aPost.extra.hasMore()) {
      extra.push(aPost.extra.getNext());
    }
  }

  var nocomments = (extra[2] == "0") ? false : true;

  var date = new Date();
  var month = date.getMonth() + 1;
  var day = date.getDate();
  var year = date.getFullYear();
  var hour = date.getHours();
  var min = date.getMinutes();

  var labels = [];
  if (aPost.tags) {
    while (aPost.tags.hasMore()) {
      labels.push(aPost.tags.getNext());
    }
  }
  var content = aPost.description.replace("</lj>", "", "gi");

  var npListener = {
    onResult: function npListener_onResult(aResult) {
      svc._logger.debug("npListener_onResult: " + aResult.url);
      aPublishListener.onResult(aResult.url);
    },
    onFault: function npListener_onFault(aFlockError) {
      svc._logger.error("npListener Fault: " + aFlockError.serviceErrorCode
                        + " " + aFlockError.serviceErrorString);
      switch (aFlockError.serviceErrorCode) {
        case 0: // Invalid login/pass
          aFlockError.errorCode = CI.flockIError.BLOG_INVALID_AUTH;
          break;
        default: // Unknown error code
          aFlockError.errorCode = CI.flockIError.BLOG_UNKNOWN_ERROR;
      }
      aPublishListener.onFault(aFlockError);
    },
    onError: function npListener_onError(aFlockError) {
      svc._logger.error("npListener Error: " + aFlockError.serviceErrorString);
      aPublishListener.onError(aFlockError);
    }
  };

  var xmlrpcServer = new flockXmlRpcServer(coopBlog.apiLink);
  var args = [{username: coopAccount.accountId, password: pw.password,
               subject: aPost.title, event: content,
               security: extra[0], allowmask: extra[1], ver: 1,
               year: year, mon: month, day: day, hour: hour, min: min,
               // props: [{opts_nocomments: nocomments}],
               props: {taglist: labels.join(","), opt_nocomments: nocomments}
               }];
  xmlrpcServer.call("LJ.XMLRPC.postevent", args, npListener);
};

// void editPost(in flockIBlogPublishListener aPublishListener, ...
flockLJService.prototype.editPost =
function flockLJService_editPost(aPublishListener,
                                 aBlogId,
                                 aPost,
                                 aPublish,
                                 aNotifications)
{
  this._logger.debug("editPost('" + aBlogId + "')");
  var svc = this;

  var coopBlog = this._coop.get(aBlogId);
  var coopAccount = coopBlog.getParent();

  var pw = this._acUtils.getPassword(this.urn + ":" + coopAccount.accountId);

  var labels = [];
  if (aPost.tags) {
    while (aPost.tags.hasMore()) {
      labels.push(aPost.tags.getNext());
    }
  }
  var content = aPost.description.replace("</lj>", "", "gi");
  var extra = [];
  if (aPost.extra) {
    while (aPost.extra.hasMore()) {
      extra.push(aPost.extra.getNext());
    }
  }
  var nocomments = (extra[2] == "0") ? false : true;

  var epListener = {
    onResult: function epListener_onResult(aResult) {
      svc._logger.debug("epListener Result: " + aResult);
      aPublishListener.onResult(aResult);
    },
    onFault: function epListener_onFault(aFlockError) {
      svc._logger.error("epListener Fault: " + aFlockError.serviceErrorCode
                        + " " + aFlockError.serviceErrorString);
      aPublishListener.onFault(aFlockError);
    },
    onError: function epListener_onError(aFlockError) {
      svc._logger.error("epListener Error: " + aFlockError.serviceErrorString);
      aPublishListener.onError(aFlockError);
    }
  };

  var xmlrpcServer = new flockXmlRpcServer(coopBlog.apiLink);
  var args = [{username: coopAccount.accountId,
               password: pw.password,
               itemid: aPost.postid,
               subject: aPost.title,
               event: content,
               security: extra[0],
               allowmask: extra[1],
               ver: 1,
               props: {
                 taglist: labels.join(","),
                 opt_nocomments: nocomments
               }
              }];
  xmlrpcServer.call("LJ.XMLRPC.editevent", args, epListener);
};

// TODO: This is not in the IDL, but I am keeping it for possible addition.
// void flockLJService_deletePost(in flockIBlogPublishListener aPublishListener,
//                                in AString aBlogId,
//                                in AString aPostId);
flockLJService.prototype.deletePost =
function flockLJService_deletePost(aListener, aBlogId, aPostid) {
  this._logger.error("deletePost: not implemented!");
  throw CR.NS_ERROR_NOT_IMPLEMENTED;
};

// void setPostCategories(in flockIBlogPublishListener aPublishListener, ...
// This was not defined at all, so a stub is better than nothing.
flockLJService.prototype.setPostCategories =
function flockLJService_setPostCategories(aPublishListener,
                                          aBlogId,
                                          aPostId,
                                          aCategories)
{
  this._logger.error("setPostCategories: not implemented!");
  throw CR.NS_ERROR_NOT_IMPLEMENTED;
};


/*************************************************************************
 * flockLJService: Private methods
 *************************************************************************/

flockLJService.prototype._init =
function flockLJService__init() {
  FlockSvcUtils.getLogger(this).debug(".init()");

  var evtID = this._profiler.profileEventStart("lj-init");

  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.");
    this.deleteCategories();
    return;
  }

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

  this.account_root = this._coop.accounts_root;

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

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


/*************************************************************************
 * Component: flockLJAccount
 *************************************************************************/

function flockLJAccount() {
  FlockSvcUtils.getLogger(this).init(CLASS_SHORT_NAME + "Account");
  this._logger.info(".init()");
  FlockSvcUtils.getACUtils(this);
  FlockSvcUtils.getCoop(this);

  var wsa = FlockSvcUtils.flockIWebServiceAccount;
  wsa.addDefaultMethod(this, "getService");
  wsa.addDefaultMethod(this, "login");
  wsa.addDefaultMethod(this, "logout");
  wsa.addDefaultMethod(this, "keep");
  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 LJ Account: XPCOM Component Creation
 **************************************************************************/

flockLJAccount.prototype = new FlockXPCOMUtils.genericComponent(
  CLASS_NAME + " Account",
  "",
  "",
  flockLJAccount,
  0,
  [
    CI.flockIWebServiceAccount,
    CI.flockIBlogAccount
  ]
);

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

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

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