/*
# 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";

(function(){

let Cc = Components.classes;
let Ci = Components.interfaces;
let Cu = Components.utils;

const MSG_INSTALL_ENABLED  = "WebInstallerIsInstallEnabled";
const MSG_INSTALL_ADDONS   = "WebInstallerInstallAddonsFromWebpage";
const MSG_INSTALL_CALLBACK = "WebInstallerInstallCallback";
const MSG_JAR_FLUSH        = "AddonJarFlush";

var gIoService = Components.classes["@mozilla.org/network/io-service;1"]
                           .getService(Components.interfaces.nsIIOService);

function createInstallTrigger(window) {
  let chromeObject = {
    principal: window.document.nodePrincipal,
    url: window.document.documentURIObject,

    __exposedProps__: {
      SKIN: "r",
      LOCALE: "r",
      CONTENT: "r",
      PACKAGE: "r",
      enabled: "r",
      updateEnabled: "r",
      install: "r",
      installChrome: "r",
      startSoftwareUpdate: "r"
    },

    // == Public interface ==

    SKIN: Ci.amIInstallTrigger.SKIN,
    LOCALE: Ci.amIInstallTrigger.LOCALE,
    CONTENT: Ci.amIInstallTrigger.CONTENT,
    PACKAGE: Ci.amIInstallTrigger.PACKAGE,

    /**
     * @see amIInstallTriggerInstaller.idl
     */
    enabled: function() {
      return sendSyncMessage(MSG_INSTALL_ENABLED, {
        mimetype: "application/x-xpinstall", referer: this.url.spec
      })[0];
    },

    /**
     * @see amIInstallTriggerInstaller.idl
     */
    updateEnabled: function() {
      return this.enabled();
    },

    /**
     * @see amIInstallTriggerInstaller.idl
     */
    install: function(aArgs, aCallback) {
      if (!aArgs || typeof aArgs != "object")
        throw new Error("Incorrect arguments passed to InstallTrigger.install()");

      var params = {
        installerId: this.installerId,
        mimetype: "application/x-xpinstall",
        referer: this.url.spec,
        uris: [],
        hashes: [],
        names: [],
        icons: [],
      };

      for (var name in aArgs) {
        var item = aArgs[name];
        if (typeof item === 'string') {
          item = { URL: item };
        } else if (!("URL" in item) || item.URL === undefined) {
          throw new Error("Missing URL property for '" + name + "'");
        }

        // Resolve and validate urls
        var url = this.resolveURL(item.URL);
        if (!this.checkLoadURIFromScript(url))
          throw new Error("insufficient permissions to install: " + url);

        var iconUrl = null;
        if ("IconURL" in item && item.IconURL !== undefined) {
          iconUrl = this.resolveURL(item.IconURL);
          if (!this.checkLoadURIFromScript(iconUrl)) {
            iconUrl = null; // If page can't load the icon, just ignore it
          }
        }
        params.uris.push(url.spec);
        params.hashes.push("Hash" in item ? item.Hash : null);
        params.names.push(name);
        params.icons.push(iconUrl ? iconUrl.spec : null);
      }
      // Add callback Id, done here, so only if we actually got here
      params.callbackId = manager.addCallback(aCallback, params.uris);
      // Send message
      return sendSyncMessage(MSG_INSTALL_ADDONS, params)[0];
    },

    /**
     * @see amIInstallTriggerInstaller.idl
     */
    startSoftwareUpdate: function(aUrl, aFlags) {
      var url = gIoService.newURI(aUrl, null, null)
                          .QueryInterface(Ci.nsIURL).filename;
      var object = {};
      object[url] = { "URL": aUrl };
      return this.install(object);
    },

    /**
     * @see amIInstallTriggerInstaller.idl
     */
    installChrome: function(aType, aUrl, aSkin) {
      return this.startSoftwareUpdate(aUrl);
    },

    /**
     * Resolves a URL in the context of our current window. We need to do
     * this before sending URLs to the parent process.
     *
     * @param  aUrl
     *         The url to resolve.
     *
     * @return A resolved, absolute nsURI object.
     */
    resolveURL: function(aUrl) {
      return gIoService.newURI(aUrl, null, this.url);
    },

    /**
     * @see amInstallTrigger.cpp
     * TODO: When e10s lands on m-c, consider removing amInstallTrigger.cpp
     *       See bug 571166
     */
    checkLoadURIFromScript: function(aUri) {
      var secman = Cc["@mozilla.org/scriptsecuritymanager;1"].
                   getService(Ci.nsIScriptSecurityManager);
      try {
        secman.checkLoadURIWithPrincipal(this.principal, aUri,
          Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
        return true;
      }
      catch(e) {
        return false;
      }
    }
  };

  let obj = Cu.createObjectIn(window);
  function genPropDesc(fun) {
    return { enumerable: true, configurable: true, writable: true,
             value: chromeObject[fun].bind(chromeObject) };
  }
  const properties = {
    'enabled': genPropDesc('enabled'),
    'updateEnabled': genPropDesc('updateEnabled'),
    'install': genPropDesc('install'),
    'installChrome': genPropDesc('installChrome'),
    'startSoftwareUpdate': genPropDesc('startSoftwareUpdate')
  };

  Object.defineProperties(obj, properties);

  Cu.makeObjectPropsNormal(obj);

  obj.SKIN = chromeObject.SKIN;
  obj.LOCALE = chromeObject.LOCALE;
  obj.CONTENT = chromeObject.CONTENT;
  obj.PACKAGE = chromeObject.PACKAGE;

  return obj;
};

