/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* 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 {Cc, Ci, Cm, Cu, Cr, components} = require("chrome");
const Services = require("Services");
const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");

loader.lazyRequireGetter(this, "NetworkHelper",
                         "devtools/shared/webconsole/network-helper");
loader.lazyRequireGetter(this, "DevToolsUtils",
                         "devtools/shared/DevToolsUtils");
loader.lazyRequireGetter(this, "flags",
                         "devtools/shared/flags");
loader.lazyRequireGetter(this, "DebuggerServer",
                         "devtools/server/main", true);
loader.lazyImporter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");
loader.lazyServiceGetter(this, "gActivityDistributor",
                         "@mozilla.org/network/http-activity-distributor;1",
                         "nsIHttpActivityDistributor");
const {NetworkThrottleManager} = require("devtools/shared/webconsole/throttle");

// Network logging

// The maximum uint32 value.
const PR_UINT32_MAX = 4294967295;

// HTTP status codes.
const HTTP_MOVED_PERMANENTLY = 301;
const HTTP_FOUND = 302;
const HTTP_SEE_OTHER = 303;
const HTTP_TEMPORARY_REDIRECT = 307;

// The maximum number of bytes a NetworkResponseListener can hold: 1 MB
const RESPONSE_BODY_LIMIT = 1048576;
// Exported for testing.
exports.RESPONSE_BODY_LIMIT = RESPONSE_BODY_LIMIT;

/**
 * Check if a given network request should be logged by a network monitor
 * based on the specified filters.
 *
 * @param nsIHttpChannel channel
 *        Request to check.
 * @param filters
 *        NetworkMonitor filters to match against.
 * @return boolean
 *         True if the network request should be logged, false otherwise.
 */
function matchRequest(channel, filters) {
  // Log everything if no filter is specified
  if (!filters.outerWindowID && !filters.window && !filters.appId) {
    return true;
  }

  // Ignore requests from chrome or add-on code when we are monitoring
  // content.
  // TODO: one particular test (browser_styleeditor_fetch-from-cache.js) needs
  // the flags.testing check. We will move to a better way to serve
  // its needs in bug 1167188, where this check should be removed.
  if (!flags.testing && channel.loadInfo &&
      channel.loadInfo.loadingDocument === null &&
      channel.loadInfo.loadingPrincipal ===
      Services.scriptSecurityManager.getSystemPrincipal()) {
    return false;
  }

  if (filters.window) {
    // Since frames support, this.window may not be the top level content
    // frame, so that we can't only compare with win.top.
    let win = NetworkHelper.getWindowForRequest(channel);
    while (win) {
      if (win == filters.window) {
        return true;
      }
      if (win.parent == win) {
        break;
      }
      win = win.parent;
    }
  }

  if (filters.outerWindowID) {
    let topFrame = NetworkHelper.getTopFrameForRequest(channel);
    if (topFrame && topFrame.outerWindowID &&
        topFrame.outerWindowID == filters.outerWindowID) {
      return true;
    }
  }

  if (filters.appId) {
    let appId = NetworkHelper.getAppIdForRequest(channel);
    if (appId && appId == filters.appId) {
      return true;
    }
  }

  return false;
}

/**
 * This is a nsIChannelEventSink implementation that monitors channel redirects and
 * informs the registered StackTraceCollector about the old and new channels.
 */
const SINK_CLASS_DESCRIPTION = "NetworkMonitor Channel Event Sink";
const SINK_CLASS_ID = components.ID("{e89fa076-c845-48a8-8c45-2604729eba1d}");
const SINK_CONTRACT_ID = "@mozilla.org/network/monitor/channeleventsink;1";
const SINK_CATEGORY_NAME = "net-channel-event-sinks";

function ChannelEventSink() {
  this.wrappedJSObject = this;
  this.collectors = new Set();
}

ChannelEventSink.prototype = {
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannelEventSink]),

  registerCollector(collector) {
    this.collectors.add(collector);
  },

  unregisterCollector(collector) {
    this.collectors.delete(collector);

    if (this.collectors.size == 0) {
      ChannelEventSinkFactory.unregister();
    }
  },

  asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
    for (let collector of this.collectors) {
      try {
        collector.onChannelRedirect(oldChannel, newChannel, flags);
      } catch (ex) {
        console.error("StackTraceCollector.onChannelRedirect threw an exception", ex);
      }
    }
    callback.onRedirectVerifyCallback(Cr.NS_OK);
  }
};

const ChannelEventSinkFactory = XPCOMUtils.generateSingletonFactory(ChannelEventSink);

ChannelEventSinkFactory.register = function () {
  const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
  if (registrar.isCIDRegistered(SINK_CLASS_ID)) {
    return;
  }

  registrar.registerFactory(SINK_CLASS_ID,
                            SINK_CLASS_DESCRIPTION,
                            SINK_CONTRACT_ID,
                            ChannelEventSinkFactory);

  XPCOMUtils.categoryManager.addCategoryEntry(SINK_CATEGORY_NAME, SINK_CONTRACT_ID,
    SINK_CONTRACT_ID, false, true);
};

ChannelEventSinkFactory.unregister = function () {
  const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
  registrar.unregisterFactory(SINK_CLASS_ID, ChannelEventSinkFactory);

  XPCOMUtils.categoryManager.deleteCategoryEntry(SINK_CATEGORY_NAME, SINK_CONTRACT_ID,
    false);
};

ChannelEventSinkFactory.getService = function () {
  // Make sure the ChannelEventSink service is registered before accessing it
  ChannelEventSinkFactory.register();

  return Cc[SINK_CONTRACT_ID].getService(Ci.nsIChannelEventSink).wrappedJSObject;
};

function StackTraceCollector(filters) {
  this.filters = filters;
  this.stacktracesById = new Map();
}

StackTraceCollector.prototype = {
  init() {
    Services.obs.addObserver(this, "http-on-opening-request", false);
    ChannelEventSinkFactory.getService().registerCollector(this);
  },

  destroy() {
    Services.obs.removeObserver(this, "http-on-opening-request");
    ChannelEventSinkFactory.getService().unregisterCollector(this);
  },

  _saveStackTrace(channel, stacktrace) {
    this.stacktracesById.set(channel.channelId, stacktrace);
  },

  observe(subject) {
    let channel = subject.QueryInterface(Ci.nsIHttpChannel);

    if (!matchRequest(channel, this.filters)) {
      return;
    }

    // Convert the nsIStackFrame XPCOM objects to a nice JSON that can be
    // passed around through message managers etc.
    let frame = components.stack;
    let stacktrace = [];
    if (frame && frame.caller) {
      frame = frame.caller;
      while (frame) {
        stacktrace.push({
          filename: frame.filename,
          lineNumber: frame.lineNumber,
          columnNumber: frame.columnNumber,
          functionName: frame.name,
          asyncCause: frame.asyncCause,
        });
        frame = frame.caller || frame.asyncCaller;
      }
    }

    this._saveStackTrace(channel, stacktrace);
  },

  onChannelRedirect(oldChannel, newChannel, flags) {
    // We can be called with any nsIChannel, but are interested only in HTTP channels
    try {
      oldChannel.QueryInterface(Ci.nsIHttpChannel);
      newChannel.QueryInterface(Ci.nsIHttpChannel);
    } catch (ex) {
      return;
    }

    let oldId = oldChannel.channelId;
    let stacktrace = this.stacktracesById.get(oldId);
    if (stacktrace) {
      this.stacktracesById.delete(oldId);
      this._saveStackTrace(newChannel, stacktrace);
    }
  },

  getStackTrace(channelId) {
    let trace = this.stacktracesById.get(channelId);
    this.stacktracesById.delete(channelId);
    return trace;
  }
};

exports.StackTraceCollector = StackTraceCollector;

