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

this.EXPORTED_SYMBOLS = ["RemoteAddonsChild"];

const Ci = Components.interfaces;
const Cc = Components.classes;
const Cu = Components.utils;
const Cr = Components.results;

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

XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
                                  "resource://gre/modules/BrowserUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Prefetcher",
                                  "resource://gre/modules/Prefetcher.jsm");

XPCOMUtils.defineLazyServiceGetter(this, "SystemPrincipal",
                                   "@mozilla.org/systemprincipal;1", "nsIPrincipal");

XPCOMUtils.defineLazyServiceGetter(this, "contentSecManager",
                                   "@mozilla.org/contentsecuritymanager;1",
                                   "nsIContentSecurityManager");

// Similar to Python. Returns dict[key] if it exists. Otherwise,
// sets dict[key] to default_ and returns default_.
function setDefault(dict, key, default_)
{
  if (key in dict) {
    return dict[key];
  }
  dict[key] = default_;
  return default_;
}

// This code keeps track of a set of paths of the form [component_1,
// ..., component_n]. The components can be strings or booleans. The
// child is notified whenever a path is added or removed, and new
// children can request the current set of paths. The purpose is to
// keep track of all the observers and events that the child should
// monitor for the parent.
//
// In the child, clients can watch for changes to all paths that start
// with a given component.
var NotificationTracker = {
  init: function() {
    let cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
               .getService(Ci.nsISyncMessageSender);
    cpmm.addMessageListener("Addons:ChangeNotification", this);
    this._paths = cpmm.initialProcessData.remoteAddonsNotificationPaths;
    this._registered = new Map();
    this._watchers = {};
  },

  receiveMessage: function(msg) {
    let path = msg.data.path;
    let count = msg.data.count;

    let tracked = this._paths;
    for (let component of path) {
      tracked = setDefault(tracked, component, {});
    }

    tracked._count = count;

    if (this._watchers[path[0]]) {
      for (let watcher of this._watchers[path[0]]) {
        this.runCallback(watcher, path, count);
      }
    }
  },

  runCallback: function(watcher, path, count) {
    let pathString = path.join("/");
    let registeredSet = this._registered.get(watcher);
    let registered = registeredSet.has(pathString);
    if (count && !registered) {
      watcher.track(path, true);
      registeredSet.add(pathString);
    } else if (!count && registered) {
      watcher.track(path, false);
      registeredSet.delete(pathString);
    }
  },

  findPaths: function(prefix) {
    if (!this._paths) {
      return [];
    }

    let tracked = this._paths;
    for (let component of prefix) {
      tracked = setDefault(tracked, component, {});
    }

    let result = [];
    let enumerate = (tracked, curPath) => {
      for (let component in tracked) {
        if (component == "_count") {
          result.push([curPath, tracked._count]);
        } else {
          let path = curPath.slice();
          if (component === "true") {
            component = true;
          } else if (component === "false") {
            component = false;
          }
          path.push(component);
          enumerate(tracked[component], path);
        }
      }
    }
    enumerate(tracked, prefix);

    return result;
  },

  findSuffixes: function(prefix) {
    let paths = this.findPaths(prefix);
    return paths.map(([path, count]) => path[path.length - 1]);
  },

  watch: function(component1, watcher) {
    setDefault(this._watchers, component1, []).push(watcher);
    this._registered.set(watcher, new Set());

    let paths = this.findPaths([component1]);
    for (let [path, count] of paths) {
      this.runCallback(watcher, path, count);
    }
  },

  unwatch: function(component1, watcher) {
    let watchers = this._watchers[component1];
    let index = watchers.lastIndexOf(watcher);
    if (index > -1) {
      watchers.splice(index, 1);
    }

    this._registered.delete(watcher);
  },

  getCount(component1) {
    return this.findPaths([component1]).length;
  },
};

