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

ChromeUtils.defineESModuleGetters(this, {
  WebRequest: "resource://gre/modules/WebRequest.sys.mjs",
});

var { parseMatchPatterns, DefaultMap } = ExtensionUtils;

const MAX_HANDLER_BEHAVIOR_CHANGED_CALLS_PER_10_MINUTES = 20;
const TIME_10_MINUTES_IN_MS = 10 * 60 * 1000;

// The timestamp of handlerBehaviorChanged calls which weren't rejected,
// per extension id.
const handlerBehaviorChangedTimeStampsMap = new DefaultMap(() => []);

function clearOldTimeStamps(timestamps, now) {
  while (timestamps.length) {
    if (timestamps[0] < now - TIME_10_MINUTES_IN_MS) {
      timestamps.shift();
    } else {
      break;
    }
  }
}

function handlerBehaviorChangedCallAllowed(timestamps) {
  return timestamps.length < MAX_HANDLER_BEHAVIOR_CHANGED_CALLS_PER_10_MINUTES;
}

// The guts of a WebRequest event handler.  Takes care of converting
// |details| parameter when invoking listeners.
function registerEvent(
  extension,
  eventName,
  fire,
  filter,
  info,
  remoteTab = null
) {
  let listener = async data => {
    let event = data.serialize(eventName);
    if (data.registerTraceableChannel) {
      // If this is a primed listener, no tabParent was passed in here,
      // but the convert() callback later in this function will be called
      // when the background page is started.  Force that to happen here
      // after which we'll have a valid tabParent.
      if (fire.wakeup) {
        await fire.wakeup();
      }
      data.registerTraceableChannel(extension.policy, remoteTab);
    }

    return fire.sync(event);
  };

  let filter2 = {};
  if (filter.urls) {
    let perms = new MatchPatternSet([
      ...extension.allowedOrigins.patterns,
      ...extension.optionalOrigins.patterns,
    ]);

    filter2.urls = parseMatchPatterns(filter.urls);

    if (!perms.overlapsAll(filter2.urls)) {
      Cu.reportError(
        "The webRequest.addListener filter doesn't overlap with host permissions."
      );
    }
  }
  if (filter.types) {
    filter2.types = filter.types;
  }
  if (filter.tabId !== undefined) {
    filter2.tabId = filter.tabId;
  }
  if (filter.windowId !== undefined) {
    filter2.windowId = filter.windowId;
  }
  if (filter.incognito !== undefined) {
    filter2.incognito = filter.incognito;
  }

  let blockingAllowed =
    eventName == "onAuthRequired"
      ? extension.hasPermission("webRequestBlocking") ||
        extension.hasPermission("webRequestAuthProvider")
      : extension.hasPermission("webRequestBlocking");

  let info2 = [];
  if (info) {
    for (let desc of info) {
      if ((desc == "blocking" || desc == "asyncBlocking") && !blockingAllowed) {
        // This is usually checked in the child process (based on the API schemas, where these options
        // should be checked with the "webRequestBlockingPermissionRequired" postprocess property),
        // but it is worth to also check it here just in case a new webRequest has been added and
        // it has not yet using the expected postprocess property).
        Cu.reportError(
          "Using webRequest.addListener with the blocking option " +
            "requires the 'webRequestBlocking' permission."
        );
      } else {
        info2.push(desc);
      }
    }
  }

  let listenerDetails = {
    addonId: extension.id,
    policy: extension.policy,
    blockingAllowed,
  };
  WebRequest[eventName].addListener(listener, filter2, info2, listenerDetails);

  return {
    unregister: () => {
      WebRequest[eventName].removeListener(listener);
    },
    convert(_fire, context) {
      fire = _fire;
      remoteTab = context.xulBrowser.frameLoader.remoteTab;
    },
  };
}

function makeWebRequestEventAPI(context, event, extensionApi) {
  return new EventManager({
    context,
    module: "webRequest",
    event,
    extensionApi,
  }).api();
}