/**
 * The network response listener implements the nsIStreamListener and
 * nsIRequestObserver interfaces. This is used within the NetworkMonitor feature
 * to get the response body of the request.
 *
 * The code is mostly based on code listings from:
 *
 *   http://www.softwareishard.com/blog/firebug/
 *      nsitraceablechannel-intercept-http-traffic/
 *
 * @constructor
 * @param object owner
 *        The response listener owner. This object needs to hold the
 *        |openResponses| object.
 * @param object httpActivity
 *        HttpActivity object associated with this request. See NetworkMonitor
 *        for more information.
 */
function NetworkResponseListener(owner, httpActivity) {
  this.owner = owner;
  this.receivedData = "";
  this.httpActivity = httpActivity;
  this.bodySize = 0;
  // Note that this is really only needed for the non-e10s case.
  // See bug 1309523.
  let channel = this.httpActivity.channel;
  this._wrappedNotificationCallbacks = channel.notificationCallbacks;
  channel.notificationCallbacks = this;
}

NetworkResponseListener.prototype = {
  QueryInterface:
    XPCOMUtils.generateQI([Ci.nsIStreamListener, Ci.nsIInputStreamCallback,
                           Ci.nsIRequestObserver, Ci.nsIInterfaceRequestor,
                           Ci.nsISupports]),

  // nsIInterfaceRequestor implementation

  /**
   * This object implements nsIProgressEventSink, but also needs to forward
   * interface requests to the notification callbacks of other objects.
   */
  getInterface(iid) {
    if (iid.equals(Ci.nsIProgressEventSink)) {
      return this;
    }
    if (this._wrappedNotificationCallbacks) {
      return this._wrappedNotificationCallbacks.getInterface(iid);
    }
    throw Cr.NS_ERROR_NO_INTERFACE;
  },

  /**
   * Forward notifications for interfaces this object implements, in case other
   * objects also implemented them.
   */
  _forwardNotification(iid, method, args) {
    if (!this._wrappedNotificationCallbacks) {
      return;
    }
    try {
      let impl = this._wrappedNotificationCallbacks.getInterface(iid);
      impl[method].apply(impl, args);
    } catch (e) {
      if (e.result != Cr.NS_ERROR_NO_INTERFACE) {
        throw e;
      }
    }
  },

  /**
   * This NetworkResponseListener tracks the NetworkMonitor.openResponses object
   * to find the associated uncached headers.
   * @private
   */
  _foundOpenResponse: false,

  /**
   * If the channel already had notificationCallbacks, hold them here internally
   * so that we can forward getInterface requests to that object.
   */
  _wrappedNotificationCallbacks: null,

  /**
   * The response listener owner.
   */
  owner: null,

  /**
   * The response will be written into the outputStream of this nsIPipe.
   * Both ends of the pipe must be blocking.
   */
  sink: null,

  /**
   * The HttpActivity object associated with this response.
   */
  httpActivity: null,

  /**
   * Stores the received data as a string.
   */
  receivedData: null,

  /**
   * The uncompressed, decoded response body size.
   */
  bodySize: null,

  /**
   * Response body size on the wire, potentially compressed / encoded.
   */
  transferredSize: null,

  /**
   * The nsIRequest we are started for.
   */
  request: null,

  /**
   * Set the async listener for the given nsIAsyncInputStream. This allows us to
   * wait asynchronously for any data coming from the stream.
   *
   * @param nsIAsyncInputStream stream
   *        The input stream from where we are waiting for data to come in.
   * @param nsIInputStreamCallback listener
   *        The input stream callback you want. This is an object that must have
   *        the onInputStreamReady() method. If the argument is null, then the
   *        current callback is removed.
   * @return void
   */
  setAsyncListener: function (stream, listener) {
    // Asynchronously wait for the stream to be readable or closed.
    stream.asyncWait(listener, 0, 0, Services.tm.mainThread);
  },

  /**
   * Stores the received data, if request/response body logging is enabled. It
   * also does limit the number of stored bytes, based on the
   * RESPONSE_BODY_LIMIT constant.
   *
   * Learn more about nsIStreamListener at:
   * https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIStreamListener
   *
   * @param nsIRequest request
   * @param nsISupports context
   * @param nsIInputStream inputStream
   * @param unsigned long offset
   * @param unsigned long count
   */
  onDataAvailable: function (request, context, inputStream, offset, count) {
    this._findOpenResponse();
    let data = NetUtil.readInputStreamToString(inputStream, count);

    this.bodySize += count;

    if (!this.httpActivity.discardResponseBody &&
        this.receivedData.length < RESPONSE_BODY_LIMIT) {
      this.receivedData +=
        NetworkHelper.convertToUnicode(data, request.contentCharset);
    }
  },

  /**
   * See documentation at
   * https://developer.mozilla.org/En/NsIRequestObserver
   *
   * @param nsIRequest request
   * @param nsISupports context
   */
  onStartRequest: function (request) {
    // Converter will call this again, we should just ignore that.
    if (this.request) {
      return;
    }

    this.request = request;
    this._getSecurityInfo();
    this._findOpenResponse();
    // We need to track the offset for the onDataAvailable calls where
    // we pass the data from our pipe to the converter.
    this.offset = 0;

    // In the multi-process mode, the conversion happens on the child
    // side while we can only monitor the channel on the parent
    // side. If the content is gzipped, we have to unzip it
    // ourself. For that we use the stream converter services.  Do not
    // do that for Service workers as they are run in the child
    // process.
    let channel = this.request;
    if (!this.httpActivity.fromServiceWorker &&
        channel instanceof Ci.nsIEncodedChannel &&
        channel.contentEncodings &&
        !channel.applyConversion) {
      let encodingHeader = channel.getResponseHeader("Content-Encoding");
      let scs = Cc["@mozilla.org/streamConverters;1"]
        .getService(Ci.nsIStreamConverterService);
      let encodings = encodingHeader.split(/\s*\t*,\s*\t*/);
      let nextListener = this;
      let acceptedEncodings = ["gzip", "deflate", "br", "x-gzip", "x-deflate"];
      for (let i in encodings) {
        // There can be multiple conversions applied
        let enc = encodings[i].toLowerCase();
        if (acceptedEncodings.indexOf(enc) > -1) {
          this.converter = scs.asyncConvertData(enc, "uncompressed",
                                                nextListener, null);
          nextListener = this.converter;
        }
      }
      if (this.converter) {
        this.converter.onStartRequest(this.request, null);
      }
    }
    // Asynchronously wait for the data coming from the request.
    this.setAsyncListener(this.sink.inputStream, this);
  },

  /**
   * Parse security state of this request and report it to the client.
   */
  _getSecurityInfo: DevToolsUtils.makeInfallible(function () {
    // Many properties of the securityInfo (e.g., the server certificate or HPKP
    // status) are not available in the content process and can't be even touched safely,
    // because their C++ getters trigger assertions. This function is called in content
    // process for synthesized responses from service workers, in the parent otherwise.
    if (Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) {
      return;
    }

    // Take the security information from the original nsIHTTPChannel instead of
    // the nsIRequest received in onStartRequest. If response to this request
    // was a redirect from http to https, the request object seems to contain
    // security info for the https request after redirect.
    let secinfo = this.httpActivity.channel.securityInfo;
    let info = NetworkHelper.parseSecurityInfo(secinfo, this.httpActivity);

    this.httpActivity.owner.addSecurityInfo(info);
  }),

  /**
   * Handle the onStopRequest by closing the sink output stream.
   *
   * For more documentation about nsIRequestObserver go to:
   * https://developer.mozilla.org/En/NsIRequestObserver
   */
  onStopRequest: function () {
    this._findOpenResponse();
    this.sink.outputStream.close();
  },

  // nsIProgressEventSink implementation

  /**
   * Handle progress event as data is transferred.  This is used to record the
   * size on the wire, which may be compressed / encoded.
   */
  onProgress: function (request, context, progress, progressMax) {
    this.transferredSize = progress;
    // Need to forward as well to keep things like Download Manager's progress
    // bar working properly.
    this._forwardNotification(Ci.nsIProgressEventSink, "onProgress", arguments);
  },

  onStatus: function () {
    this._forwardNotification(Ci.nsIProgressEventSink, "onStatus", arguments);
  },

  /**
   * Find the open response object associated to the current request. The
   * NetworkMonitor._httpResponseExaminer() method saves the response headers in
   * NetworkMonitor.openResponses. This method takes the data from the open
   * response object and puts it into the HTTP activity object, then sends it to
   * the remote Web Console instance.
   *
   * @private
   */
  _findOpenResponse: function () {
    if (!this.owner || this._foundOpenResponse) {
      return;
    }

    let openResponse = null;

    for (let id in this.owner.openResponses) {
      let item = this.owner.openResponses[id];
      if (item.channel === this.httpActivity.channel) {
        openResponse = item;
        break;
      }
    }

    if (!openResponse) {
      return;
    }
    this._foundOpenResponse = true;

    delete this.owner.openResponses[openResponse.id];

    this.httpActivity.owner.addResponseHeaders(openResponse.headers);
    this.httpActivity.owner.addResponseCookies(openResponse.cookies);
  },

  /**
   * Clean up the response listener once the response input stream is closed.
   * This is called from onStopRequest() or from onInputStreamReady() when the
   * stream is closed.
   * @return void
   */
  onStreamClose: function () {
    if (!this.httpActivity) {
      return;
    }
    // Remove our listener from the request input stream.
    this.setAsyncListener(this.sink.inputStream, null);

    this._findOpenResponse();

    if (!this.httpActivity.discardResponseBody && this.receivedData.length) {
      this._onComplete(this.receivedData);
    } else if (!this.httpActivity.discardResponseBody &&
               this.httpActivity.responseStatus == 304) {
      // Response is cached, so we load it from cache.
      let charset = this.request.contentCharset || this.httpActivity.charset;
      NetworkHelper.loadFromCache(this.httpActivity.url, charset,
                                  this._onComplete.bind(this));
    } else {
      this._onComplete();
    }
  },

  /**
   * Handler for when the response completes. This function cleans up the
   * response listener.
   *
   * @param string [data]
   *        Optional, the received data coming from the response listener or
   *        from the cache.
   */
  _onComplete: function (data) {
    let response = {
      mimeType: "",
      text: data || "",
    };

    response.size = this.bodySize;
    response.transferredSize = this.transferredSize;

    try {
      response.mimeType = this.request.contentType;
    } catch (ex) {
      // Ignore.
    }

    if (!response.mimeType ||
        !NetworkHelper.isTextMimeType(response.mimeType)) {
      response.encoding = "base64";
      try {
        response.text = btoa(response.text);
      } catch (err) {
        // Ignore.
      }
    }

    if (response.mimeType && this.request.contentCharset) {
      response.mimeType += "; charset=" + this.request.contentCharset;
    }

    this.receivedData = "";

    this.httpActivity.owner.addResponseContent(
      response,
      this.httpActivity.discardResponseBody
    );

    this._wrappedNotificationCallbacks = null;
    this.httpActivity = null;
    this.sink = null;
    this.inputStream = null;
    this.converter = null;
    this.request = null;
    this.owner = null;
  },

  /**
   * The nsIInputStreamCallback for when the request input stream is ready -
   * either it has more data or it is closed.
   *
   * @param nsIAsyncInputStream stream
   *        The sink input stream from which data is coming.
   * @returns void
   */
  onInputStreamReady: function (stream) {
    if (!(stream instanceof Ci.nsIAsyncInputStream) || !this.httpActivity) {
      return;
    }

    let available = -1;
    try {
      // This may throw if the stream is closed normally or due to an error.
      available = stream.available();
    } catch (ex) {
      // Ignore.
    }

    if (available != -1) {
      if (available != 0) {
        if (this.converter) {
          this.converter.onDataAvailable(this.request, null, stream,
                                         this.offset, available);
        } else {
          this.onDataAvailable(this.request, null, stream, this.offset,
                               available);
        }
      }
      this.offset += available;
      this.setAsyncListener(stream, this);
    } else {
      this.onStreamClose();
      this.offset = 0;
    }
  },
};