// This code registers an nsIContentPolicy in the child process. When
// it runs, it notifies the parent that it needs to run its own
// nsIContentPolicy list. If any policy in the parent rejects a
// resource load, that answer is returned to the child.
var ContentPolicyChild = {
  _classDescription: "Addon shim content policy",
  _classID: Components.ID("6e869130-635c-11e2-bcfd-0800200c9a66"),
  _contractID: "@mozilla.org/addon-child/policy;1",

  init: function() {
    let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
    registrar.registerFactory(this._classID, this._classDescription, this._contractID, this);

    NotificationTracker.watch("content-policy", this);
  },

  QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPolicy, Ci.nsIObserver,
                                         Ci.nsIChannelEventSink, Ci.nsIFactory,
                                         Ci.nsISupportsWeakReference]),

  track: function(path, register) {
    let catMan = Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager);
    if (register) {
      catMan.addCategoryEntry("content-policy", this._contractID, this._contractID, false, true);
    } else {
      catMan.deleteCategoryEntry("content-policy", this._contractID, false);
    }
  },

  shouldLoad: function(contentType, contentLocation, requestOrigin,
                       node, mimeTypeGuess, extra, requestPrincipal) {
    let addons = NotificationTracker.findSuffixes(["content-policy"]);
    let [prefetched, cpows] = Prefetcher.prefetch("ContentPolicy.shouldLoad",
                                                  addons, {InitNode: node});
    cpows.node = node;

    let cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
               .getService(Ci.nsISyncMessageSender);
    let rval = cpmm.sendRpcMessage("Addons:ContentPolicy:Run", {
      contentType: contentType,
      contentLocation: contentLocation.spec,
      requestOrigin: requestOrigin ? requestOrigin.spec : null,
      mimeTypeGuess: mimeTypeGuess,
      requestPrincipal: requestPrincipal,
      prefetched: prefetched,
    }, cpows);
    if (rval.length != 1) {
      return Ci.nsIContentPolicy.ACCEPT;
    }

    return rval[0];
  },

  shouldProcess: function(contentType, contentLocation, requestOrigin, insecNode, mimeType, extra) {
    return Ci.nsIContentPolicy.ACCEPT;
  },

  createInstance: function(outer, iid) {
    if (outer) {
      throw Cr.NS_ERROR_NO_AGGREGATION;
    }
    return this.QueryInterface(iid);
  },
};

// This is a shim channel whose only purpose is to return some string
// data from an about: protocol handler.
function AboutProtocolChannel(uri, contractID, loadInfo)
{
  this.URI = uri;
  this.originalURI = uri;
  this._contractID = contractID;
  this._loadingPrincipal = loadInfo.loadingPrincipal;
  this._securityFlags = loadInfo.securityFlags;
  this._contentPolicyType = loadInfo.externalContentPolicyType;
}

AboutProtocolChannel.prototype = {
  contentCharset: "utf-8",
  contentLength: 0,
  owner: SystemPrincipal,
  securityInfo: null,
  notificationCallbacks: null,
  loadFlags: 0,
  loadGroup: null,
  name: null,
  status: Cr.NS_OK,

  asyncOpen: function(listener, context) {
    // Ask the parent to synchronously read all the data from the channel.
    let cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
               .getService(Ci.nsISyncMessageSender);
    let rval = cpmm.sendRpcMessage("Addons:AboutProtocol:OpenChannel", {
      uri: this.URI.spec,
      contractID: this._contractID,
      loadingPrincipal: this._loadingPrincipal,
      securityFlags: this._securityFlags,
      contentPolicyType: this._contentPolicyType
    }, {
      notificationCallbacks: this.notificationCallbacks,
      loadGroupNotificationCallbacks: this.loadGroup ? this.loadGroup.notificationCallbacks : null,
    });

    if (rval.length != 1) {
      throw Cr.NS_ERROR_FAILURE;
    }

    let {data, contentType} = rval[0];
    this.contentType = contentType;

    // Return the data via an nsIStringInputStream.
    let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
    stream.setData(data, data.length);

    let runnable = {
      run: () => {
        try {
          listener.onStartRequest(this, context);
        } catch (e) {}
        try {
          listener.onDataAvailable(this, context, stream, 0, stream.available());
        } catch (e) {}
        try {
          listener.onStopRequest(this, context, Cr.NS_OK);
        } catch (e) {}
      }
    };
    Services.tm.currentThread.dispatch(runnable, Ci.nsIEventTarget.DISPATCH_NORMAL);
  },

  asyncOpen2: function(listener) {
    // throws an error if security checks fail
    var outListener = contentSecManager.performSecurityCheck(this, listener);
    this.asyncOpen(outListener, null);
  },

  open: function() {
    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
  },

  open2: function() {
    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
  },

  isPending: function() {
    return false;
  },

  cancel: function() {
    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
  },

  suspend: function() {
    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
  },

  resume: function() {
    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
  },

  QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannel, Ci.nsIRequest])
};