/**
 * Child part of InstallTrigger e10s handling.
 *
 * Sets up InstallTrigger for newly-created windows,
 * that will relay messages for InstallTrigger
 * activity. We also process the parameters for
 * the InstallTrigger to proper parameters for
 * amIWebInstaller.
 */
function InstallTriggerManager() {
  this.callbacks = {};

  addMessageListener(MSG_INSTALL_CALLBACK, this);

  try {
    // only if we live in a child process...
    if (Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).processType !== Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
      // ... propagate JAR cache flush notifications across process boundaries
      addMessageListener(MSG_JAR_FLUSH, function(msg) {
        let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
        file.initWithPath(msg.json);
        Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService)
          .notifyObservers(file, "flush-cache-entry", null);
      });
    }
  } catch(e) {
    Cu.reportError(e);
  }
    
  addEventListener("DOMWindowCreated", this, false);

  var self = this;
  addEventListener("unload", function() {
    // Clean up all references, to help gc work quickly
    self.callbacks = null;
  }, false);
}

InstallTriggerManager.prototype = {
  handleEvent: function handleEvent(aEvent) {
    var window = aEvent.target.defaultView;

    window.wrappedJSObject.__defineGetter__("InstallTrigger", function() {
      // We do this in a getter, so that we create these objects
      // only on demand (this is a potential concern, since
      // otherwise we might add one per iframe, and keep them
      // alive for as long as the tab is alive).

      delete window.wrappedJSObject.InstallTrigger;
      var installTrigger = createInstallTrigger(window);
      window.wrappedJSObject.InstallTrigger = installTrigger;
      return installTrigger;
    });
  },

  /**
   * Adds a callback to the list of callbacks we may receive messages
   * about from the parent process. We save them here; only callback IDs
   * are sent over IPC.
   *
   * @param  callback
   *         The callback function
   * @param  urls
   *         The urls this callback function will receive responses for.
   *         After all the callbacks have arrived, we can forget about the
   *         callback.
   *
   * @return The callback ID, an integer identifying this callback.
   */
  addCallback: function(aCallback, aUrls) {
    if (!aCallback || typeof aCallback != "function")
      return -1;
    var callbackId = 0;
    while (callbackId in this.callbacks)
      callbackId++;
    this.callbacks[callbackId] = {
      callback: aCallback,
      urls: aUrls.slice(0), // Clone the urls for our own use (it lets
                            // us know when no further callbacks will
                            // occur)
    };
    return callbackId;
  },

  /**
   * Receives a message about a callback. Performs the actual callback
   * (for the callback with the ID we are given). When
   * all URLs are exhausted, can free the callbackId and linked stuff.
   *
   * @param  message
   *         The IPC message. Contains the callback ID.
   *
   */
  receiveMessage: function(aMessage) {
    var payload = aMessage.json;
    var callbackId = payload.callbackId;
    var url = payload.url;
    var status = payload.status;
    var callbackObj = this.callbacks[callbackId];
    if (!callbackObj)
      return;
    try {
      callbackObj.callback(url, status);
    }
    catch (e) {
      dump("InstallTrigger callback threw an exception: " + e + "\n");
    }
    callbackObj.urls.splice(callbackObj.urls.indexOf(url), 1);
    if (callbackObj.urls.length == 0)
      this.callbacks[callbackId] = null;
  },
};

var manager = new InstallTriggerManager();

})();