/**
 * The network monitor uses the nsIHttpActivityDistributor to monitor network
 * requests. The nsIObserverService is also used for monitoring
 * http-on-examine-response notifications. All network request information is
 * routed to the remote Web Console.
 *
 * @constructor
 * @param object filters
 *        Object with the filters to use for network requests:
 *        - window (nsIDOMWindow): filter network requests by the associated
 *          window object.
 *        - appId (number): filter requests by the appId.
 *        - outerWindowID (number): filter requests by their top frame's outerWindowID.
 *        Filters are optional. If any of these filters match the request is
 *        logged (OR is applied). If no filter is provided then all requests are
 *        logged.
 * @param object owner
 *        The network monitor owner. This object needs to hold:
 *        - onNetworkEvent(requestInfo)
 *          This method is invoked once for every new network request and it is
 *          given the initial network request information as an argument.
 *          onNetworkEvent() must return an object which holds several add*()
 *          methods which are used to add further network request/response information.
 *        - stackTraceCollector
 *          If the owner has this optional property, it will be used as a
 *          StackTraceCollector by the NetworkMonitor.
 */
function NetworkMonitor(filters, owner) {
  this.filters = filters;
  this.owner = owner;
  this.openRequests = {};
  this.openResponses = {};
  this._httpResponseExaminer =
    DevToolsUtils.makeInfallible(this._httpResponseExaminer).bind(this);
  this._httpModifyExaminer =
    DevToolsUtils.makeInfallible(this._httpModifyExaminer).bind(this);
  this._serviceWorkerRequest = this._serviceWorkerRequest.bind(this);
  this._throttleData = null;
  this._throttler = null;
}

exports.NetworkMonitor = NetworkMonitor;