// This shim protocol handler is used when content fetches an about: URL.
function AboutProtocolInstance(contractID)
{
  this._contractID = contractID;
  this._uriFlags = undefined;
}

AboutProtocolInstance.prototype = {
  createInstance: function(outer, iid) {
    if (outer != null) {
      throw Cr.NS_ERROR_NO_AGGREGATION;
    }

    return this.QueryInterface(iid);
  },

  getURIFlags: function(uri) {
    // Cache the result to avoid the extra IPC.
    if (this._uriFlags !== undefined) {
      return this._uriFlags;
    }

    let cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
               .getService(Ci.nsISyncMessageSender);

    let rval = cpmm.sendRpcMessage("Addons:AboutProtocol:GetURIFlags", {
      uri: uri.spec,
      contractID: this._contractID
    });

    if (rval.length != 1) {
      throw Cr.NS_ERROR_FAILURE;
    }

    this._uriFlags = rval[0];
    return this._uriFlags;
  },

  // We take some shortcuts here. Ideally, we would return a CPOW that
  // wraps the add-on's nsIChannel. However, many of the methods
  // related to nsIChannel are marked [noscript], so they're not
  // available to CPOWs. Consequently, we return a shim channel that,
  // when opened, asks the parent to open the channel and read out all
  // the data.
  newChannel: function(uri, loadInfo) {
    return new AboutProtocolChannel(uri, this._contractID, loadInfo);
  },

  QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory, Ci.nsIAboutModule])
};

var AboutProtocolChild = {
  _classDescription: "Addon shim about: protocol handler",

  init: function() {
    // Maps contractIDs to instances
    this._instances = new Map();
    // Maps contractIDs to classIDs
    this._classIDs = new Map();
    NotificationTracker.watch("about-protocol", this);
  },

  track: function(path, register) {
    let contractID = path[1];
    let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
    if (register) {
      let instance = new AboutProtocolInstance(contractID);
      let classID = Cc["@mozilla.org/uuid-generator;1"]
                      .getService(Ci.nsIUUIDGenerator)
                      .generateUUID();

      this._instances.set(contractID, instance);
      this._classIDs.set(contractID, classID);
      registrar.registerFactory(classID, this._classDescription, contractID, instance);
    } else {
      let instance = this._instances.get(contractID);
      let classID = this._classIDs.get(contractID);
      registrar.unregisterFactory(classID, instance);
      this._instances.delete(contractID);
      this._classIDs.delete(contractID);
    }
  },
};

// This code registers observers in the child whenever an add-on in
// the parent asks for notifications on the given topic.
var ObserverChild = {
  init: function() {
    NotificationTracker.watch("observer", this);
  },

  track: function(path, register) {
    let topic = path[1];
    if (register) {
      Services.obs.addObserver(this, topic, false);
    } else {
      Services.obs.removeObserver(this, topic);
    }
  },

  observe: function(subject, topic, data) {
    let cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
               .getService(Ci.nsISyncMessageSender);
    cpmm.sendRpcMessage("Addons:Observer:Run", {}, {
      topic: topic,
      subject: subject,
      data: data
    });
  }
};

