/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

const {classes: Cc,
       interfaces: Ci,
       utils: Cu,
       results: Cr} = Components;

Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");

const PREF_FXA_ENABLED = "identity.fxaccounts.enabled";
const FXA_PERMISSION = "firefox-accounts";

// This is the parent process corresponding to nsDOMIdentity.
this.EXPORTED_SYMBOLS = ["DOMIdentity"];

XPCOMUtils.defineLazyModuleGetter(this, "objectCopy",
                                  "resource://gre/modules/identity/IdentityUtils.jsm");

/* jshint ignore:start */
XPCOMUtils.defineLazyModuleGetter(this, "IdentityService",
//@line 27 "/Users/t_mrc-ct/Projects/Tenfourbird/repos/comm-esr38/mozilla/dom/identity/DOMIdentity.jsm"
                                  "resource://gre/modules/identity/MinimalIdentity.jsm");
//@line 31 "/Users/t_mrc-ct/Projects/Tenfourbird/repos/comm-esr38/mozilla/dom/identity/DOMIdentity.jsm"
/* jshint ignore:end */

XPCOMUtils.defineLazyModuleGetter(this, "FirefoxAccounts",
                                  "resource://gre/modules/identity/FirefoxAccounts.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "makeMessageObject",
                                  "resource://gre/modules/identity/IdentityUtils.jsm");

XPCOMUtils.defineLazyModuleGetter(this,
                                  "Logger",
                                  "resource://gre/modules/identity/LogUtils.jsm");

XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
                                   "@mozilla.org/parentprocessmessagemanager;1",
                                   "nsIMessageListenerManager");

XPCOMUtils.defineLazyServiceGetter(this, "permissionManager",
                                   "@mozilla.org/permissionmanager;1",
                                   "nsIPermissionManager");

function log(...aMessageArgs) {
  Logger.log.apply(Logger, ["DOMIdentity"].concat(aMessageArgs));
}

function IDDOMMessage(aOptions) {
  objectCopy(aOptions, this);
}

function _sendAsyncMessage(identifier, message) {
  if (this._mm) {
    try {
      this._mm.sendAsyncMessage(identifier, message);
    } catch(err) {
      // We may receive a NS_ERROR_NOT_INITIALIZED if the target window has
      // been closed.  This can legitimately happen if an app has been killed
      // while we are in the midst of a sign-in flow.
      if (err.result == Cr.NS_ERROR_NOT_INITIALIZED) {
        log("Cannot sendAsyncMessage because the recipient frame has closed");
        return;
      }
      log("ERROR: sendAsyncMessage: " + err);
    }
  }
};

function IDPProvisioningContext(aID, aOrigin, aTargetMM) {
  this._id = aID;
  this._origin = aOrigin;
  this._mm = aTargetMM;
}

IDPProvisioningContext.prototype = {
  get id() this._id,
  get origin() this._origin,

  sendAsyncMessage: _sendAsyncMessage,

  doBeginProvisioningCallback: function IDPPC_doBeginProvCB(aID, aCertDuration) {
    let message = new IDDOMMessage({id: this.id});
    message.identity = aID;
    message.certDuration = aCertDuration;
    this.sendAsyncMessage("Identity:IDP:CallBeginProvisioningCallback",
                          message);
  },

  doGenKeyPairCallback: function IDPPC_doGenKeyPairCallback(aPublicKey) {
    log("doGenKeyPairCallback");
    let message = new IDDOMMessage({id: this.id});
    message.publicKey = aPublicKey;
    this.sendAsyncMessage("Identity:IDP:CallGenKeyPairCallback", message);
  },

  doError: function(msg) {
    log("Provisioning ERROR: " + msg);
  }
};

function IDPAuthenticationContext(aID, aOrigin, aTargetMM) {
  this._id = aID;
  this._origin = aOrigin;
  this._mm = aTargetMM;
}