NetworkMonitor.prototype = {
  filters: null,

  httpTransactionCodes: {
    0x5001: "REQUEST_HEADER",
    0x5002: "REQUEST_BODY_SENT",
    0x5003: "RESPONSE_START",
    0x5004: "RESPONSE_HEADER",
    0x5005: "RESPONSE_COMPLETE",
    0x5006: "TRANSACTION_CLOSE",

    0x804b0003: "STATUS_RESOLVING",
    0x804b000b: "STATUS_RESOLVED",
    0x804b0007: "STATUS_CONNECTING_TO",
    0x804b0004: "STATUS_CONNECTED_TO",
    0x804b0005: "STATUS_SENDING_TO",
    0x804b000a: "STATUS_WAITING_FOR",
    0x804b0006: "STATUS_RECEIVING_FROM",
    0x804b000c: "STATUS_TLS_STARTING",
    0x804b000d: "STATUS_TLS_ENDING"
  },

  httpDownloadActivities: [
    gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_START,
    gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_HEADER,
    gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_COMPLETE,
    gActivityDistributor.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE
  ],

  // Network response bodies are piped through a buffer of the given size (in
  // bytes).
  responsePipeSegmentSize: null,

  owner: null,

  /**
   * Whether to save the bodies of network requests and responses.
   * @type boolean
   */
  saveRequestAndResponseBodies: true,

  /**
   * Object that holds the HTTP activity objects for ongoing requests.
   */
  openRequests: null,

  /**
   * Object that holds response headers coming from this._httpResponseExaminer.
   */
  openResponses: null,

  /**
   * The network monitor initializer.
   */
  init: function () {
    this.responsePipeSegmentSize = Services.prefs
                                   .getIntPref("network.buffer.cache.size");
    this.interceptedChannels = new Set();

    if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) {
      gActivityDistributor.addObserver(this);
      Services.obs.addObserver(this._httpResponseExaminer,
                               "http-on-examine-response", false);
      Services.obs.addObserver(this._httpResponseExaminer,
                               "http-on-examine-cached-response", false);
      Services.obs.addObserver(this._httpModifyExaminer,
                               "http-on-modify-request", false);
    }
    // In child processes, only watch for service worker requests
    // everything else only happens in the parent process
    Services.obs.addObserver(this._serviceWorkerRequest,
                             "service-worker-synthesized-response", false);
  },

  get throttleData() {
    return this._throttleData;
  },

  set throttleData(value) {
    this._throttleData = value;
    // Clear out any existing throttlers
    this._throttler = null;
  },

  _getThrottler: function () {
    if (this.throttleData !== null && this._throttler === null) {
      this._throttler = new NetworkThrottleManager(this.throttleData);
    }
    return this._throttler;
  },

  _serviceWorkerRequest: function (subject, topic, data) {
    let channel = subject.QueryInterface(Ci.nsIHttpChannel);

    if (!matchRequest(channel, this.filters)) {
      return;
    }

    this.interceptedChannels.add(subject);

    // On e10s, we never receive http-on-examine-cached-response, so fake one.
    if (Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) {
      this._httpResponseExaminer(channel, "http-on-examine-cached-response");
    }
  },

  /**
   * Observe notifications for the http-on-examine-response topic, coming from
   * the nsIObserverService.
   *
   * @private
   * @param nsIHttpChannel subject
   * @param string topic
   * @returns void
   */
  _httpResponseExaminer: function (subject, topic) {
    // The httpResponseExaminer is used to retrieve the uncached response
    // headers. The data retrieved is stored in openResponses. The
    // NetworkResponseListener is responsible with updating the httpActivity
    // object with the data from the new object in openResponses.

    if (!this.owner ||
        (topic != "http-on-examine-response" &&
         topic != "http-on-examine-cached-response") ||
        !(subject instanceof Ci.nsIHttpChannel)) {
      return;
    }

    let channel = subject.QueryInterface(Ci.nsIHttpChannel);

    if (!matchRequest(channel, this.filters)) {
      return;
    }

    let response = {
      id: gSequenceId(),
      channel: channel,
      headers: [],
      cookies: [],
    };

    let setCookieHeader = null;

    channel.visitResponseHeaders({
      visitHeader: function (name, value) {
        let lowerName = name.toLowerCase();
        if (lowerName == "set-cookie") {
          setCookieHeader = value;
        }
        response.headers.push({ name: name, value: value });
      }
    });

    if (!response.headers.length) {
      // No need to continue.
      return;
    }

    if (setCookieHeader) {
      response.cookies = NetworkHelper.parseSetCookieHeader(setCookieHeader);
    }

    // Determine the HTTP version.
    let httpVersionMaj = {};
    let httpVersionMin = {};

    channel.QueryInterface(Ci.nsIHttpChannelInternal);
    channel.getResponseVersion(httpVersionMaj, httpVersionMin);

    response.status = channel.responseStatus;
    response.statusText = channel.responseStatusText;
    response.httpVersion = "HTTP/" + httpVersionMaj.value + "." +
                                     httpVersionMin.value;

    this.openResponses[response.id] = response;

    if (topic === "http-on-examine-cached-response") {
      // Service worker requests emits cached-reponse notification on non-e10s,
      // and we fake one on e10s.
      let fromServiceWorker = this.interceptedChannels.has(channel);
      this.interceptedChannels.delete(channel);

      // If this is a cached response, there never was a request event
      // so we need to construct one here so the frontend gets all the
      // expected events.
      let httpActivity = this._createNetworkEvent(channel, {
        fromCache: !fromServiceWorker,
        fromServiceWorker: fromServiceWorker
      });
      httpActivity.owner.addResponseStart({
        httpVersion: response.httpVersion,
        remoteAddress: "",
        remotePort: "",
        status: response.status,
        statusText: response.statusText,
        headersSize: 0,
      }, "", true);

      // There also is never any timing events, so we can fire this
      // event with zeroed out values.
      let timings = this._setupHarTimings(httpActivity, true);
      httpActivity.owner.addEventTimings(timings.total, timings.timings);
    }
  },

  /**
   * Observe notifications for the http-on-modify-request topic, coming from
   * the nsIObserverService.
   *
   * @private
   * @param nsIHttpChannel aSubject
   * @returns void
   */
  _httpModifyExaminer: function (subject) {
    let throttler = this._getThrottler();
    if (throttler) {
      let channel = subject.QueryInterface(Ci.nsIHttpChannel);
      if (matchRequest(channel, this.filters)) {
        // Read any request body here, before it is throttled.
        let httpActivity = this.createOrGetActivityObject(channel);
        this._onRequestBodySent(httpActivity);
        throttler.manageUpload(channel);
      }
    }
  },

  /**
   * A helper function for observeActivity.  This does whatever work
   * is required by a particular http activity event.  Arguments are
   * the same as for observeActivity.
   */
  _dispatchActivity: function (httpActivity, channel, activityType,
                               activitySubtype, timestamp, extraSizeData,
                               extraStringData) {
    let transCodes = this.httpTransactionCodes;

    // Store the time information for this activity subtype.
    if (activitySubtype in transCodes) {
      let stage = transCodes[activitySubtype];
      if (stage in httpActivity.timings) {
        httpActivity.timings[stage].last = timestamp;
      } else {
        httpActivity.timings[stage] = {
          first: timestamp,
          last: timestamp,
        };
      }
    }

    switch (activitySubtype) {
      case gActivityDistributor.ACTIVITY_SUBTYPE_REQUEST_BODY_SENT:
        this._onRequestBodySent(httpActivity);
        if (httpActivity.sentBody !== null) {
          httpActivity.owner.addRequestPostData({ text: httpActivity.sentBody });
          httpActivity.sentBody = null;
        }
        break;
      case gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_HEADER:
        this._onResponseHeader(httpActivity, extraStringData);
        break;
      case gActivityDistributor.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE:
        this._onTransactionClose(httpActivity);
        break;
      default:
        break;
    }
  },

  /**
   * Begin observing HTTP traffic that originates inside the current tab.
   *
   * @see https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIHttpActivityObserver
   *
   * @param nsIHttpChannel channel
   * @param number activityType
   * @param number activitySubtype
   * @param number timestamp
   * @param number extraSizeData
   * @param string extraStringData
   */
  observeActivity:
  DevToolsUtils.makeInfallible(function (channel, activityType, activitySubtype,
                                        timestamp, extraSizeData,
                                        extraStringData) {
    if (!this.owner ||
        activityType != gActivityDistributor.ACTIVITY_TYPE_HTTP_TRANSACTION &&
        activityType != gActivityDistributor.ACTIVITY_TYPE_SOCKET_TRANSPORT) {
      return;
    }

    if (!(channel instanceof Ci.nsIHttpChannel)) {
      return;
    }

    channel = channel.QueryInterface(Ci.nsIHttpChannel);

    if (activitySubtype ==
        gActivityDistributor.ACTIVITY_SUBTYPE_REQUEST_HEADER) {
      this._onRequestHeader(channel, timestamp, extraStringData);
      return;
    }

    // Iterate over all currently ongoing requests. If channel can't
    // be found within them, then exit this function.
    let httpActivity = this._findActivityObject(channel);
    if (!httpActivity) {
      return;
    }

    // If we're throttling, we must not report events as they arrive
    // from platform, but instead let the throttler emit the events
    // after some time has elapsed.
    if (httpActivity.downloadThrottle &&
        this.httpDownloadActivities.indexOf(activitySubtype) >= 0) {
      let callback = this._dispatchActivity.bind(this);
      httpActivity.downloadThrottle
        .addActivityCallback(callback, httpActivity, channel, activityType,
                             activitySubtype, timestamp, extraSizeData,
                             extraStringData);
    } else {
      this._dispatchActivity(httpActivity, channel, activityType,
                             activitySubtype, timestamp, extraSizeData,
                             extraStringData);
    }
  }),

  /**
   *
   */
  _createNetworkEvent: function (channel, { timestamp, extraStringData,
                                           fromCache, fromServiceWorker }) {
    let httpActivity = this.createOrGetActivityObject(channel);

    channel.QueryInterface(Ci.nsIPrivateBrowsingChannel);
    httpActivity.private = channel.isChannelPrivate;

    if (timestamp) {
      httpActivity.timings.REQUEST_HEADER = {
        first: timestamp,
        last: timestamp
      };
    }

    let event = {};
    event.method = channel.requestMethod;
    event.channelId = channel.channelId;
    event.url = channel.URI.spec;
    event.private = httpActivity.private;
    event.headersSize = 0;
    event.startedDateTime =
      (timestamp ? new Date(Math.round(timestamp / 1000)) : new Date())
      .toISOString();
    event.fromCache = fromCache;
    event.fromServiceWorker = fromServiceWorker;
    httpActivity.fromServiceWorker = fromServiceWorker;

    if (extraStringData) {
      event.headersSize = extraStringData.length;
    }

    // Determine the cause and if this is an XHR request.
    let causeType = channel.loadInfo.externalContentPolicyType;
    let loadingPrincipal = channel.loadInfo.loadingPrincipal;
    let causeUri = loadingPrincipal ? loadingPrincipal.URI : null;
    let stacktrace;
    // If this is the parent process, there is no stackTraceCollector - the stack
    // trace will be added in NetworkMonitorChild._onNewEvent.
    if (this.owner.stackTraceCollector) {
      stacktrace = this.owner.stackTraceCollector.getStackTrace(event.channelId);
    }

    event.cause = {
      type: causeType,
      loadingDocumentUri: causeUri ? causeUri.spec : null,
      stacktrace
    };

    httpActivity.isXHR = event.isXHR =
        (causeType === Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST ||
         causeType === Ci.nsIContentPolicy.TYPE_FETCH);

    // Determine the HTTP version.
    let httpVersionMaj = {};
    let httpVersionMin = {};
    channel.QueryInterface(Ci.nsIHttpChannelInternal);
    channel.getRequestVersion(httpVersionMaj, httpVersionMin);

    event.httpVersion = "HTTP/" + httpVersionMaj.value + "." +
                                  httpVersionMin.value;

    event.discardRequestBody = !this.saveRequestAndResponseBodies;
    event.discardResponseBody = !this.saveRequestAndResponseBodies;

    let headers = [];
    let cookies = [];
    let cookieHeader = null;

    // Copy the request header data.
    channel.visitRequestHeaders({
      visitHeader: function (name, value) {
        if (name == "Cookie") {
          cookieHeader = value;
        }
        headers.push({ name: name, value: value });
      }
    });

    if (cookieHeader) {
      cookies = NetworkHelper.parseCookieHeader(cookieHeader);
    }

    httpActivity.owner = this.owner.onNetworkEvent(event);

    this._setupResponseListener(httpActivity, fromCache);

    httpActivity.owner.addRequestHeaders(headers, extraStringData);
    httpActivity.owner.addRequestCookies(cookies);

    return httpActivity;
  },

  /**
   * Handler for ACTIVITY_SUBTYPE_REQUEST_HEADER. When a request starts the
   * headers are sent to the server. This method creates the |httpActivity|
   * object where we store the request and response information that is
   * collected through its lifetime.
   *
   * @private
   * @param nsIHttpChannel channel
   * @param number timestamp
   * @param string extraStringData
   * @return void
   */
  _onRequestHeader: function (channel, timestamp, extraStringData) {
    if (!matchRequest(channel, this.filters)) {
      return;
    }

    this._createNetworkEvent(channel, { timestamp, extraStringData });
  },

  /**
   * Find an HTTP activity object for the channel.
   *
   * @param nsIHttpChannel channel
   *        The HTTP channel whose activity object we want to find.
   * @return object
   *        The HTTP activity object, or null if it is not found.
   */
  _findActivityObject: function (channel) {
    for (let id in this.openRequests) {
      let item = this.openRequests[id];
      if (item.channel === channel) {
        return item;
      }
    }
    return null;
  },

  /**
   * Find an existing HTTP activity object, or create a new one. This
   * object is used for storing all the request and response
   * information.
   *
   * This is a HAR-like object. Conformance to the spec is not guaranteed at
   * this point.
   *
   * @see http://www.softwareishard.com/blog/har-12-spec
   * @param nsIHttpChannel channel
   *        The HTTP channel for which the HTTP activity object is created.
   * @return object
   *         The new HTTP activity object.
   */
  createOrGetActivityObject: function (channel) {
    let httpActivity = this._findActivityObject(channel);
    if (!httpActivity) {
      let win = NetworkHelper.getWindowForRequest(channel);
      let charset = win ? win.document.characterSet : null;

      httpActivity = {
        id: gSequenceId(),
        channel: channel,
        // see _onRequestBodySent()
        charset: charset,
        sentBody: null,
        url: channel.URI.spec,
        // needed for host specific security info
        hostname: channel.URI.host,
        discardRequestBody: !this.saveRequestAndResponseBodies,
        discardResponseBody: !this.saveRequestAndResponseBodies,
        // internal timing information, see observeActivity()
        timings: {},
        // see _onResponseHeader()
        responseStatus: null,
        // the activity owner which is notified when changes happen
        owner: null,
      };

      this.openRequests[httpActivity.id] = httpActivity;
    }

    return httpActivity;
  },

  /**
   * Setup the network response listener for the given HTTP activity. The
   * NetworkResponseListener is responsible for storing the response body.
   *
   * @private
   * @param object httpActivity
   *        The HTTP activity object we are tracking.
   */
  _setupResponseListener: function (httpActivity, fromCache) {
    let channel = httpActivity.channel;
    channel.QueryInterface(Ci.nsITraceableChannel);

    if (!fromCache) {
      let throttler = this._getThrottler();
      if (throttler) {
        httpActivity.downloadThrottle = throttler.manage(channel);
      }
    }

    // The response will be written into the outputStream of this pipe.
    // This allows us to buffer the data we are receiving and read it
    // asynchronously.
    // Both ends of the pipe must be blocking.
    let sink = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);

    // The streams need to be blocking because this is required by the
    // stream tee.
    sink.init(false, false, this.responsePipeSegmentSize, PR_UINT32_MAX, null);

    // Add listener for the response body.
    let newListener = new NetworkResponseListener(this, httpActivity);

    // Remember the input stream, so it isn't released by GC.
    newListener.inputStream = sink.inputStream;
    newListener.sink = sink;

    let tee = Cc["@mozilla.org/network/stream-listener-tee;1"]
              .createInstance(Ci.nsIStreamListenerTee);

    let originalListener = channel.setNewListener(tee);

    tee.init(originalListener, sink.outputStream, newListener);
  },

  /**
   * Handler for ACTIVITY_SUBTYPE_REQUEST_BODY_SENT. The request body is logged
   * here.
   *
   * @private
   * @param object httpActivity
   *        The HTTP activity object we are working with.
   */
  _onRequestBodySent: function (httpActivity) {
    // Return early if we don't need the request body, or if we've
    // already found it.
    if (httpActivity.discardRequestBody || httpActivity.sentBody !== null) {
      return;
    }

    let sentBody = NetworkHelper.readPostTextFromRequest(httpActivity.channel,
                                                         httpActivity.charset);

    if (sentBody !== null && this.window &&
        httpActivity.url == this.window.location.href) {
      // If the request URL is the same as the current page URL, then
      // we can try to get the posted text from the page directly.
      // This check is necessary as otherwise the
      //   NetworkHelper.readPostTextFromPageViaWebNav()
      // function is called for image requests as well but these
      // are not web pages and as such don't store the posted text
      // in the cache of the webpage.
      let webNav = this.window.QueryInterface(Ci.nsIInterfaceRequestor)
                   .getInterface(Ci.nsIWebNavigation);
      sentBody = NetworkHelper
                 .readPostTextFromPageViaWebNav(webNav, httpActivity.charset);
    }

    if (sentBody !== null) {
      httpActivity.sentBody = sentBody;
    }
  },

  /**
   * Handler for ACTIVITY_SUBTYPE_RESPONSE_HEADER. This method stores
   * information about the response headers.
   *
   * @private
   * @param object httpActivity
   *        The HTTP activity object we are working with.
   * @param string extraStringData
   *        The uncached response headers.
   */
  _onResponseHeader: function (httpActivity, extraStringData) {
    // extraStringData contains the uncached response headers. The first line
    // contains the response status (e.g. HTTP/1.1 200 OK).
    //
    // Note: The response header is not saved here. Calling the
    // channel.visitResponseHeaders() method at this point sometimes causes an
    // NS_ERROR_NOT_AVAILABLE exception.
    //
    // We could parse extraStringData to get the headers and their values, but
    // that is not trivial to do in an accurate manner. Hence, we save the
    // response headers in this._httpResponseExaminer().

    let headers = extraStringData.split(/\r\n|\n|\r/);
    let statusLine = headers.shift();
    let statusLineArray = statusLine.split(" ");

    let response = {};
    response.httpVersion = statusLineArray.shift();
    // XXX: 
    // Sometimes, when using a proxy server (manual proxy configuration),
    // throws an error:
    // 0x80040111 (NS_ERROR_NOT_AVAILABLE)
    // [nsIHttpChannelInternal.remoteAddress]
    // Bug 1337791 is the suspect.
    response.remoteAddress = null;
    try {
      response.remoteAddress = httpActivity.channel.remoteAddress;
    } catch (e) {
      Cu.reportError(e);
    }
    response.remotePort = null;
    try {
      response.remotePort = httpActivity.channel.remotePort;
    } catch (e) {
      Cu.reportError(e);
    }
    response.status = statusLineArray.shift();
    response.statusText = statusLineArray.join(" ");
    response.headersSize = extraStringData.length;

    httpActivity.responseStatus = response.status;

    // Discard the response body for known response statuses.
    switch (parseInt(response.status, 10)) {
      case HTTP_MOVED_PERMANENTLY:
      case HTTP_FOUND:
      case HTTP_SEE_OTHER:
      case HTTP_TEMPORARY_REDIRECT:
        httpActivity.discardResponseBody = true;
        break;
    }

    response.discardResponseBody = httpActivity.discardResponseBody;

    httpActivity.owner.addResponseStart(response, extraStringData);
  },

  /**
   * Handler for ACTIVITY_SUBTYPE_TRANSACTION_CLOSE. This method updates the HAR
   * timing information on the HTTP activity object and clears the request
   * from the list of known open requests.
   *
   * @private
   * @param object httpActivity
   *        The HTTP activity object we work with.
   */
  _onTransactionClose: function (httpActivity) {
    let result = this._setupHarTimings(httpActivity);
    httpActivity.owner.addEventTimings(result.total, result.timings);
    delete this.openRequests[httpActivity.id];
  },

  /**
   * Update the HTTP activity object to include timing information as in the HAR
   * spec. The HTTP activity object holds the raw timing information in
   * |timings| - these are timings stored for each activity notification. The
   * HAR timing information is constructed based on these lower level
   * data.
   *
   * @param object httpActivity
   *        The HTTP activity object we are working with.
   * @param boolean fromCache
   *        Indicates that the result was returned from the browser cache
   * @return object
   *         This object holds two properties:
   *         - total - the total time for all of the request and response.
   *         - timings - the HAR timings object.
   */
  _setupHarTimings: function (httpActivity, fromCache) {
    if (fromCache) {
      // If it came from the browser cache, we have no timing
      // information and these should all be 0
      return {
        total: 0,
        timings: {
          blocked: 0,
          dns: 0,
          ssl: 0,
          connect: 0,
          send: 0,
          wait: 0,
          receive: 0
        }
      };
    }

    let timings = httpActivity.timings;
    let harTimings = {};

    if (timings.STATUS_RESOLVING && timings.STATUS_CONNECTING_TO) {
      harTimings.blocked = timings.STATUS_RESOLVING.first -
                           timings.REQUEST_HEADER.first;
    } else if (timings.STATUS_SENDING_TO) {
      harTimings.blocked = timings.STATUS_SENDING_TO.first -
                           timings.REQUEST_HEADER.first;
    } else {
      harTimings.blocked = -1;
    }

    // DNS timing information is available only in when the DNS record is not
    // cached.
    harTimings.dns = timings.STATUS_RESOLVING && timings.STATUS_RESOLVED ?
                     timings.STATUS_RESOLVED.last -
                     timings.STATUS_RESOLVING.first : -1;

    if (timings.STATUS_CONNECTING_TO && timings.STATUS_CONNECTED_TO) {
      harTimings.connect = timings.STATUS_CONNECTED_TO.last -
                           timings.STATUS_CONNECTING_TO.first;
    } else {
      harTimings.connect = -1;
    }

    if (timings.STATUS_TLS_STARTING && timings.STATUS_TLS_ENDING) {
      harTimings.ssl = timings.STATUS_TLS_ENDING.last -
                           timings.STATUS_TLS_STARTING.first;
    } else {
      harTimings.ssl = -1;
    }

    // sometimes the connection information events are attached to a speculative
    // channel instead of this one, but necko might glue them back together in the
    // nsITimedChannel interface used by Resource and Navigation Timing
    let timedChannel = httpActivity.channel.QueryInterface(Ci.nsITimedChannel);

    if ((harTimings.connect <= 0) && timedChannel) {
      if (timedChannel.secureConnectionStartTime > timedChannel.connectStartTime) {
        harTimings.connect =
          timedChannel.secureConnectionStartTime - timedChannel.connectStartTime;
        harTimings.ssl =
          timedChannel.connectEndTime - timedChannel.secureConnectionStartTime;
      } else {
        harTimings.connect =
          timedChannel.connectEndTime - timedChannel.connectStartTime;
        harTimings.ssl = -1;
      }
    }

    if ((harTimings.dns <= 0) && timedChannel) {
      harTimings.dns =
        timedChannel.domainLookupEndTime - timedChannel.domainLookupStartTime;
    }

    if (timings.STATUS_SENDING_TO) {
      harTimings.send = timings.STATUS_SENDING_TO.last - timings.STATUS_SENDING_TO.first;
    } else if (timings.REQUEST_HEADER && timings.REQUEST_BODY_SENT) {
      harTimings.send = timings.REQUEST_BODY_SENT.last - timings.REQUEST_HEADER.first;
    } else {
      harTimings.send = -1;
    }

    if (timings.RESPONSE_START) {
      harTimings.wait = timings.RESPONSE_START.first -
                        (timings.REQUEST_BODY_SENT ||
                         timings.STATUS_SENDING_TO).last;
    } else {
      harTimings.wait = -1;
    }

    if (timings.RESPONSE_START && timings.RESPONSE_COMPLETE) {
      harTimings.receive = timings.RESPONSE_COMPLETE.last -
                           timings.RESPONSE_START.first;
    } else {
      harTimings.receive = -1;
    }

    let totalTime = 0;
    for (let timing in harTimings) {
      let time = Math.max(Math.round(harTimings[timing] / 1000), -1);
      harTimings[timing] = time;
      if (time > -1) {
        totalTime += time;
      }
    }

    return {
      total: totalTime,
      timings: harTimings,
    };
  },

  /**
   * Suspend Web Console activity. This is called when all Web Consoles are
   * closed.
   */
  destroy: function () {
    if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) {
      gActivityDistributor.removeObserver(this);
      Services.obs.removeObserver(this._httpResponseExaminer,
                                  "http-on-examine-response");
      Services.obs.removeObserver(this._httpResponseExaminer,
                                  "http-on-examine-cached-response");
      Services.obs.removeObserver(this._httpModifyExaminer,
                                  "http-on-modify-request", false);
    }

    Services.obs.removeObserver(this._serviceWorkerRequest,
                                "service-worker-synthesized-response");

    this.interceptedChannels.clear();
    this.openRequests = {};
    this.openResponses = {};
    this.owner = null;
    this.filters = null;
    this._throttler = null;
  },
};