// There is one of these objects per browser tab in the child. When an
// add-on in the parent listens for an event, this child object
// listens for that event in the child.
function EventTargetChild(childGlobal)
{
  this._childGlobal = childGlobal;
  this.capturingHandler = (event) => this.handleEvent(true, event);
  this.nonCapturingHandler = (event) => this.handleEvent(false, event);
  NotificationTracker.watch("event", this);
}

EventTargetChild.prototype = {
  uninit: function() {
    NotificationTracker.unwatch("event", this);
  },

  track: function(path, register) {
    let eventType = path[1];
    let useCapture = path[2];
    let listener = useCapture ? this.capturingHandler : this.nonCapturingHandler;
    if (register) {
      this._childGlobal.addEventListener(eventType, listener, useCapture, true);
    } else {
      this._childGlobal.removeEventListener(eventType, listener, useCapture);
    }
  },

  handleEvent: function(capturing, event) {
    let addons = NotificationTracker.findSuffixes(["event", event.type, capturing]);
    let [prefetched, cpows] = Prefetcher.prefetch("EventTarget.handleEvent",
                                                  addons,
                                                  {Event: event,
                                                   Window: this._childGlobal.content});
    cpows.event = event;
    cpows.eventTarget = event.target;

    this._childGlobal.sendRpcMessage("Addons:Event:Run",
                                     {type: event.type,
                                      capturing: capturing,
                                      isTrusted: event.isTrusted,
                                      prefetched: prefetched},
                                     cpows);
  }
};

// The parent can create a sandbox to run code in the child
// process. We actually create the sandbox in the child so that the
// code runs there. However, managing the lifetime of these sandboxes
// can be tricky. The parent references these sandboxes using CPOWs,
// which only keep weak references. So we need to create a strong
// reference in the child. For simplicity, we kill off these strong
// references whenever we navigate away from the page for which the
// sandbox was created.
function SandboxChild(chromeGlobal)
{
  this.chromeGlobal = chromeGlobal;
  this.sandboxes = [];
}

SandboxChild.prototype = {
  uninit: function() {
    this.clearSandboxes();
  },

  addListener: function() {
    let webProgress = this.chromeGlobal.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
      .getInterface(Ci.nsIWebProgress);
    webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
  },

  removeListener: function() {
    let webProgress = this.chromeGlobal.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
      .getInterface(Ci.nsIWebProgress);
    webProgress.removeProgressListener(this);
  },

  onLocationChange: function(webProgress, request, location, flags) {
    this.clearSandboxes();
  },

  addSandbox: function(sandbox) {
    if (this.sandboxes.length == 0) {
      this.addListener();
    }
    this.sandboxes.push(sandbox);
  },

  clearSandboxes: function() {
    if (this.sandboxes.length) {
      this.removeListener();
    }
    this.sandboxes = [];
  },

  QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
                                         Ci.nsISupportsWeakReference])
};

var RemoteAddonsChild = {
  _ready: false,

  makeReady: function() {
    let shims = [
      Prefetcher,
      NotificationTracker,
      ContentPolicyChild,
      AboutProtocolChild,
      ObserverChild,
    ];

    for (let shim of shims) {
      try {
        shim.init();
      } catch (e) {
        Cu.reportError(e);
      }
    }
  },

  init: function(global) {

    if (!this._ready) {
      if (!Services.cpmm.initialProcessData.remoteAddonsParentInitted) {
        return null;
      }

      this.makeReady();
      this._ready = true;
    }

    global.sendAsyncMessage("Addons:RegisterGlobal", {}, {global: global});

    let sandboxChild = new SandboxChild(global);
    global.addSandbox = sandboxChild.addSandbox.bind(sandboxChild);

    // Return this so it gets rooted in the content script.
    return [new EventTargetChild(global), sandboxChild];
  },

  uninit: function(perTabShims) {
    for (let shim of perTabShims) {
      try {
        shim.uninit();
      } catch (e) {
        Cu.reportError(e);
      }
    }
  },

  get useSyncWebProgress() {
    return NotificationTracker.getCount("web-progress") > 0;
  },
};