IDPAuthenticationContext.prototype = {
  get id() this._id,
  get origin() this._origin,

  sendAsyncMessage: _sendAsyncMessage,

  doBeginAuthenticationCallback: function IDPAC_doBeginAuthCB(aIdentity) {
    let message = new IDDOMMessage({id: this.id});
    message.identity = aIdentity;
    this.sendAsyncMessage("Identity:IDP:CallBeginAuthenticationCallback",
                          message);
  },

  doError: function IDPAC_doError(msg) {
    log("Authentication ERROR: " + msg);
  }
};

function RPWatchContext(aOptions, aTargetMM, aPrincipal) {
  objectCopy(aOptions, this);

  // id and origin are required
  if (! (this.id && this.origin)) {
    throw new Error("id and origin are required for RP watch context");
  }

  this.principal = aPrincipal;

  // default for no loggedInUser is undefined, not null
  this.loggedInUser = aOptions.loggedInUser;

  // Maybe internal.  For hosted b2g identity shim.
  this._internal = aOptions._internal;

  this._mm = aTargetMM;
}

RPWatchContext.prototype = {
  sendAsyncMessage: _sendAsyncMessage,

  doLogin: function RPWatchContext_onlogin(aAssertion, aMaybeInternalParams) {
    log("doLogin: " + this.id);
    let message = new IDDOMMessage({id: this.id, assertion: aAssertion});
    if (aMaybeInternalParams) {
      message._internalParams = aMaybeInternalParams;
    }
    this.sendAsyncMessage("Identity:RP:Watch:OnLogin", message);
  },

  doLogout: function RPWatchContext_onlogout() {
    log("doLogout: " + this.id);
    let message = new IDDOMMessage({id: this.id});
    this.sendAsyncMessage("Identity:RP:Watch:OnLogout", message);
  },

  doReady: function RPWatchContext_onready() {
    log("doReady: " + this.id);
    let message = new IDDOMMessage({id: this.id});
    this.sendAsyncMessage("Identity:RP:Watch:OnReady", message);
  },

  doCancel: function RPWatchContext_oncancel() {
    log("doCancel: " + this.id);
    let message = new IDDOMMessage({id: this.id});
    this.sendAsyncMessage("Identity:RP:Watch:OnCancel", message);
  },

  doError: function RPWatchContext_onerror(aMessage) {
    log("doError: " + this.id + ": " + JSON.stringify(aMessage));
    let message = new IDDOMMessage({id: this.id, message: aMessage});
    this.sendAsyncMessage("Identity:RP:Watch:OnError", message);
  }
};