/**
 * The NetworkMonitorChild is used to proxy all of the network activity of the
 * child app process from the main process. The child WebConsoleActor creates an
 * instance of this object.
 *
 * Network requests for apps happen in the main process. As such,
 * a NetworkMonitor instance is used by the WebappsActor in the main process to
 * log the network requests for this child process.
 *
 * The main process creates NetworkEventActorProxy instances per request. These
 * send the data to this object using the nsIMessageManager. Here we proxy the
 * data to the WebConsoleActor or to a NetworkEventActor.
 *
 * @constructor
 * @param number outerWindowID
 *        The outerWindowID of the TabActor's main window.
 * @param nsIMessageManager messageManager
 *        The nsIMessageManager to use to communicate with the parent process.
 * @param object DebuggerServerConnection
 *        The RDP connection to the client.
 * @param object owner
 *        The WebConsoleActor that is listening for the network requests.
 */
function NetworkMonitorChild(outerWindowID, messageManager, conn, owner) {
  this.outerWindowID = outerWindowID;
  this.conn = conn;
  this.owner = owner;
  this._messageManager = messageManager;
  this._onNewEvent = this._onNewEvent.bind(this);
  this._onUpdateEvent = this._onUpdateEvent.bind(this);
  this._netEvents = new Map();
  this._msgName = `debug:${this.conn.prefix}netmonitor`;
}

