"use strict";

var {classes: Cc, interfaces: Ci, utils: Cu} = Components;

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

XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
                                  "resource://gre/modules/MatchPattern.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "WebRequest",
                                  "resource://gre/modules/WebRequest.jsm");

Cu.import("resource://gre/modules/ExtensionManagement.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
  SingletonEventManager,
} = ExtensionUtils;

// EventManager-like class specifically for WebRequest. Inherits from
// SingletonEventManager. Takes care of converting |details| parameter
// when invoking listeners.
function WebRequestEventManager(context, eventName) {
  let name = `webRequest.${eventName}`;
  let register = (callback, filter, info) => {
    let listener = data => {
      // Prevent listening in on requests originating from system principal to
      // prevent tinkering with OCSP, app and addon updates, etc.
      if (data.isSystemPrincipal) {
        return;
      }

      let data2 = {
        requestId: data.requestId,
        url: data.url,
        originUrl: data.originUrl,
        method: data.method,
        type: data.type,
        timeStamp: Date.now(),
        frameId: data.type == "main_frame" ? 0 : ExtensionManagement.getFrameId(data.windowId),
        parentFrameId: ExtensionManagement.getParentFrameId(data.parentWindowId, data.windowId),
      };

      const maybeCached = ["onResponseStarted", "onBeforeRedirect", "onCompleted", "onErrorOccurred"];
      if (maybeCached.includes(eventName)) {
        data2.fromCache = !!data.fromCache;
      }

      if ("ip" in data) {
        data2.ip = data.ip;
      }

      if ("incognito" in data) {
        data2.incognito = data.incognito;
      }

      extensions.emit("fill-browser-data", data.browser, data2);

      if ("tabId" in filter && data2.tabId !== filter.tabId) {
        return;
      }

      if ("windowId" in filter && data2.windowId !== filter.windowId) {
        return;
      }

      if ("incognito" in filter && !!data2.incognito !== filter.incognito) {
        return;
      }

      let optional = ["requestHeaders", "responseHeaders", "statusCode", "statusLine", "error", "redirectUrl",
                      "requestBody", "documentUrl", "scheme", "realm", "isProxy", "challenger",
                      "proxyInfo", "frameAncestors"];
      for (let opt of optional) {
        if (opt in data) {
          data2[opt] = data[opt];
        }
      }

      return context.runSafe(callback, data2);
    };

    let filter2 = {};
    filter2.urls = new MatchPattern(filter.urls);
    if (filter.types) {
      filter2.types = filter.types;
    }
    if ("tabId" in filter) {
      filter2.tabId = filter.tabId;
    }
    if ("windowId" in filter) {
      filter2.windowId = filter.windowId;
    }
    if ("incognito" in filter) {
      filter2.incognito = filter.incognito;
    }

    let info2 = [];
    if (info) {
      for (let desc of info) {
        if (desc == "blocking" && !context.extension.hasPermission("webRequestBlocking")) {
          Cu.reportError("Using webRequest.addListener with the blocking option " +
                         "requires the 'webRequestBlocking' permission.");
        } else {
          info2.push(desc);
        }
      }
    }

    WebRequest[eventName].addListener(listener, filter2, info2);
    return () => {
      WebRequest[eventName].removeListener(listener);
    };
  };

  return SingletonEventManager.call(this, context, name, register);
}

WebRequestEventManager.prototype = Object.create(SingletonEventManager.prototype);

function makeWebRequestEvent(context, eventName) {
  if (!(eventName in WebRequest) || !WebRequest[eventName]) {
    let name = `webRequest.${eventName}`;
    return new SingletonEventManager(context, name, () => {
      Cu.reportError(`webRequest.${eventName} is not supported by this runtime.`);
      return () => {};
    }).api();
  }

  return new WebRequestEventManager(context, eventName).api();
}

extensions.registerSchemaAPI("webRequest", "addon_parent", context => {
  return {
    webRequest: {
      onBeforeRequest: makeWebRequestEvent(context, "onBeforeRequest"),
      onBeforeSendHeaders: makeWebRequestEvent(context, "onBeforeSendHeaders"),
      onSendHeaders: makeWebRequestEvent(context, "onSendHeaders"),
      onHeadersReceived: makeWebRequestEvent(context, "onHeadersReceived"),
      onAuthRequired: makeWebRequestEvent(context, "onAuthRequired"),
      onBeforeRedirect: makeWebRequestEvent(context, "onBeforeRedirect"),
      onResponseStarted: makeWebRequestEvent(context, "onResponseStarted"),
      onErrorOccurred: makeWebRequestEvent(context, "onErrorOccurred"),
      onCompleted: makeWebRequestEvent(context, "onCompleted"),
      handlerBehaviorChanged: function() {
        // TODO: Flush all caches.
        return Promise.resolve();
      },
      filterResponseData: function(requestId) {
        requestId = parseInt(requestId, 10);
        return context.cloneScope.StreamFilter.create(requestId, context.extension.id);
      },
      getSecurityInfo: function(requestId, options = {}) {
        let remoteTab = null;
        if (context.xulBrowser && context.xulBrowser.frameLoader) {
          remoteTab = context.xulBrowser.frameLoader.remoteTab;
        }

        return WebRequest.getSecurityInfo({
          id: requestId,
          policy: context.extension.policy,
          remoteTab,
          options,
        });
      },
      // Resource type constants for feature detection and filtering
      ResourceType: Object.freeze({
        MAIN_FRAME: "main_frame",
        SUB_FRAME: "sub_frame",
        STYLESHEET: "stylesheet",
        SCRIPT: "script",
        IMAGE: "image",
        OBJECT: "object",
        XMLHTTPREQUEST: "xmlhttprequest",
        XBL: "xbl",
        XSLT: "xslt",
        PING: "ping",
        BEACON: "beacon",
        XML_DTD: "xml_dtd",
        FONT: "font",
        MEDIA: "media",
        WEBSOCKET: "websocket",
        CSP_REPORT: "csp_report",
        IMAGESET: "imageset",
        WEB_MANIFEST: "web_manifest",
        OTHER: "other"
      }),
      // Maximum handler behavior change calls per 10 minutes (per MDN spec)
      MAX_HANDLER_BEHAVIOR_CHANGED_CALLS_PER_10_MINUTES: 20,
    },
  };
});