function makeWebRequestEventRegistrar(event) {
  return function ({ fire, context }, params) {
    // ExtensionAPIPersistent makes sure this function will be bound
    // to the ExtensionAPIPersistent instance.
    const { extension } = this;

    const [filter, info] = params;

    // When we are registering the real listener coming from the extension context,
    // we should get the additional remoteTab parameter value from the extension context
    // (which is then used by the registerTraceableChannel helper to register stream
    // filters to the channel and associate them to the extension context that has
    // created it and will be handling the filter onstart/ondata/onend events).
    let remoteTab;
    if (context) {
      remoteTab = context.xulBrowser.frameLoader.remoteTab;
    }

    return registerEvent(extension, event, fire, filter, info, remoteTab);
  };
}

this.webRequest = class extends ExtensionAPIPersistent {
  primeListener(event, fire, params, isInStartup) {
    // During early startup if the listener does not use blocking we do not prime it.
    if (
      !isInStartup ||
      params[1]?.some(v => v === "blocking" || v === "asyncBlocking")
    ) {
      return super.primeListener(event, fire, params, isInStartup);
    }
  }

  PERSISTENT_EVENTS = {
    onBeforeRequest: makeWebRequestEventRegistrar("onBeforeRequest"),
    onBeforeSendHeaders: makeWebRequestEventRegistrar("onBeforeSendHeaders"),
    onSendHeaders: makeWebRequestEventRegistrar("onSendHeaders"),
    onHeadersReceived: makeWebRequestEventRegistrar("onHeadersReceived"),
    onAuthRequired: makeWebRequestEventRegistrar("onAuthRequired"),
    onBeforeRedirect: makeWebRequestEventRegistrar("onBeforeRedirect"),
    onResponseStarted: makeWebRequestEventRegistrar("onResponseStarted"),
    onErrorOccurred: makeWebRequestEventRegistrar("onErrorOccurred"),
    onCompleted: makeWebRequestEventRegistrar("onCompleted"),
  };

  getAPI(context) {
    return {
      webRequest: {
        onBeforeRequest: makeWebRequestEventAPI(
          context,
          "onBeforeRequest",
          this
        ),
        onBeforeSendHeaders: makeWebRequestEventAPI(
          context,
          "onBeforeSendHeaders",
          this
        ),
        onSendHeaders: makeWebRequestEventAPI(context, "onSendHeaders", this),
        onHeadersReceived: makeWebRequestEventAPI(
          context,
          "onHeadersReceived",
          this
        ),
        onAuthRequired: makeWebRequestEventAPI(context, "onAuthRequired", this),
        onBeforeRedirect: makeWebRequestEventAPI(
          context,
          "onBeforeRedirect",
          this
        ),
        onResponseStarted: makeWebRequestEventAPI(
          context,
          "onResponseStarted",
          this
        ),
        onErrorOccurred: makeWebRequestEventAPI(
          context,
          "onErrorOccurred",
          this
        ),
        onCompleted: makeWebRequestEventAPI(context, "onCompleted", this),
        getSecurityInfo: (requestId, options) => {
          options ??= {};
          return WebRequest.getSecurityInfo({
            id: requestId,
            policy: context.extension.policy,
            remoteTab: context.xulBrowser.frameLoader.remoteTab,
            options,
          });
        },
        handlerBehaviorChanged: () => {
          let timestamps = handlerBehaviorChangedTimeStampsMap.get(
            context.extension.id
          );

          const now = Date.now();
          clearOldTimeStamps(timestamps, now);
          if (!handlerBehaviorChangedCallAllowed(timestamps)) {
            context.extension.logger.warn(
              `The number of webRequest.handlerBehaviorChanged calls exceeds the limit: ${MAX_HANDLER_BEHAVIOR_CHANGED_CALLS_PER_10_MINUTES} calls per 10 minutes`
            );
            return;
          }
          timestamps.push(now);

          ChromeUtils.clearResourceCache({ target: "content" });
        },
      },
    };
  }
};