exports.NetworkMonitorChild = NetworkMonitorChild;

NetworkMonitorChild.prototype = {
  owner: null,
  _netEvents: null,
  _saveRequestAndResponseBodies: true,
  _throttleData: null,

  get saveRequestAndResponseBodies() {
    return this._saveRequestAndResponseBodies;
  },

  set saveRequestAndResponseBodies(val) {
    this._saveRequestAndResponseBodies = val;

    this._messageManager.sendAsyncMessage(this._msgName, {
      action: "setPreferences",
      preferences: {
        saveRequestAndResponseBodies: this._saveRequestAndResponseBodies,
      },
    });
  },

  get throttleData() {
    return this._throttleData;
  },

  set throttleData(val) {
    this._throttleData = val;

    this._messageManager.sendAsyncMessage(this._msgName, {
      action: "setPreferences",
      preferences: {
        throttleData: this._throttleData,
      },
    });
  },

  init: function () {
    this.conn.setupInParent({
      module: "devtools/shared/webconsole/network-monitor",
      setupParent: "setupParentProcess"
    });

    let mm = this._messageManager;
    mm.addMessageListener(`${this._msgName}:newEvent`, this._onNewEvent);
    mm.addMessageListener(`${this._msgName}:updateEvent`, this._onUpdateEvent);
    mm.sendAsyncMessage(this._msgName, {
      outerWindowID: this.outerWindowID,
      action: "start",
    });
  },

  _onNewEvent: DevToolsUtils.makeInfallible(function _onNewEvent(msg) {
    let {id, event} = msg.data;

    // Try to add stack trace to the event data received from parent
    if (this.owner.stackTraceCollector) {
      event.cause.stacktrace =
        this.owner.stackTraceCollector.getStackTrace(event.channelId);
    }

    let actor = this.owner.onNetworkEvent(event);
    this._netEvents.set(id, Cu.getWeakReference(actor));
  }),

  _onUpdateEvent: DevToolsUtils.makeInfallible(function _onUpdateEvent(msg) {
    let {id, method, args} = msg.data;
    let weakActor = this._netEvents.get(id);
    let actor = weakActor ? weakActor.get() : null;
    if (!actor) {
      console.error(`Received ${this._msgName}:updateEvent for unknown event ID: ${id}`);
      return;
    }
    if (!(method in actor)) {
      console.error(`Received ${this._msgName}:updateEvent unsupported ` +
                    `method: ${method}`);
      return;
    }
    actor[method].apply(actor, args);
  }),

  destroy: function () {
    let mm = this._messageManager;
    try {
      mm.removeMessageListener(`${this._msgName}:newEvent`, this._onNewEvent);
      mm.removeMessageListener(`${this._msgName}:updateEvent`, this._onUpdateEvent);
    } catch (e) {
      // On b2g, when registered to a new root docshell,
      // all message manager functions throw when trying to call them during
      // message-manager-disconnect event.
      // As there is no attribute/method on message manager to know
      // if they are still usable or not, we can only catch the exception...
    }
    this._netEvents.clear();
    this._messageManager = null;
    this.conn = null;
    this.owner = null;
  },
};