this.DOMIdentity = {
  /*
   * When relying parties (RPs) invoke the watch() method, they can request
   * to use Firefox Accounts as their auth service or BrowserID (the default).
   * For each RP, we create an RPWatchContext to store the parameters given to
   * watch(), and to provide hooks to invoke the onlogin(), onlogout(), etc.
   * callbacks held in the nsDOMIdentity state.
   *
   * The serviceContexts map associates the window ID of the RP with the
   * context object.  The mmContexts map associates a message manager with a
   * window ID.  We use the mmContexts map when child-process-shutdown is
   * observed, and all we have is a message manager to identify the window in
   * question.
   */
  _serviceContexts: new Map(),
  _mmContexts: new Map(),

  /*
   * Mockable, for testing
   */
  _mockIdentityService: null,
  get IdentityService() {
    if (this._mockIdentityService) {
      log("Using a mocked identity service");
      return this._mockIdentityService;
    }
    return IdentityService;
  },

  /*
   * Create a new RPWatchContext, and update the context maps.
   */
  newContext: function(message, targetMM, principal) {
    let context = new RPWatchContext(message, targetMM, principal);
    this._serviceContexts.set(message.id, context);
    this._mmContexts.set(targetMM, message.id);
    return context;
  },

  /*
   * Get the identity service used for an RP.
   *
   * @object message
   *         A message received from an RP.  Will include the id of the window
   *         whence the message originated.
   *
   * Returns FirefoxAccounts or IdentityService
   */
  getService: function(message) {
    if (!this._serviceContexts.has(message.id)) {
      log("ERROR: getService called before newContext for " + message.id);
      return null;
    }

    let context = this._serviceContexts.get(message.id);
    if (context.wantIssuer == "firefox-accounts") {
      if (Services.prefs.getPrefType(PREF_FXA_ENABLED) === Ci.nsIPrefBranch.PREF_BOOL
          && Services.prefs.getBoolPref(PREF_FXA_ENABLED)) {
        return FirefoxAccounts;
      }
      log("WARNING: Firefox Accounts is not enabled; Defaulting to BrowserID");
    }
    return this.IdentityService;
  },

  /*
   * Get the RPWatchContext object for a given message manager.
   */
  getContextForMM: function(targetMM) {
    return this._serviceContexts.get(this._mmContexts.get(targetMM));
  },

  hasContextForMM: function(targetMM) {
    return this._mmContexts.has(targetMM);
  },

  /*
   * Delete the RPWatchContext object for a given message manager.  Removes the
   * mapping both from _serviceContexts and _mmContexts.
   */
  deleteContextForMM: function(targetMM) {
    this._serviceContexts.delete(this._mmContexts.get(targetMM));
    this._mmContexts.delete(targetMM);
  },

  hasPermission: function(aMessage) {
    // We only check that the firefox accounts permission is present in the
    // manifest.
    if (aMessage.json && aMessage.json.wantIssuer == "firefox-accounts") {
      if (!aMessage.principal) {
        return false;
      }
      let secMan = Cc["@mozilla.org/scriptsecuritymanager;1"]
                     .getService(Ci.nsIScriptSecurityManager);
      let uri = Services.io.newURI(aMessage.principal.origin, null, null);
      let principal = secMan.getAppCodebasePrincipal(uri,
        aMessage.principal.appId, aMessage.principal.isInBrowserElement);

      let permission =
        permissionManager.testPermissionFromPrincipal(principal,
                                                      FXA_PERMISSION);
      return permission != Ci.nsIPermissionManager.UNKNOWN_ACTION &&
             permission != Ci.nsIPermissionManager.DENY_ACTION;
    }
    return true;
  },

  // nsIMessageListener
  receiveMessage: function DOMIdentity_receiveMessage(aMessage) {
    let msg = aMessage.json;

    // Target is the frame message manager that called us and is
    // used to send replies back to the proper window.
    let targetMM = aMessage.target;

    if (!this.hasPermission(aMessage)) {
      throw new Error("PERMISSION_DENIED");
    }

    switch (aMessage.name) {
      // RP
      case "Identity:RP:Watch":
        this._watch(msg, targetMM, aMessage.principal);
        break;
      case "Identity:RP:Unwatch":
        this._unwatch(msg, targetMM);
        break;
      case "Identity:RP:Request":
        this._request(msg);
        break;
      case "Identity:RP:Logout":
        this._logout(msg);
        break;
      // IDP
      case "Identity:IDP:BeginProvisioning":
        this._beginProvisioning(msg, targetMM);
        break;
      case "Identity:IDP:GenKeyPair":
        this._genKeyPair(msg);
        break;
      case "Identity:IDP:RegisterCertificate":
        this._registerCertificate(msg);
        break;
      case "Identity:IDP:ProvisioningFailure":
        this._provisioningFailure(msg);
        break;
      case "Identity:IDP:BeginAuthentication":
        this._beginAuthentication(msg, targetMM);
        break;
      case "Identity:IDP:CompleteAuthentication":
        this._completeAuthentication(msg);
        break;
      case "Identity:IDP:AuthenticationFailure":
        this._authenticationFailure(msg);
        break;
      case "child-process-shutdown":
        // we receive child-process-shutdown if the appliction crashes,
        // including if it is crashed by the OS (killed for out-of-memory,
        // for example)
        this._childProcessShutdown(targetMM);
        break;
    }
  },

  // nsIObserver
  observe: function DOMIdentity_observe(aSubject, aTopic, aData) {
    switch (aTopic) {
      case "xpcom-shutdown":
        this._unsubscribeListeners();
        Services.obs.removeObserver(this, "xpcom-shutdown");
        Services.ww.unregisterNotification(this);
        break;
    }
  },

  messages: ["Identity:RP:Watch", "Identity:RP:Request", "Identity:RP:Logout",
             "Identity:IDP:BeginProvisioning", "Identity:IDP:ProvisioningFailure",
             "Identity:IDP:RegisterCertificate", "Identity:IDP:GenKeyPair",
             "Identity:IDP:BeginAuthentication",
             "Identity:IDP:CompleteAuthentication",
             "Identity:IDP:AuthenticationFailure",
             "Identity:RP:Unwatch",
             "child-process-shutdown"],

  // Private.
  _init: function DOMIdentity__init() {
    Services.ww.registerNotification(this);
    Services.obs.addObserver(this, "xpcom-shutdown", false);
    this._subscribeListeners();
  },

  _subscribeListeners: function DOMIdentity__subscribeListeners() {
    if (!ppmm) {
      return;
    }
    for (let message of this.messages) {
      ppmm.addMessageListener(message, this);
    }
  },

  _unsubscribeListeners: function DOMIdentity__unsubscribeListeners() {
    for (let message of this.messages) {
      ppmm.removeMessageListener(message, this);
    }
    ppmm = null;
  },

  _watch: function DOMIdentity__watch(message, targetMM, principal) {
    log("DOMIdentity__watch: " + message.id + " - " + principal);
    let context = this.newContext(message, targetMM, principal);
    this.getService(message).RP.watch(context);
  },

  _unwatch: function DOMIdentity_unwatch(message, targetMM) {
    log("DOMIDentity__unwatch: " + message.id);
    // If watch failed for some reason (e.g., exception thrown because RP did
    // not have the right callbacks, we don't want unwatch to throw, because it
    // will break the process of releasing the page's resources and leak
    // memory.
    let service = this.getService(message);
    if (service && service.RP) {
      service.RP.unwatch(message.id, targetMM);
      this.deleteContextForMM(targetMM);
      return;
    }
    log("Can't find a service to unwatch() for " + message.id);
  },

  _request: function DOMIdentity__request(message) {
    let service = this.getService(message);
    if (service && service.RP) {
      service.RP.request(message.id, message);
      return;
    }
    log("No context in which to call request(); Did you call watch() first?");
  },

  _logout: function DOMIdentity__logout(message) {
    let service = this.getService(message);
    if (service && service.RP) {
      service.RP.logout(message.id, message.origin, message);
      return;
    }
    log("No context in which to call logout(); Did you call watch() first?");
  },

  _childProcessShutdown: function DOMIdentity__childProcessShutdown(targetMM) {
    if (!this.hasContextForMM(targetMM)) {
      return;
    }

    let service = this.getContextForMM(targetMM);
    if (service && service.RP) {
      service.RP.childProcessShutdown(targetMM);
    }

    this.deleteContextForMM(targetMM);

    let options = makeMessageObject({messageManager: targetMM, id: null, origin: null});
    Services.obs.notifyObservers({wrappedJSObject: options}, "identity-child-process-shutdown", null);
  },

  _beginProvisioning: function DOMIdentity__beginProvisioning(message, targetMM) {
    let context = new IDPProvisioningContext(message.id, message.origin,
                                             targetMM);
    this.getService(message).IDP.beginProvisioning(context);
  },

  _genKeyPair: function DOMIdentity__genKeyPair(message) {
    this.getService(message).IDP.genKeyPair(message.id);
  },

  _registerCertificate: function DOMIdentity__registerCertificate(message) {
    this.getService(message).IDP.registerCertificate(message.id, message.cert);
  },

  _provisioningFailure: function DOMIdentity__provisioningFailure(message) {
    this.getService(message).IDP.raiseProvisioningFailure(message.id, message.reason);
  },

  _beginAuthentication: function DOMIdentity__beginAuthentication(message, targetMM) {
    let context = new IDPAuthenticationContext(message.id, message.origin,
                                               targetMM);
    this.getService(message).IDP.beginAuthentication(context);
  },

  _completeAuthentication: function DOMIdentity__completeAuthentication(message) {
    this.getService(message).IDP.completeAuthentication(message.id);
  },

  _authenticationFailure: function DOMIdentity__authenticationFailure(message) {
    this.getService(message).IDP.cancelAuthentication(message.id);
  }
};

// Object is initialized by nsIDService.js