/**
 * The NetworkEventActorProxy is used to send network request information from
 * the main process to the child app process. One proxy is used per request.
 * Similarly, one NetworkEventActor in the child app process is used per
 * request. The client receives all network logs from the child actors.
 *
 * The child process has a NetworkMonitorChild instance that is listening for
 * all network logging from the main process. The net monitor shim is used to
 * proxy the data to the WebConsoleActor instance of the child process.
 *
 * @constructor
 * @param nsIMessageManager messageManager
 *        The message manager for the child app process. This is used for
 *        communication with the NetworkMonitorChild instance of the process.
 * @param string msgName
 *        The message name to be used for this connection.
 */
function NetworkEventActorProxy(messageManager, msgName) {
  this.id = gSequenceId();
  this.messageManager = messageManager;
  this._msgName = msgName;
}
exports.NetworkEventActorProxy = NetworkEventActorProxy;

NetworkEventActorProxy.methodFactory = function (method) {
  return DevToolsUtils.makeInfallible(function () {
    let args = Array.slice(arguments);
    let mm = this.messageManager;
    mm.sendAsyncMessage(`${this._msgName}:updateEvent`, {
      id: this.id,
      method: method,
      args: args,
    });
  }, "NetworkEventActorProxy." + method);
};

NetworkEventActorProxy.prototype = {
  /**
   * Initialize the network event. This method sends the network request event
   * to the content process.
   *
   * @param object event
   *        Object describing the network request.
   * @return object
   *         This object.
   */
  init: DevToolsUtils.makeInfallible(function (event) {
    let mm = this.messageManager;
    mm.sendAsyncMessage(`${this._msgName}:newEvent`, {
      id: this.id,
      event: event,
    });
    return this;
  }),
};

(function () {
  // Listeners for new network event data coming from the NetworkMonitor.
  let methods = ["addRequestHeaders", "addRequestCookies", "addRequestPostData",
                 "addResponseStart", "addSecurityInfo", "addResponseHeaders",
                 "addResponseCookies", "addResponseContent", "addEventTimings"];
  let factory = NetworkEventActorProxy.methodFactory;
  for (let method of methods) {
    NetworkEventActorProxy.prototype[method] = factory(method);
  }
})();

/**
 * This is triggered by the child calling `setupInParent` when the child's network monitor
 * is starting up.  This initializes the parent process side of the monitoring.
 */
function setupParentProcess({ mm, prefix }) {
  let networkMonitor = new NetworkMonitorParent(mm, prefix);
  return {
    onBrowserSwap: newMM => networkMonitor.setMessageManager(newMM),
    onDisconnected: () => {
      networkMonitor.destroy();
      networkMonitor = null;
    }
  };
}

exports.setupParentProcess = setupParentProcess;

/**
 * The NetworkMonitorParent runs in the parent process and uses the message manager to
 * listen for requests from the child process to start/stop the network monitor.  Most
 * request data is only available from the parent process, so that's why the network
 * monitor needs to run there when debugging tabs that are in the child.
 *
 * @param nsIMessageManager mm
 *        The message manager for the browser we're filtering on.
 * @param string prefix
 *        The RDP connection prefix that uniquely identifies the connection.
 */
function NetworkMonitorParent(mm, prefix) {
  this._msgName = `debug:${prefix}netmonitor`;
  this.onNetMonitorMessage = this.onNetMonitorMessage.bind(this);
  this.onNetworkEvent = this.onNetworkEvent.bind(this);
  this.setMessageManager(mm);
}

NetworkMonitorParent.prototype = {
  netMonitor: null,
  messageManager: null,

  setMessageManager(mm) {
    if (this.messageManager) {
      let oldMM = this.messageManager;
      oldMM.removeMessageListener(this._msgName, this.onNetMonitorMessage);
    }
    this.messageManager = mm;
    if (mm) {
      mm.addMessageListener(this._msgName, this.onNetMonitorMessage);
    }
  },

  /**
   * Handler for `debug:${prefix}netmonitor` messages received through the message manager
   * from the content process.
   *
   * @param object msg
   *        Message from the content.
   */
  onNetMonitorMessage: DevToolsUtils.makeInfallible(function (msg) {
    let {action} = msg.json;
    // Pipe network monitor data from parent to child via the message manager.
    switch (action) {
      case "start":
        if (!this.netMonitor) {
          let {appId, outerWindowID} = msg.json;
          this.netMonitor = new NetworkMonitor({
            outerWindowID,
            appId,
          }, this);
          this.netMonitor.init();
        }
        break;
      case "setPreferences": {
        let {preferences} = msg.json;
        for (let key of Object.keys(preferences)) {
          if ((key == "saveRequestAndResponseBodies" ||
               key == "throttleData") && this.netMonitor) {
            this.netMonitor[key] = preferences[key];
          }
        }
        break;
      }

      case "stop":
        if (this.netMonitor) {
          this.netMonitor.destroy();
          this.netMonitor = null;
        }
        break;

      case "disconnect":
        this.destroy();
        break;
    }
  }),

  /**
   * Handler for new network requests. This method is invoked by the current
   * NetworkMonitor instance.
   *
   * @param object event
   *        Object describing the network request.
   * @return object
   *         A NetworkEventActorProxy instance which is notified when further
   *         data about the request is available.
   */
  onNetworkEvent: DevToolsUtils.makeInfallible(function (event) {
    return new NetworkEventActorProxy(this.messageManager, this._msgName).init(event);
  }),

  destroy: function () {
    this.setMessageManager(null);

    if (this.netMonitor) {
      this.netMonitor.destroy();
      this.netMonitor = null;
    }
  },
};

/**
 * A WebProgressListener that listens for location changes.
 *
 * This progress listener is used to track file loads and other kinds of
 * location changes.
 *
 * @constructor
 * @param object window
 *        The window for which we need to track location changes.
 * @param object owner
 *        The listener owner which needs to implement two methods:
 *        - onFileActivity(aFileURI)
 *        - onLocationChange(aState, aTabURI, aPageTitle)
 */
function ConsoleProgressListener(window, owner) {
  this.window = window;
  this.owner = owner;
}
exports.ConsoleProgressListener = ConsoleProgressListener;

ConsoleProgressListener.prototype = {
  /**
   * Constant used for startMonitor()/stopMonitor() that tells you want to
   * monitor file loads.
   */
  MONITOR_FILE_ACTIVITY: 1,

  /**
   * Constant used for startMonitor()/stopMonitor() that tells you want to
   * monitor page location changes.
   */
  MONITOR_LOCATION_CHANGE: 2,

  /**
   * Tells if you want to monitor file activity.
   * @private
   * @type boolean
   */
  _fileActivity: false,

  /**
   * Tells if you want to monitor location changes.
   * @private
   * @type boolean
   */
  _locationChange: false,

  /**
   * Tells if the console progress listener is initialized or not.
   * @private
   * @type boolean
   */
  _initialized: false,

  _webProgress: null,

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

  /**
   * Initialize the ConsoleProgressListener.
   * @private
   */
  _init: function () {
    if (this._initialized) {
      return;
    }

    this._webProgress = this.window.QueryInterface(Ci.nsIInterfaceRequestor)
                        .getInterface(Ci.nsIWebNavigation)
                        .QueryInterface(Ci.nsIWebProgress);
    this._webProgress.addProgressListener(this,
                                          Ci.nsIWebProgress.NOTIFY_STATE_ALL);

    this._initialized = true;
  },

  /**
   * Start a monitor/tracker related to the current nsIWebProgressListener
   * instance.
   *
   * @param number monitor
   *        Tells what you want to track. Available constants:
   *        - this.MONITOR_FILE_ACTIVITY
   *          Track file loads.
   *        - this.MONITOR_LOCATION_CHANGE
   *          Track location changes for the top window.
   */
  startMonitor: function (monitor) {
    switch (monitor) {
      case this.MONITOR_FILE_ACTIVITY:
        this._fileActivity = true;
        break;
      case this.MONITOR_LOCATION_CHANGE:
        this._locationChange = true;
        break;
      default:
        throw new Error("ConsoleProgressListener: unknown monitor type " +
                        monitor + "!");
    }
    this._init();
  },

  /**
   * Stop a monitor.
   *
   * @param number monitor
   *        Tells what you want to stop tracking. See this.startMonitor() for
   *        the list of constants.
   */
  stopMonitor: function (monitor) {
    switch (monitor) {
      case this.MONITOR_FILE_ACTIVITY:
        this._fileActivity = false;
        break;
      case this.MONITOR_LOCATION_CHANGE:
        this._locationChange = false;
        break;
      default:
        throw new Error("ConsoleProgressListener: unknown monitor type " +
                        monitor + "!");
    }

    if (!this._fileActivity && !this._locationChange) {
      this.destroy();
    }
  },

  onStateChange: function (progress, request, state, status) {
    if (!this.owner) {
      return;
    }

    if (this._fileActivity) {
      this._checkFileActivity(progress, request, state, status);
    }

    if (this._locationChange) {
      this._checkLocationChange(progress, request, state, status);
    }
  },

  /**
   * Check if there is any file load, given the arguments of
   * nsIWebProgressListener.onStateChange. If the state change tells that a file
   * URI has been loaded, then the remote Web Console instance is notified.
   * @private
   */
  _checkFileActivity: function (progress, request, state, status) {
    if (!(state & Ci.nsIWebProgressListener.STATE_START)) {
      return;
    }

    let uri = null;
    if (request instanceof Ci.imgIRequest) {
      let imgIRequest = request.QueryInterface(Ci.imgIRequest);
      uri = imgIRequest.URI;
    } else if (request instanceof Ci.nsIChannel) {
      let nsIChannel = request.QueryInterface(Ci.nsIChannel);
      uri = nsIChannel.URI;
    }

    if (!uri || !uri.schemeIs("file") && !uri.schemeIs("ftp")) {
      return;
    }

    this.owner.onFileActivity(uri.spec);
  },

  /**
   * Check if the current window.top location is changing, given the arguments
   * of nsIWebProgressListener.onStateChange. If that is the case, the remote
   * Web Console instance is notified.
   * @private
   */
  _checkLocationChange: function (progress, request, state) {
    let isStart = state & Ci.nsIWebProgressListener.STATE_START;
    let isStop = state & Ci.nsIWebProgressListener.STATE_STOP;
    let isNetwork = state & Ci.nsIWebProgressListener.STATE_IS_NETWORK;
    let isWindow = state & Ci.nsIWebProgressListener.STATE_IS_WINDOW;

    // Skip non-interesting states.
    if (!isNetwork || !isWindow || progress.DOMWindow != this.window) {
      return;
    }

    if (isStart && request instanceof Ci.nsIChannel) {
      this.owner.onLocationChange("start", request.URI.spec, "");
    } else if (isStop) {
      this.owner.onLocationChange("stop", this.window.location.href,
                                  this.window.document.title);
    }
  },

  onLocationChange: function () {},
  onStatusChange: function () {},
  onProgressChange: function () {},
  onSecurityChange: function () {},

  /**
   * Destroy the ConsoleProgressListener.
   */
  destroy: function () {
    if (!this._initialized) {
      return;
    }

    this._initialized = false;
    this._fileActivity = false;
    this._locationChange = false;

    try {
      this._webProgress.removeProgressListener(this);
    } catch (ex) {
      // This can throw during browser shutdown.
    }

    this._webProgress = null;
    this.window = null;
    this.owner = null;
  },
};

function gSequenceId() {
  return gSequenceId.n++;
}
gSequenceId.n = 1;
