mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 10:18:41 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			2464 lines
		
	
	
	
		
			86 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			2464 lines
		
	
	
	
		
			86 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* 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/. */
 | 
						|
 | 
						|
// Each extension that uses DNR has one RuleManager. All registered RuleManagers
 | 
						|
// are checked whenever a network request occurs. Individual extensions may
 | 
						|
// occasionally modify their rules (e.g. via the updateSessionRules API).
 | 
						|
const gRuleManagers = [];
 | 
						|
 | 
						|
/**
 | 
						|
 * Whenever a request occurs, the rules of each RuleManager are matched against
 | 
						|
 * the request to determine the final action to take. The RequestEvaluator class
 | 
						|
 * is responsible for evaluating rules, and its behavior is described below.
 | 
						|
 *
 | 
						|
 * Short version:
 | 
						|
 * Find the highest-priority rule that matches the given request. If the
 | 
						|
 * request is not canceled, all matching allowAllRequests and modifyHeaders
 | 
						|
 * actions are returned.
 | 
						|
 *
 | 
						|
 * Longer version:
 | 
						|
 * Unless stated otherwise, the explanation below describes the behavior within
 | 
						|
 * an extension.
 | 
						|
 * An extension can specify rules, optionally in multiple rulesets. The ability
 | 
						|
 * to have multiple ruleset exists to support bulk updates of rules. Rulesets
 | 
						|
 * are NOT independent - rules from different rulesets can affect each other.
 | 
						|
 *
 | 
						|
 * When multiple rules match, the order between rules are defined as follows:
 | 
						|
 * - Ruleset precedence: session > dynamic > static (order from manifest.json).
 | 
						|
 * - Rules in ruleset precedence: ordered by rule.id, lowest (numeric) ID first.
 | 
						|
 * - Across all rules+rulesets: highest rule.priority (default 1) first,
 | 
						|
 *                              action precedence if rule priority are the same.
 | 
						|
 *
 | 
						|
 * The primary documented way for extensions to describe precedence is by
 | 
						|
 * specifying rule.priority. Between same-priority rules, their precedence is
 | 
						|
 * dependent on the rule action. The ruleset/rule ID precedence is only used to
 | 
						|
 * have a defined ordering if multiple rules have the same priority+action.
 | 
						|
 *
 | 
						|
 * Rule actions have the following order of precedence and meaning:
 | 
						|
 * - "allow" can be used to ignore other same-or-lower-priority rules.
 | 
						|
 * - "allowAllRequests" (for main_frame / sub_frame resourceTypes only) has the
 | 
						|
 *      same effect as allow, but also applies to (future) subresource loads in
 | 
						|
 *      the document (including descendant frames) generated from the request.
 | 
						|
 * - "block" cancels the matched request.
 | 
						|
 * - "upgradeScheme" upgrades the scheme of the request.
 | 
						|
 * - "redirect" redirects the request.
 | 
						|
 * - "modifyHeaders" rewrites request/response headers.
 | 
						|
 *
 | 
						|
 * The matched rules are evaluated in two passes:
 | 
						|
 * 1. findMatchingRules():
 | 
						|
 *    Find the highest-priority rule(s), and choose the action with the highest
 | 
						|
 *    precedence (across all rulesets, any action except modifyHeaders).
 | 
						|
 *    This also accounts for any allowAllRequests from an ancestor frame.
 | 
						|
 *
 | 
						|
 * 2. getMatchingModifyHeadersRules():
 | 
						|
 *    Find matching rules with the "modifyHeaders" action, minus ignored rules.
 | 
						|
 *    Reaching this step implies that the request was not canceled, so either
 | 
						|
 *    the first step did not yield a rule, or the rule action is "allow" or
 | 
						|
 *    "allowAllRequests" (i.e. ignore same-or-lower-priority rules).
 | 
						|
 *
 | 
						|
 * If an extension does not have sufficient permissions for the action, the
 | 
						|
 * resulting action is ignored.
 | 
						|
 *
 | 
						|
 * The above describes the evaluation within one extension. When a sequence of
 | 
						|
 * (multiple) extensions is given, they may return conflicting actions in the
 | 
						|
 * first pass. This is resolved by choosing the action with the following order
 | 
						|
 * of precedence, in RequestEvaluator.evaluateRequest():
 | 
						|
 *  - block
 | 
						|
 *  - redirect / upgradeScheme
 | 
						|
 *  - allow / allowAllRequests
 | 
						|
 */
 | 
						|
 | 
						|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
 | 
						|
 | 
						|
/** @type {Lazy} */
 | 
						|
const lazy = {};
 | 
						|
 | 
						|
ChromeUtils.defineESModuleGetters(lazy, {
 | 
						|
  ExtensionDNRLimits: "resource://gre/modules/ExtensionDNRLimits.sys.mjs",
 | 
						|
  ExtensionDNRStore: "resource://gre/modules/ExtensionDNRStore.sys.mjs",
 | 
						|
  WebRequest: "resource://gre/modules/WebRequest.sys.mjs",
 | 
						|
});
 | 
						|
 | 
						|
import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";
 | 
						|
 | 
						|
const { ExtensionError } = ExtensionUtils;
 | 
						|
 | 
						|
XPCOMUtils.defineLazyPreferenceGetter(
 | 
						|
  lazy,
 | 
						|
  "gMatchRequestsFromOtherExtensions",
 | 
						|
  "extensions.dnr.match_requests_from_other_extensions",
 | 
						|
  false
 | 
						|
);
 | 
						|
 | 
						|
// As documented above:
 | 
						|
// Ruleset precedence: session > dynamic > static (order from manifest.json).
 | 
						|
const PRECEDENCE_SESSION_RULESET = 1;
 | 
						|
const PRECEDENCE_DYNAMIC_RULESET = 2;
 | 
						|
const PRECEDENCE_STATIC_RULESETS_BASE = 3;
 | 
						|
 | 
						|
// The RuleCondition class represents a rule's "condition" type as described in
 | 
						|
// schemas/declarative_net_request.json. This class exists to allow the JS
 | 
						|
// engine to use one Shape for all Rule instances.
 | 
						|
class RuleCondition {
 | 
						|
  #compiledUrlFilter;
 | 
						|
  #compiledRegexFilter;
 | 
						|
 | 
						|
  constructor(cond) {
 | 
						|
    this.urlFilter = cond.urlFilter;
 | 
						|
    this.regexFilter = cond.regexFilter;
 | 
						|
    this.isUrlFilterCaseSensitive = cond.isUrlFilterCaseSensitive;
 | 
						|
    this.initiatorDomains = cond.initiatorDomains;
 | 
						|
    this.excludedInitiatorDomains = cond.excludedInitiatorDomains;
 | 
						|
    this.requestDomains = cond.requestDomains;
 | 
						|
    this.excludedRequestDomains = cond.excludedRequestDomains;
 | 
						|
    this.resourceTypes = cond.resourceTypes;
 | 
						|
    this.excludedResourceTypes = cond.excludedResourceTypes;
 | 
						|
    this.requestMethods = cond.requestMethods;
 | 
						|
    this.excludedRequestMethods = cond.excludedRequestMethods;
 | 
						|
    this.domainType = cond.domainType;
 | 
						|
    this.tabIds = cond.tabIds;
 | 
						|
    this.excludedTabIds = cond.excludedTabIds;
 | 
						|
  }
 | 
						|
 | 
						|
  // See CompiledUrlFilter for documentation.
 | 
						|
  urlFilterMatches(requestDataForUrlFilter) {
 | 
						|
    if (!this.#compiledUrlFilter) {
 | 
						|
      // eslint-disable-next-line no-use-before-define
 | 
						|
      this.#compiledUrlFilter = new CompiledUrlFilter(
 | 
						|
        this.urlFilter,
 | 
						|
        this.isUrlFilterCaseSensitive
 | 
						|
      );
 | 
						|
    }
 | 
						|
    return this.#compiledUrlFilter.matchesRequest(requestDataForUrlFilter);
 | 
						|
  }
 | 
						|
 | 
						|
  // Used for testing regexFilter matches in RuleEvaluator.#matchRuleCondition
 | 
						|
  // and to get redirect URL from regexSubstitution in applyRegexSubstitution.
 | 
						|
  getCompiledRegexFilter() {
 | 
						|
    return this.#compiledRegexFilter;
 | 
						|
  }
 | 
						|
 | 
						|
  // RuleValidator compiles regexFilter before this Rule class is instantiated.
 | 
						|
  // To avoid unnecessarily compiling it again, the result is assigned here.
 | 
						|
  setCompiledRegexFilter(compiledRegexFilter) {
 | 
						|
    this.#compiledRegexFilter = compiledRegexFilter;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
export class Rule {
 | 
						|
  constructor(rule) {
 | 
						|
    this.id = rule.id;
 | 
						|
    this.priority = rule.priority;
 | 
						|
    this.condition = new RuleCondition(rule.condition);
 | 
						|
    this.action = rule.action;
 | 
						|
  }
 | 
						|
 | 
						|
  // The precedence of rules within an extension. This method is frequently
 | 
						|
  // used during the first pass of the RequestEvaluator.
 | 
						|
  actionPrecedence() {
 | 
						|
    switch (this.action.type) {
 | 
						|
      case "allow":
 | 
						|
        return 1; // Highest precedence.
 | 
						|
      case "allowAllRequests":
 | 
						|
        return 2;
 | 
						|
      case "block":
 | 
						|
        return 3;
 | 
						|
      case "upgradeScheme":
 | 
						|
        return 4;
 | 
						|
      case "redirect":
 | 
						|
        return 5;
 | 
						|
      case "modifyHeaders":
 | 
						|
        return 6;
 | 
						|
      default:
 | 
						|
        throw new Error(`Unexpected action type: ${this.action.type}`);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  isAllowOrAllowAllRequestsAction() {
 | 
						|
    const type = this.action.type;
 | 
						|
    return type === "allow" || type === "allowAllRequests";
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class Ruleset {
 | 
						|
  /**
 | 
						|
   * @typedef {number} integer
 | 
						|
   *
 | 
						|
   * @param {string} rulesetId - extension-defined ruleset ID.
 | 
						|
   * @param {integer} rulesetPrecedence
 | 
						|
   * @param {Rule[]} rules - extension-defined rules
 | 
						|
   * @param {RuleManager} ruleManager - owner of this ruleset.
 | 
						|
   */
 | 
						|
  constructor(rulesetId, rulesetPrecedence, rules, ruleManager) {
 | 
						|
    this.id = rulesetId;
 | 
						|
    this.rulesetPrecedence = rulesetPrecedence;
 | 
						|
    this.rules = rules;
 | 
						|
    // For use by MatchedRule.
 | 
						|
    this.ruleManager = ruleManager;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * @param {string} uriQuery - The query of a nsIURI to transform.
 | 
						|
 * @param {object} queryTransform - The value of the
 | 
						|
 *   Rule.action.redirect.transform.queryTransform property as defined in
 | 
						|
 *   declarative_net_request.json.
 | 
						|
 * @returns {string} The uriQuery with the queryTransform applied to it.
 | 
						|
 */
 | 
						|
function applyQueryTransform(uriQuery, queryTransform) {
 | 
						|
  // URLSearchParams cannot be applied to the full query string, because that
 | 
						|
  // API formats the full query string using form-urlencoding. But the input
 | 
						|
  // may be in a different format. So we try to only modify matched params.
 | 
						|
 | 
						|
  function urlencode(s) {
 | 
						|
    // Encode in application/x-www-form-urlencoded format.
 | 
						|
    // The only JS API to do that is URLSearchParams. encodeURIComponent is not
 | 
						|
    // the same, it differs in how it handles " " ("%20") and "!'()~" (raw).
 | 
						|
    // But urlencoded space should be "+" and the latter be "%21%27%28%29%7E".
 | 
						|
    return new URLSearchParams({ s }).toString().slice(2);
 | 
						|
  }
 | 
						|
  if (!uriQuery.length && !queryTransform.addOrReplaceParams) {
 | 
						|
    // Nothing to do.
 | 
						|
    return "";
 | 
						|
  }
 | 
						|
  const removeParamsSet = new Set(queryTransform.removeParams?.map(urlencode));
 | 
						|
  const addParams = (queryTransform.addOrReplaceParams || []).map(orig => ({
 | 
						|
    normalizedKey: urlencode(orig.key),
 | 
						|
    orig,
 | 
						|
  }));
 | 
						|
  const finalParams = [];
 | 
						|
  if (uriQuery.length) {
 | 
						|
    for (let part of uriQuery.split("&")) {
 | 
						|
      let key = part.split("=", 1)[0];
 | 
						|
      if (removeParamsSet.has(key)) {
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
      let i = addParams.findIndex(p => p.normalizedKey === key);
 | 
						|
      if (i !== -1) {
 | 
						|
        // Replace found param with the key-value from addOrReplaceParams.
 | 
						|
        finalParams.push(`${key}=${urlencode(addParams[i].orig.value)}`);
 | 
						|
        // Omit param so that a future search for the same key can find the next
 | 
						|
        // specified key-value pair, if any. And to prevent the already-used
 | 
						|
        // key-value pairs from being appended after the loop.
 | 
						|
        addParams.splice(i, 1);
 | 
						|
      } else {
 | 
						|
        finalParams.push(part);
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
  // Append remaining, unused key-value pairs.
 | 
						|
  for (let { normalizedKey, orig } of addParams) {
 | 
						|
    if (!orig.replaceOnly) {
 | 
						|
      finalParams.push(`${normalizedKey}=${urlencode(orig.value)}`);
 | 
						|
    }
 | 
						|
  }
 | 
						|
  return finalParams.length ? `?${finalParams.join("&")}` : "";
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * @param {nsIURI} uri - Usually a http(s) URL.
 | 
						|
 * @param {object} transform - The value of the Rule.action.redirect.transform
 | 
						|
 *   property as defined in declarative_net_request.json.
 | 
						|
 * @returns {nsIURI} uri - The new URL.
 | 
						|
 * @throws if the transformation is invalid.
 | 
						|
 */
 | 
						|
function applyURLTransform(uri, transform) {
 | 
						|
  let mut = uri.mutate();
 | 
						|
  if (transform.scheme) {
 | 
						|
    // Note: declarative_net_request.json only allows http(s)/moz-extension:.
 | 
						|
    mut.setScheme(transform.scheme);
 | 
						|
    if (uri.port !== -1 || transform.port) {
 | 
						|
      // If the URI contains a port or transform.port was specified, the default
 | 
						|
      // port is significant. So we must set it in that case.
 | 
						|
      if (transform.scheme === "https") {
 | 
						|
        mut.QueryInterface(Ci.nsIStandardURLMutator).setDefaultPort(443);
 | 
						|
      } else if (transform.scheme === "http") {
 | 
						|
        mut.QueryInterface(Ci.nsIStandardURLMutator).setDefaultPort(80);
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
  if (transform.username != null) {
 | 
						|
    mut.setUsername(transform.username);
 | 
						|
  }
 | 
						|
  if (transform.password != null) {
 | 
						|
    mut.setPassword(transform.password);
 | 
						|
  }
 | 
						|
  if (transform.host != null) {
 | 
						|
    mut.setHost(transform.host);
 | 
						|
  }
 | 
						|
  if (transform.port != null) {
 | 
						|
    // The caller ensures that transform.port is a string consisting of digits
 | 
						|
    // only. When it is an empty string, it should be cleared (-1).
 | 
						|
    mut.setPort(transform.port || -1);
 | 
						|
  }
 | 
						|
  if (transform.path != null) {
 | 
						|
    mut.setFilePath(transform.path);
 | 
						|
  }
 | 
						|
  if (transform.query != null) {
 | 
						|
    mut.setQuery(transform.query);
 | 
						|
  } else if (transform.queryTransform) {
 | 
						|
    mut.setQuery(applyQueryTransform(uri.query, transform.queryTransform));
 | 
						|
  }
 | 
						|
  if (transform.fragment != null) {
 | 
						|
    mut.setRef(transform.fragment);
 | 
						|
  }
 | 
						|
  return mut.finalize();
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * @param {nsIURI} uri - Usually a http(s) URL.
 | 
						|
 * @param {MatchedRule} matchedRule - The matched rule with a regexFilter
 | 
						|
 *   condition and regexSubstitution action.
 | 
						|
 * @returns {nsIURI} The new URL derived from the regexSubstitution combined
 | 
						|
 *   with capturing group from regexFilter applied to the input uri.
 | 
						|
 * @throws if the resulting URL is an invalid redirect target.
 | 
						|
 */
 | 
						|
function applyRegexSubstitution(uri, matchedRule) {
 | 
						|
  const rule = matchedRule.rule;
 | 
						|
  const extension = matchedRule.ruleManager.extension;
 | 
						|
  const regexSubstitution = rule.action.redirect.regexSubstitution;
 | 
						|
  const compiledRegexFilter = rule.condition.getCompiledRegexFilter();
 | 
						|
  // This method being called implies that regexFilter matched, so |matches| is
 | 
						|
  // always non-null, i.e. an array of string/undefined values.
 | 
						|
  const matches = compiledRegexFilter.exec(uri.spec);
 | 
						|
 | 
						|
  let redirectUrl = regexSubstitution.replace(/\\(.)/g, (_, char) => {
 | 
						|
    // #checkActionRedirect ensures that every \ is followed by a \ or digit.
 | 
						|
    return char === "\\" ? char : matches[char] ?? "";
 | 
						|
  });
 | 
						|
 | 
						|
  // Throws if the URL is invalid:
 | 
						|
  let redirectUri;
 | 
						|
  try {
 | 
						|
    redirectUri = Services.io.newURI(redirectUrl);
 | 
						|
  } catch (e) {
 | 
						|
    throw new Error(
 | 
						|
      `Extension ${extension.id} tried to redirect to an invalid URL: ${redirectUrl}`
 | 
						|
    );
 | 
						|
  }
 | 
						|
  if (!extension.checkLoadURI(redirectUri, { dontReportErrors: true })) {
 | 
						|
    throw new Error(
 | 
						|
      `Extension ${extension.id} may not redirect to: ${redirectUrl}`
 | 
						|
    );
 | 
						|
  }
 | 
						|
  return redirectUri;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * An urlFilter is a string pattern to match a canonical http(s) URL.
 | 
						|
 * urlFilter matches anywhere in the string, unless an anchor is present:
 | 
						|
 * - ||... ("Domain name anchor") - domain or subdomain starts with ...
 | 
						|
 * - |... ("Left anchor") - URL starts with ...
 | 
						|
 * - ...| ("Right anchor") - URL ends with ...
 | 
						|
 *
 | 
						|
 * Other than the anchors, the following special characters exist:
 | 
						|
 * - ^ = end of URL, or any char except: alphanum _ - . % ("Separator")
 | 
						|
 * - * = any number of characters ("Wildcard")
 | 
						|
 *
 | 
						|
 * Ambiguous cases (undocumented but actual Chrome behavior):
 | 
						|
 * - Plain "||" is a domain name anchor, not left + empty + right anchor.
 | 
						|
 * - "^" repeated at end of pattern: "^" matches end of URL only once.
 | 
						|
 * - "^|" at end of pattern: "^" is allowed to match end of URL.
 | 
						|
 *
 | 
						|
 * Implementation details:
 | 
						|
 * - CompiledUrlFilter's constructor (+#initializeUrlFilter) extracts the
 | 
						|
 *   actual urlFilter and anchors, for matching against URLs later.
 | 
						|
 * - RequestDataForUrlFilter class precomputes the URL / domain anchors to
 | 
						|
 *   support matching more efficiently.
 | 
						|
 * - CompiledUrlFilter's matchesRequest(request) checks whether the request is
 | 
						|
 *   actually matched, using the precomputed information.
 | 
						|
 *
 | 
						|
 * The class was designed to minimize the number of string allocations during
 | 
						|
 * request evaluation, because the matchesRequest method may be called very
 | 
						|
 * often for every network request.
 | 
						|
 */
 | 
						|
class CompiledUrlFilter {
 | 
						|
  #isUrlFilterCaseSensitive;
 | 
						|
  #urlFilterParts; // = parts of urlFilter, minus anchors, split at "*".
 | 
						|
  // isAnchorLeft and isAnchorDomain are mutually exclusive.
 | 
						|
  #isAnchorLeft = false;
 | 
						|
  #isAnchorDomain = false;
 | 
						|
  #isAnchorRight = false;
 | 
						|
  #isTrailingSeparator = false; // Whether urlFilter ends with "^".
 | 
						|
 | 
						|
  /**
 | 
						|
   * @param {string} urlFilter - non-empty urlFilter
 | 
						|
   * @param {boolean} [isUrlFilterCaseSensitive]
 | 
						|
   */
 | 
						|
  constructor(urlFilter, isUrlFilterCaseSensitive) {
 | 
						|
    this.#isUrlFilterCaseSensitive = isUrlFilterCaseSensitive;
 | 
						|
    this.#initializeUrlFilter(urlFilter, isUrlFilterCaseSensitive);
 | 
						|
  }
 | 
						|
 | 
						|
  #initializeUrlFilter(urlFilter, isUrlFilterCaseSensitive) {
 | 
						|
    let start = 0;
 | 
						|
    let end = urlFilter.length;
 | 
						|
 | 
						|
    // First, trim the anchors off urlFilter.
 | 
						|
    if (urlFilter[0] === "|") {
 | 
						|
      if (urlFilter[1] === "|") {
 | 
						|
        start = 2;
 | 
						|
        this.#isAnchorDomain = true;
 | 
						|
        // ^ will not revert to false below, because "||*" is already rejected
 | 
						|
        // by RuleValidator's #checkCondUrlFilterAndRegexFilter method.
 | 
						|
      } else {
 | 
						|
        start = 1;
 | 
						|
        this.#isAnchorLeft = true; // may revert to false below.
 | 
						|
      }
 | 
						|
    }
 | 
						|
    if (end > start && urlFilter[end - 1] === "|") {
 | 
						|
      --end;
 | 
						|
      this.#isAnchorRight = true; // may revert to false below.
 | 
						|
    }
 | 
						|
 | 
						|
    // Skip unnecessary wildcards, and adjust meaningless anchors accordingly:
 | 
						|
    // "|*" and "*|" are not effective anchors, they could have been omitted.
 | 
						|
    while (start < end && urlFilter[start] === "*") {
 | 
						|
      ++start;
 | 
						|
      this.#isAnchorLeft = false;
 | 
						|
    }
 | 
						|
    while (end > start && urlFilter[end - 1] === "*") {
 | 
						|
      --end;
 | 
						|
      this.#isAnchorRight = false;
 | 
						|
    }
 | 
						|
 | 
						|
    // Special-case the last "^", so that the matching algorithm can rely on
 | 
						|
    // the simple assumption that a "^" in the filter matches exactly one char:
 | 
						|
    // The "^" at the end of the pattern is specified to match either one char
 | 
						|
    // as usual, or as an anchor for the end of the URL (i.e. zero characters).
 | 
						|
    this.#isTrailingSeparator = urlFilter[end - 1] === "^";
 | 
						|
 | 
						|
    let urlFilterWithoutAnchors = urlFilter.slice(start, end);
 | 
						|
    if (!isUrlFilterCaseSensitive) {
 | 
						|
      urlFilterWithoutAnchors = urlFilterWithoutAnchors.toLowerCase();
 | 
						|
    }
 | 
						|
    this.#urlFilterParts = urlFilterWithoutAnchors.split("*");
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Tests whether |request| matches the urlFilter.
 | 
						|
   *
 | 
						|
   * @param {RequestDataForUrlFilter} requestDataForUrlFilter
 | 
						|
   * @returns {boolean} Whether the condition matches the URL.
 | 
						|
   */
 | 
						|
  matchesRequest(requestDataForUrlFilter) {
 | 
						|
    const url = requestDataForUrlFilter.getUrl(this.#isUrlFilterCaseSensitive);
 | 
						|
    const domainAnchors = requestDataForUrlFilter.domainAnchors;
 | 
						|
 | 
						|
    const urlFilterParts = this.#urlFilterParts;
 | 
						|
 | 
						|
    const REAL_END_OF_URL = url.length - 1; // minus trailing "^"
 | 
						|
 | 
						|
    // atUrlIndex is the position after the most recently matched part.
 | 
						|
    // If a match is not found, it is -1 and we should return false.
 | 
						|
    let atUrlIndex = 0;
 | 
						|
 | 
						|
    // The head always exists, potentially even an empty string.
 | 
						|
    const head = urlFilterParts[0];
 | 
						|
    if (this.#isAnchorLeft) {
 | 
						|
      if (!this.#startsWithPart(head, url, 0)) {
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
      atUrlIndex = head.length;
 | 
						|
    } else if (this.#isAnchorDomain) {
 | 
						|
      atUrlIndex = this.#indexAfterDomainPart(head, url, domainAnchors);
 | 
						|
    } else {
 | 
						|
      atUrlIndex = this.#indexAfterPart(head, url, 0);
 | 
						|
    }
 | 
						|
 | 
						|
    let previouslyAtUrlIndex = 0;
 | 
						|
    for (let i = 1; i < urlFilterParts.length && atUrlIndex !== -1; ++i) {
 | 
						|
      previouslyAtUrlIndex = atUrlIndex;
 | 
						|
      atUrlIndex = this.#indexAfterPart(urlFilterParts[i], url, atUrlIndex);
 | 
						|
    }
 | 
						|
    if (atUrlIndex === -1) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    if (atUrlIndex === url.length) {
 | 
						|
      // We always append a "^" to the URL, so if the match is at the end of the
 | 
						|
      // URL (REAL_END_OF_URL), only accept if the pattern ended with a "^".
 | 
						|
      return this.#isTrailingSeparator;
 | 
						|
    }
 | 
						|
    if (!this.#isAnchorRight || atUrlIndex === REAL_END_OF_URL) {
 | 
						|
      // Either not interested in the end, or already at the end of the URL.
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
 | 
						|
    // #isAnchorRight is true but we are not at the end of the URL.
 | 
						|
    // Backtrack once, to retry the last pattern (tail) with the end of the URL.
 | 
						|
 | 
						|
    const tail = urlFilterParts[urlFilterParts.length - 1];
 | 
						|
    // The expected offset where the tail should be located.
 | 
						|
    const expectedTailIndex = REAL_END_OF_URL - tail.length;
 | 
						|
    // If #isTrailingSeparator is true, then accept the URL's trailing "^".
 | 
						|
    const expectedTailIndexPlus1 = expectedTailIndex + 1;
 | 
						|
    if (urlFilterParts.length === 1) {
 | 
						|
      if (this.#isAnchorLeft) {
 | 
						|
        // If matched, we would have returned at the REAL_END_OF_URL checks.
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
      if (this.#isAnchorDomain) {
 | 
						|
        // The tail must be exactly at one of the domain anchors.
 | 
						|
        return (
 | 
						|
          (domainAnchors.includes(expectedTailIndex) &&
 | 
						|
            this.#startsWithPart(tail, url, expectedTailIndex)) ||
 | 
						|
          (this.#isTrailingSeparator &&
 | 
						|
            domainAnchors.includes(expectedTailIndexPlus1) &&
 | 
						|
            this.#startsWithPart(tail, url, expectedTailIndexPlus1))
 | 
						|
        );
 | 
						|
      }
 | 
						|
      // head has no left/domain anchor, fall through.
 | 
						|
    }
 | 
						|
    // The tail is not left/domain anchored, accept it as long as it did not
 | 
						|
    // overlap with an already-matched part of the URL.
 | 
						|
    return (
 | 
						|
      (expectedTailIndex > previouslyAtUrlIndex &&
 | 
						|
        this.#startsWithPart(tail, url, expectedTailIndex)) ||
 | 
						|
      (this.#isTrailingSeparator &&
 | 
						|
        expectedTailIndexPlus1 > previouslyAtUrlIndex &&
 | 
						|
        this.#startsWithPart(tail, url, expectedTailIndexPlus1))
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  // Whether a character should match "^" in an urlFilter.
 | 
						|
  // The "match end of URL" meaning of "^" is covered by #isTrailingSeparator.
 | 
						|
  static #regexIsSep = /[^A-Za-z0-9_\-.%]/;
 | 
						|
 | 
						|
  #matchPartAt(part, url, urlIndex, sepStart) {
 | 
						|
    if (sepStart === -1) {
 | 
						|
      // Fast path.
 | 
						|
      return url.startsWith(part, urlIndex);
 | 
						|
    }
 | 
						|
    if (urlIndex + part.length > url.length) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    for (let i = 0; i < part.length; ++i) {
 | 
						|
      let partChar = part[i];
 | 
						|
      let urlChar = url[urlIndex + i];
 | 
						|
      if (
 | 
						|
        partChar !== urlChar &&
 | 
						|
        (partChar !== "^" || !CompiledUrlFilter.#regexIsSep.test(urlChar))
 | 
						|
      ) {
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return true;
 | 
						|
  }
 | 
						|
 | 
						|
  #startsWithPart(part, url, urlIndex) {
 | 
						|
    const sepStart = part.indexOf("^");
 | 
						|
    return this.#matchPartAt(part, url, urlIndex, sepStart);
 | 
						|
  }
 | 
						|
 | 
						|
  #indexAfterPart(part, url, urlIndex) {
 | 
						|
    let sepStart = part.indexOf("^");
 | 
						|
    if (sepStart === -1) {
 | 
						|
      // Fast path.
 | 
						|
      let i = url.indexOf(part, urlIndex);
 | 
						|
      return i === -1 ? i : i + part.length;
 | 
						|
    }
 | 
						|
    let maxUrlIndex = url.length - part.length;
 | 
						|
    for (let i = urlIndex; i <= maxUrlIndex; ++i) {
 | 
						|
      if (this.#matchPartAt(part, url, i, sepStart)) {
 | 
						|
        return i + part.length;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return -1;
 | 
						|
  }
 | 
						|
 | 
						|
  #indexAfterDomainPart(part, url, domainAnchors) {
 | 
						|
    const sepStart = part.indexOf("^");
 | 
						|
    for (let offset of domainAnchors) {
 | 
						|
      if (this.#matchPartAt(part, url, offset, sepStart)) {
 | 
						|
        return offset + part.length;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return -1;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
// See CompiledUrlFilter for documentation of RequestDataForUrlFilter.
 | 
						|
class RequestDataForUrlFilter {
 | 
						|
  /**
 | 
						|
   * @param {string} requestURIspec - The URL to match against.
 | 
						|
   */
 | 
						|
  constructor(requestURIspec) {
 | 
						|
    // "^" is appended, see CompiledUrlFilter's #initializeUrlFilter.
 | 
						|
    this.urlAnyCase = requestURIspec + "^";
 | 
						|
    this.urlLowerCase = this.urlAnyCase.toLowerCase();
 | 
						|
    // For "||..." (Domain name anchor): where (sub)domains start in the URL.
 | 
						|
    this.domainAnchors = this.#getDomainAnchors(this.urlAnyCase);
 | 
						|
  }
 | 
						|
 | 
						|
  getUrl(isUrlFilterCaseSensitive) {
 | 
						|
    return isUrlFilterCaseSensitive ? this.urlAnyCase : this.urlLowerCase;
 | 
						|
  }
 | 
						|
 | 
						|
  #getDomainAnchors(url) {
 | 
						|
    let hostStart = url.indexOf("://") + 3;
 | 
						|
    let hostEnd = url.indexOf("/", hostStart);
 | 
						|
    let userpassEnd = url.lastIndexOf("@", hostEnd) + 1;
 | 
						|
    if (userpassEnd) {
 | 
						|
      hostStart = userpassEnd;
 | 
						|
    }
 | 
						|
    let host = url.slice(hostStart, hostEnd);
 | 
						|
    let domainAnchors = [hostStart];
 | 
						|
    let offset = 0;
 | 
						|
    // Find all offsets after ".". If not found, -1 + 1 = 0, and the loop ends.
 | 
						|
    while ((offset = host.indexOf(".", offset) + 1)) {
 | 
						|
      domainAnchors.push(hostStart + offset);
 | 
						|
    }
 | 
						|
    return domainAnchors;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function compileRegexFilter(regexFilter, isUrlFilterCaseSensitive) {
 | 
						|
  // TODO bug 1821033: Restrict supported regex to avoid perf issues. For
 | 
						|
  // discussion on the desired syntax, see
 | 
						|
  // https://github.com/w3c/webextensions/issues/344
 | 
						|
  return new RegExp(regexFilter, isUrlFilterCaseSensitive ? "" : "i");
 | 
						|
}
 | 
						|
 | 
						|
class ModifyHeadersBase {
 | 
						|
  // Map<string,MatchedRule> - The first MatchedRule that modified the header.
 | 
						|
  // After modifying a header, it cannot be modified further, with the exception
 | 
						|
  // of the "append" operation, provided that they are from the same extension.
 | 
						|
  #alreadyModifiedMap = new Map();
 | 
						|
  // Set<string> - The list of headers allowed to be modified with "append",
 | 
						|
  // despite having been modified. Allowed for "set"/"append", not for "remove".
 | 
						|
  #appendStillAllowed = new Set();
 | 
						|
 | 
						|
  /**
 | 
						|
   * @param {ChannelWrapper} channel
 | 
						|
   */
 | 
						|
  constructor(channel) {
 | 
						|
    this.channel = channel;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * @param {MatchedRule} _matchedRule
 | 
						|
   * @returns {object[]}
 | 
						|
   */
 | 
						|
  headerActionsFor(_matchedRule) {
 | 
						|
    throw new Error("Not implemented.");
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * @param {MatchedRule} _matchedrule
 | 
						|
   * @param {string} _name
 | 
						|
   * @param {string} _value
 | 
						|
   * @param {boolean} _merge
 | 
						|
   */
 | 
						|
  setHeaderImpl(_matchedrule, _name, _value, _merge) {
 | 
						|
    throw new Error("Not implemented.");
 | 
						|
  }
 | 
						|
 | 
						|
  /** @param {MatchedRule[]} matchedRules */
 | 
						|
  applyModifyHeaders(matchedRules) {
 | 
						|
    for (const matchedRule of matchedRules) {
 | 
						|
      for (const headerAction of this.headerActionsFor(matchedRule)) {
 | 
						|
        const { header: name, operation, value } = headerAction;
 | 
						|
        if (!this.#isOperationAllowed(name, operation, matchedRule)) {
 | 
						|
          continue;
 | 
						|
        }
 | 
						|
        let ok;
 | 
						|
        switch (operation) {
 | 
						|
          case "set":
 | 
						|
            ok = this.setHeader(matchedRule, name, value, /* merge */ false);
 | 
						|
            if (ok) {
 | 
						|
              this.#appendStillAllowed.add(name);
 | 
						|
            }
 | 
						|
            break;
 | 
						|
          case "append":
 | 
						|
            ok = this.setHeader(matchedRule, name, value, /* merge */ true);
 | 
						|
            if (ok) {
 | 
						|
              this.#appendStillAllowed.add(name);
 | 
						|
            }
 | 
						|
            break;
 | 
						|
          case "remove":
 | 
						|
            ok = this.setHeader(matchedRule, name, "", /* merge */ false);
 | 
						|
            // Note: removal is final, so we don't add to #appendStillAllowed.
 | 
						|
            break;
 | 
						|
        }
 | 
						|
        if (ok) {
 | 
						|
          this.#alreadyModifiedMap.set(name, matchedRule);
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  #isOperationAllowed(name, operation, matchedRule) {
 | 
						|
    const modifiedBy = this.#alreadyModifiedMap.get(name);
 | 
						|
    if (!modifiedBy) {
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
    if (
 | 
						|
      operation === "append" &&
 | 
						|
      this.#appendStillAllowed.has(name) &&
 | 
						|
      matchedRule.ruleManager === modifiedBy.ruleManager
 | 
						|
    ) {
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
    // TODO bug 1803369: dev experience improvement: consider logging when
 | 
						|
    // a header modification was rejected.
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
 | 
						|
  setHeader(matchedRule, name, value, merge) {
 | 
						|
    try {
 | 
						|
      this.setHeaderImpl(matchedRule, name, value, merge);
 | 
						|
      return true;
 | 
						|
    } catch (e) {
 | 
						|
      const extension = matchedRule.ruleManager.extension;
 | 
						|
      extension.logger.error(
 | 
						|
        `Failed to apply modifyHeaders action to header "${name}" (DNR rule id ${matchedRule.rule.id} from ruleset "${matchedRule.ruleset.id}"): ${e}`
 | 
						|
      );
 | 
						|
    }
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
 | 
						|
  // kName should already be in lower case.
 | 
						|
  isHeaderNameEqual(name, kName) {
 | 
						|
    return name.length === kName.length && name.toLowerCase() === kName;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class ModifyRequestHeaders extends ModifyHeadersBase {
 | 
						|
  static maybeApplyModifyHeaders(channel, matchedRules) {
 | 
						|
    matchedRules = matchedRules.filter(mr => {
 | 
						|
      const action = mr.rule.action;
 | 
						|
      return action.type === "modifyHeaders" && action.requestHeaders?.length;
 | 
						|
    });
 | 
						|
    if (matchedRules.length) {
 | 
						|
      new ModifyRequestHeaders(channel).applyModifyHeaders(matchedRules);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /** @param {MatchedRule} matchedRule */
 | 
						|
  headerActionsFor(matchedRule) {
 | 
						|
    return matchedRule.rule.action.requestHeaders;
 | 
						|
  }
 | 
						|
 | 
						|
  setHeaderImpl(matchedRule, name, value, merge) {
 | 
						|
    if (this.isHeaderNameEqual(name, "host")) {
 | 
						|
      this.#checkHostHeader(matchedRule, value);
 | 
						|
    }
 | 
						|
    if (merge && value && this.isHeaderNameEqual(name, "cookie")) {
 | 
						|
      // By default, headers are merged with ",". But Cookie should use "; ".
 | 
						|
      // HTTP/1.1 allowed only one Cookie header, but HTTP/2.0 allows multiple,
 | 
						|
      // but recommends concatenation on one line. Relevant RFCs:
 | 
						|
      // - https://www.rfc-editor.org/rfc/rfc6265#section-5.4
 | 
						|
      // - https://www.rfc-editor.org/rfc/rfc7540#section-8.1.2.5
 | 
						|
      // Consistent with Firefox internals, we ensure that there is at most one
 | 
						|
      // Cookie header, by overwriting the previous one, if any.
 | 
						|
      let existingCookie = this.channel.getRequestHeader("cookie");
 | 
						|
      if (existingCookie) {
 | 
						|
        value = existingCookie + "; " + value;
 | 
						|
        merge = false;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    this.channel.setRequestHeader(name, value, merge);
 | 
						|
  }
 | 
						|
 | 
						|
  #checkHostHeader(matchedRule, value) {
 | 
						|
    let uri = Services.io.newURI(`https://${value}/`);
 | 
						|
    let { policy } = matchedRule.ruleManager.extension;
 | 
						|
 | 
						|
    if (!policy.allowedOrigins.matches(uri)) {
 | 
						|
      throw new Error(
 | 
						|
        `Unable to set host header, url missing from permissions.`
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    if (WebExtensionPolicy.isRestrictedURI(uri)) {
 | 
						|
      throw new Error(`Unable to set host header to restricted url.`);
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class ModifyResponseHeaders extends ModifyHeadersBase {
 | 
						|
  static maybeApplyModifyHeaders(channel, matchedRules) {
 | 
						|
    matchedRules = matchedRules.filter(mr => {
 | 
						|
      const action = mr.rule.action;
 | 
						|
      return action.type === "modifyHeaders" && action.responseHeaders?.length;
 | 
						|
    });
 | 
						|
    if (matchedRules.length) {
 | 
						|
      new ModifyResponseHeaders(channel).applyModifyHeaders(matchedRules);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  headerActionsFor(matchedRule) {
 | 
						|
    return matchedRule.rule.action.responseHeaders;
 | 
						|
  }
 | 
						|
 | 
						|
  setHeaderImpl(matchedRule, name, value, merge) {
 | 
						|
    this.channel.setResponseHeader(name, value, merge);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class RuleValidator {
 | 
						|
  constructor(alreadyValidatedRules, { isSessionRuleset = false } = {}) {
 | 
						|
    this.rulesMap = new Map(alreadyValidatedRules.map(r => [r.id, r]));
 | 
						|
    this.failures = [];
 | 
						|
    this.isSessionRuleset = isSessionRuleset;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Static method used to deserialize Rule class instances from a plain
 | 
						|
   * js object rule as serialized implicitly by aomStartup.encodeBlob
 | 
						|
   * when we store the rules into the startup cache file.
 | 
						|
   *
 | 
						|
   * @param {object} rule
 | 
						|
   * @returns {Rule}
 | 
						|
   */
 | 
						|
  static deserializeRule(rule) {
 | 
						|
    const newRule = new Rule(rule);
 | 
						|
    if (newRule.condition.regexFilter) {
 | 
						|
      newRule.condition.setCompiledRegexFilter(
 | 
						|
        compileRegexFilter(
 | 
						|
          newRule.condition.regexFilter,
 | 
						|
          newRule.condition.isUrlFilterCaseSensitive
 | 
						|
        )
 | 
						|
      );
 | 
						|
    }
 | 
						|
    return newRule;
 | 
						|
  }
 | 
						|
 | 
						|
  removeRuleIds(ruleIds) {
 | 
						|
    for (const ruleId of ruleIds) {
 | 
						|
      this.rulesMap.delete(ruleId);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * @param {object[]} rules - A list of objects that adhere to the Rule type
 | 
						|
   *    from declarative_net_request.json.
 | 
						|
   */
 | 
						|
  addRules(rules) {
 | 
						|
    for (const rule of rules) {
 | 
						|
      if (this.rulesMap.has(rule.id)) {
 | 
						|
        this.#collectInvalidRule(rule, `Duplicate rule ID: ${rule.id}`);
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
      // declarative_net_request.json defines basic types, such as the expected
 | 
						|
      // object properties and (primitive) type. Trivial constraints such as
 | 
						|
      // minimum array lengths are also expressed in the schema.
 | 
						|
      // Anything more complex is validated here. In particular, constraints
 | 
						|
      // involving multiple properties (e.g. mutual exclusiveness).
 | 
						|
      //
 | 
						|
      // The following conditions have already been validated by the schema:
 | 
						|
      // - isUrlFilterCaseSensitive (boolean)
 | 
						|
      // - domainType (enum string)
 | 
						|
      // - initiatorDomains & excludedInitiatorDomains & requestDomains &
 | 
						|
      //   excludedRequestDomains (array of string in canonicalDomain format)
 | 
						|
      if (
 | 
						|
        !this.#checkCondResourceTypes(rule) ||
 | 
						|
        !this.#checkCondRequestMethods(rule) ||
 | 
						|
        !this.#checkCondTabIds(rule) ||
 | 
						|
        !this.#checkCondUrlFilterAndRegexFilter(rule) ||
 | 
						|
        !this.#checkAction(rule)
 | 
						|
      ) {
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
 | 
						|
      const newRule = new Rule(rule);
 | 
						|
      // #lastCompiledRegexFilter is set if regexFilter is set, and null
 | 
						|
      // otherwise by the above call to #checkCondUrlFilterAndRegexFilter().
 | 
						|
      if (this.#lastCompiledRegexFilter) {
 | 
						|
        newRule.condition.setCompiledRegexFilter(this.#lastCompiledRegexFilter);
 | 
						|
      }
 | 
						|
 | 
						|
      this.rulesMap.set(rule.id, newRule);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  // #checkCondUrlFilterAndRegexFilter() compiles the regexFilter to check its
 | 
						|
  // validity. To avoid having to compile it again when the Rule (RuleCondition)
 | 
						|
  // is constructed, we temporarily cache the result.
 | 
						|
  #lastCompiledRegexFilter;
 | 
						|
 | 
						|
  // Checks: resourceTypes & excludedResourceTypes
 | 
						|
  #checkCondResourceTypes(rule) {
 | 
						|
    const { resourceTypes, excludedResourceTypes } = rule.condition;
 | 
						|
    if (this.#hasOverlap(resourceTypes, excludedResourceTypes)) {
 | 
						|
      this.#collectInvalidRule(
 | 
						|
        rule,
 | 
						|
        "resourceTypes and excludedResourceTypes should not overlap"
 | 
						|
      );
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    if (rule.action.type === "allowAllRequests") {
 | 
						|
      if (!resourceTypes) {
 | 
						|
        this.#collectInvalidRule(
 | 
						|
          rule,
 | 
						|
          "An allowAllRequests rule must have a non-empty resourceTypes array"
 | 
						|
        );
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
      if (resourceTypes.some(r => r !== "main_frame" && r !== "sub_frame")) {
 | 
						|
        this.#collectInvalidRule(
 | 
						|
          rule,
 | 
						|
          "An allowAllRequests rule may only include main_frame/sub_frame in resourceTypes"
 | 
						|
        );
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return true;
 | 
						|
  }
 | 
						|
 | 
						|
  // Checks: requestMethods & excludedRequestMethods
 | 
						|
  #checkCondRequestMethods(rule) {
 | 
						|
    const { requestMethods, excludedRequestMethods } = rule.condition;
 | 
						|
    if (this.#hasOverlap(requestMethods, excludedRequestMethods)) {
 | 
						|
      this.#collectInvalidRule(
 | 
						|
        rule,
 | 
						|
        "requestMethods and excludedRequestMethods should not overlap"
 | 
						|
      );
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    const isInvalidRequestMethod = method => method.toLowerCase() !== method;
 | 
						|
    if (
 | 
						|
      requestMethods?.some(isInvalidRequestMethod) ||
 | 
						|
      excludedRequestMethods?.some(isInvalidRequestMethod)
 | 
						|
    ) {
 | 
						|
      this.#collectInvalidRule(rule, "request methods must be in lower case");
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    return true;
 | 
						|
  }
 | 
						|
 | 
						|
  // Checks: tabIds & excludedTabIds
 | 
						|
  #checkCondTabIds(rule) {
 | 
						|
    const { tabIds, excludedTabIds } = rule.condition;
 | 
						|
 | 
						|
    if ((tabIds || excludedTabIds) && !this.isSessionRuleset) {
 | 
						|
      this.#collectInvalidRule(
 | 
						|
        rule,
 | 
						|
        "tabIds and excludedTabIds can only be specified in session rules"
 | 
						|
      );
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    if (this.#hasOverlap(tabIds, excludedTabIds)) {
 | 
						|
      this.#collectInvalidRule(
 | 
						|
        rule,
 | 
						|
        "tabIds and excludedTabIds should not overlap"
 | 
						|
      );
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    return true;
 | 
						|
  }
 | 
						|
 | 
						|
  static #regexNonASCII = /[^\x00-\x7F]/; // eslint-disable-line no-control-regex
 | 
						|
  static #regexDigitOrBackslash = /^[0-9\\]$/;
 | 
						|
 | 
						|
  // Checks: urlFilter & regexFilter
 | 
						|
  #checkCondUrlFilterAndRegexFilter(rule) {
 | 
						|
    const { urlFilter, regexFilter } = rule.condition;
 | 
						|
 | 
						|
    this.#lastCompiledRegexFilter = null;
 | 
						|
 | 
						|
    const checkEmptyOrNonASCII = (str, prop) => {
 | 
						|
      if (!str) {
 | 
						|
        this.#collectInvalidRule(rule, `${prop} should not be an empty string`);
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
      // Non-ASCII in URLs are always encoded in % (or punycode in domains).
 | 
						|
      if (RuleValidator.#regexNonASCII.test(str)) {
 | 
						|
        this.#collectInvalidRule(
 | 
						|
          rule,
 | 
						|
          `${prop} should not contain non-ASCII characters`
 | 
						|
        );
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
      return true;
 | 
						|
    };
 | 
						|
    if (urlFilter != null) {
 | 
						|
      if (regexFilter != null) {
 | 
						|
        this.#collectInvalidRule(
 | 
						|
          rule,
 | 
						|
          "urlFilter and regexFilter are mutually exclusive"
 | 
						|
        );
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
      if (!checkEmptyOrNonASCII(urlFilter, "urlFilter")) {
 | 
						|
        // #collectInvalidRule already called by checkEmptyOrNonASCII.
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
      if (urlFilter.startsWith("||*")) {
 | 
						|
        // Rejected because Chrome does too. '||*' is equivalent to '*'.
 | 
						|
        this.#collectInvalidRule(rule, "urlFilter should not start with '||*'");
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
    } else if (regexFilter != null) {
 | 
						|
      if (!checkEmptyOrNonASCII(regexFilter, "regexFilter")) {
 | 
						|
        // #collectInvalidRule already called by checkEmptyOrNonASCII.
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
      try {
 | 
						|
        this.#lastCompiledRegexFilter = compileRegexFilter(
 | 
						|
          regexFilter,
 | 
						|
          rule.condition.isUrlFilterCaseSensitive
 | 
						|
        );
 | 
						|
      } catch (e) {
 | 
						|
        this.#collectInvalidRule(
 | 
						|
          rule,
 | 
						|
          "regexFilter is not a valid regular expression"
 | 
						|
        );
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return true;
 | 
						|
  }
 | 
						|
 | 
						|
  #checkAction(rule) {
 | 
						|
    switch (rule.action.type) {
 | 
						|
      case "allow":
 | 
						|
      case "allowAllRequests":
 | 
						|
      case "block":
 | 
						|
      case "upgradeScheme":
 | 
						|
        // These actions have no extra properties.
 | 
						|
        break;
 | 
						|
      case "redirect":
 | 
						|
        return this.#checkActionRedirect(rule);
 | 
						|
      case "modifyHeaders":
 | 
						|
        return this.#checkActionModifyHeaders(rule);
 | 
						|
      default:
 | 
						|
        // Other values are not possible because declarative_net_request.json
 | 
						|
        // only accepts the above action types.
 | 
						|
        throw new Error(`Unexpected action type: ${rule.action.type}`);
 | 
						|
    }
 | 
						|
    return true;
 | 
						|
  }
 | 
						|
 | 
						|
  #checkActionRedirect(rule) {
 | 
						|
    const { url, extensionPath, transform, regexSubstitution } =
 | 
						|
      rule.action.redirect ?? {};
 | 
						|
    const hasExtensionPath = extensionPath != null;
 | 
						|
    const hasRegexSubstitution = regexSubstitution != null;
 | 
						|
    const redirectKeyCount = // @ts-ignore trivial/noisy
 | 
						|
      !!url + !!hasExtensionPath + !!transform + !!hasRegexSubstitution;
 | 
						|
    if (redirectKeyCount !== 1) {
 | 
						|
      if (redirectKeyCount === 0) {
 | 
						|
        this.#collectInvalidRule(
 | 
						|
          rule,
 | 
						|
          "A redirect rule must have a non-empty action.redirect object"
 | 
						|
        );
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
      // Side note: Chrome silently ignores excess keys, and skips validation
 | 
						|
      // for ignored keys, in this order:
 | 
						|
      // - url > extensionPath > transform > regexSubstitution
 | 
						|
      this.#collectInvalidRule(
 | 
						|
        rule,
 | 
						|
        "redirect.url, redirect.extensionPath, redirect.transform and redirect.regexSubstitution are mutually exclusive"
 | 
						|
      );
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    if (hasExtensionPath && !extensionPath.startsWith("/")) {
 | 
						|
      this.#collectInvalidRule(
 | 
						|
        rule,
 | 
						|
        "redirect.extensionPath should start with a '/'"
 | 
						|
      );
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    // If specified, the "url" property is described as "format": "url" in the
 | 
						|
    // JSON schema, which ensures that the URL is a canonical form, and that
 | 
						|
    // the extension is allowed to trigger a navigation to the URL.
 | 
						|
    // E.g. javascript: and privileged about:-URLs cannot be navigated to, but
 | 
						|
    // http(s) URLs can (regardless of extension permissions).
 | 
						|
    // data:-URLs are currently blocked due to bug 1622986.
 | 
						|
 | 
						|
    if (transform) {
 | 
						|
      if (transform.query != null && transform.queryTransform) {
 | 
						|
        this.#collectInvalidRule(
 | 
						|
          rule,
 | 
						|
          "redirect.transform.query and redirect.transform.queryTransform are mutually exclusive"
 | 
						|
        );
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
      // Most of the validation is done by nsIURIMutator via applyURLTransform.
 | 
						|
      // nsIURIMutator is not very strict, so we perform some extra checks here
 | 
						|
      // to reject values that are not technically valid URLs.
 | 
						|
 | 
						|
      if (transform.port && /\D/.test(transform.port)) {
 | 
						|
        // nsIURIMutator's setPort takes an int, so any string will implicitly
 | 
						|
        // be converted to a number. This part verifies that the input only
 | 
						|
        // consists of digits. setPort will ensure that it is at most 65535.
 | 
						|
        this.#collectInvalidRule(
 | 
						|
          rule,
 | 
						|
          "redirect.transform.port should be empty or an integer"
 | 
						|
        );
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
 | 
						|
      // Note: we don't verify whether transform.query starts with '/', because
 | 
						|
      // Chrome does not require it, and nsIURIMutator prepends it if missing.
 | 
						|
 | 
						|
      if (transform.query && !transform.query.startsWith("?")) {
 | 
						|
        this.#collectInvalidRule(
 | 
						|
          rule,
 | 
						|
          "redirect.transform.query should be empty or start with a '?'"
 | 
						|
        );
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
      if (transform.fragment && !transform.fragment.startsWith("#")) {
 | 
						|
        this.#collectInvalidRule(
 | 
						|
          rule,
 | 
						|
          "redirect.transform.fragment should be empty or start with a '#'"
 | 
						|
        );
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
      try {
 | 
						|
        const dummyURI = Services.io.newURI("http://dummy");
 | 
						|
        // applyURLTransform uses nsIURIMutator to transform a URI, and throws
 | 
						|
        // if |transform| is invalid, e.g. invalid host, port, etc.
 | 
						|
        applyURLTransform(dummyURI, transform);
 | 
						|
      } catch (e) {
 | 
						|
        this.#collectInvalidRule(
 | 
						|
          rule,
 | 
						|
          "redirect.transform does not describe a valid URL transformation"
 | 
						|
        );
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    if (hasRegexSubstitution) {
 | 
						|
      if (!rule.condition.regexFilter) {
 | 
						|
        this.#collectInvalidRule(
 | 
						|
          rule,
 | 
						|
          "redirect.regexSubstitution requires the regexFilter condition to be specified"
 | 
						|
        );
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
      let i = 0;
 | 
						|
      // i will be index after \. Loop breaks if not found (-1+1=0 = false).
 | 
						|
      while ((i = regexSubstitution.indexOf("\\", i) + 1)) {
 | 
						|
        let c = regexSubstitution[i++]; // may be undefined if \ is at end.
 | 
						|
        if (c === undefined || !RuleValidator.#regexDigitOrBackslash.test(c)) {
 | 
						|
          this.#collectInvalidRule(
 | 
						|
            rule,
 | 
						|
            "redirect.regexSubstitution only allows digit or \\ after \\."
 | 
						|
          );
 | 
						|
          return false;
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    return true;
 | 
						|
  }
 | 
						|
 | 
						|
  #checkActionModifyHeaders(rule) {
 | 
						|
    const { requestHeaders, responseHeaders } = rule.action;
 | 
						|
    if (!requestHeaders && !responseHeaders) {
 | 
						|
      this.#collectInvalidRule(
 | 
						|
        rule,
 | 
						|
        "A modifyHeaders rule must have a non-empty requestHeaders or modifyHeaders list"
 | 
						|
      );
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    const isValidModifyHeadersOp = ({ header, operation, value }) => {
 | 
						|
      if (!header) {
 | 
						|
        this.#collectInvalidRule(rule, "header must be non-empty");
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
      if (!value && (operation === "append" || operation === "set")) {
 | 
						|
        this.#collectInvalidRule(
 | 
						|
          rule,
 | 
						|
          "value is required for operations append/set"
 | 
						|
        );
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
      if (value && operation === "remove") {
 | 
						|
        this.#collectInvalidRule(
 | 
						|
          rule,
 | 
						|
          "value must not be provided for operation remove"
 | 
						|
        );
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
      return true;
 | 
						|
    };
 | 
						|
    if (
 | 
						|
      (requestHeaders && !requestHeaders.every(isValidModifyHeadersOp)) ||
 | 
						|
      (responseHeaders && !responseHeaders.every(isValidModifyHeadersOp))
 | 
						|
    ) {
 | 
						|
      // #collectInvalidRule already called by isValidModifyHeadersOp.
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    return true;
 | 
						|
  }
 | 
						|
 | 
						|
  // Conditions with a filter and an exclude-filter should reject overlapping
 | 
						|
  // lists, because they can never simultaneously be true.
 | 
						|
  #hasOverlap(arrayA, arrayB) {
 | 
						|
    return arrayA && arrayB && arrayA.some(v => arrayB.includes(v));
 | 
						|
  }
 | 
						|
 | 
						|
  #collectInvalidRule(rule, message) {
 | 
						|
    this.failures.push({ rule, message });
 | 
						|
  }
 | 
						|
 | 
						|
  getValidatedRules() {
 | 
						|
    return Array.from(this.rulesMap.values());
 | 
						|
  }
 | 
						|
 | 
						|
  getFailures() {
 | 
						|
    return this.failures;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
export class RuleQuotaCounter {
 | 
						|
  constructor(isStaticRulesets) {
 | 
						|
    this.isStaticRulesets = isStaticRulesets;
 | 
						|
    this.ruleLimitName = isStaticRulesets
 | 
						|
      ? "GUARANTEED_MINIMUM_STATIC_RULES"
 | 
						|
      : "MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES";
 | 
						|
    this.ruleLimitRemaining = lazy.ExtensionDNRLimits[this.ruleLimitName];
 | 
						|
    this.regexRemaining = lazy.ExtensionDNRLimits.MAX_NUMBER_OF_REGEX_RULES;
 | 
						|
  }
 | 
						|
 | 
						|
  tryAddRules(rulesetId, rules) {
 | 
						|
    if (rules.length > this.ruleLimitRemaining) {
 | 
						|
      this.#throwQuotaError(rulesetId, "rules", this.ruleLimitName);
 | 
						|
    }
 | 
						|
    let regexCount = 0;
 | 
						|
    for (let rule of rules) {
 | 
						|
      if (rule.condition.regexFilter && ++regexCount > this.regexRemaining) {
 | 
						|
        this.#throwQuotaError(
 | 
						|
          rulesetId,
 | 
						|
          "regexFilter rules",
 | 
						|
          "MAX_NUMBER_OF_REGEX_RULES"
 | 
						|
        );
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // Update counters only when there are no quota errors.
 | 
						|
    this.ruleLimitRemaining -= rules.length;
 | 
						|
    this.regexRemaining -= regexCount;
 | 
						|
  }
 | 
						|
 | 
						|
  #throwQuotaError(rulesetId, what, limitName) {
 | 
						|
    if (this.isStaticRulesets) {
 | 
						|
      throw new ExtensionError(
 | 
						|
        `Number of ${what} across all enabled static rulesets exceeds ${limitName} if ruleset "${rulesetId}" were to be enabled.`
 | 
						|
      );
 | 
						|
    }
 | 
						|
    throw new ExtensionError(
 | 
						|
      `Number of ${what} in ruleset "${rulesetId}" exceeds ${limitName}.`
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Compares two rules to determine the relative order of precedence.
 | 
						|
 * Rules are only comparable if they are from the same extension!
 | 
						|
 *
 | 
						|
 * @param {Rule} ruleA
 | 
						|
 * @param {Rule} ruleB
 | 
						|
 * @param {Ruleset} rulesetA - the ruleset ruleA is part of.
 | 
						|
 * @param {Ruleset} rulesetB - the ruleset ruleB is part of.
 | 
						|
 * @returns {integer}
 | 
						|
 *   0 if equal.
 | 
						|
 *   <0 if ruleA comes before ruleB.
 | 
						|
 *   >0 if ruleA comes after ruleB.
 | 
						|
 */
 | 
						|
function compareRule(ruleA, ruleB, rulesetA, rulesetB) {
 | 
						|
  // Comparators: 0 if equal, >0 if a after b, <0 if a before b.
 | 
						|
  function cmpHighestNumber(a, b) {
 | 
						|
    return a === b ? 0 : b - a;
 | 
						|
  }
 | 
						|
  function cmpLowestNumber(a, b) {
 | 
						|
    return a === b ? 0 : a - b;
 | 
						|
  }
 | 
						|
  return (
 | 
						|
    // All compared operands are non-negative integers.
 | 
						|
    cmpHighestNumber(ruleA.priority, ruleB.priority) ||
 | 
						|
    cmpLowestNumber(ruleA.actionPrecedence(), ruleB.actionPrecedence()) ||
 | 
						|
    // As noted in the big comment at the top of the file, the following two
 | 
						|
    // comparisons only exist in order to have a stable ordering of rules. The
 | 
						|
    // specific comparison is somewhat arbitrary and matches Chrome's behavior.
 | 
						|
    // For context, see https://github.com/w3c/webextensions/issues/280
 | 
						|
    cmpLowestNumber(rulesetA.rulesetPrecedence, rulesetB.rulesetPrecedence) ||
 | 
						|
    cmpLowestNumber(ruleA.id, ruleB.id)
 | 
						|
  );
 | 
						|
}
 | 
						|
 | 
						|
class MatchedRule {
 | 
						|
  /**
 | 
						|
   * @param {Rule} rule
 | 
						|
   * @param {Ruleset} ruleset
 | 
						|
   */
 | 
						|
  constructor(rule, ruleset) {
 | 
						|
    this.rule = rule;
 | 
						|
    this.ruleset = ruleset;
 | 
						|
  }
 | 
						|
 | 
						|
  // The RuleManager that generated this MatchedRule.
 | 
						|
  get ruleManager() {
 | 
						|
    return this.ruleset.ruleManager;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
// tabId computation is currently not free, and depends on the initialization of
 | 
						|
// ExtensionParent.apiManager.global (see WebRequest.getTabIdForChannelWrapper).
 | 
						|
// Fortunately, DNR only supports tabIds in session rules, so by keeping track
 | 
						|
// of session rules with tabIds/excludedTabIds conditions, we can find tabId
 | 
						|
// exactly and only when necessary.
 | 
						|
let gHasAnyTabIdConditions = false;
 | 
						|
 | 
						|
class RequestDetails {
 | 
						|
  /**
 | 
						|
   * @param {object} options
 | 
						|
   * @param {nsIURI} options.requestURI - URL of the requested resource.
 | 
						|
   * @param {nsIURI} [options.initiatorURI] - URL of triggering principal,
 | 
						|
   *   provided that it is a content principal. Otherwise null.
 | 
						|
   * @param {string} options.type - ResourceType (MozContentPolicyType).
 | 
						|
   * @param {string} [options.method] - HTTP method
 | 
						|
   * @param {integer} [options.tabId]
 | 
						|
   * @param {CanonicalBrowsingContext} [options.browsingContext] - The CBC
 | 
						|
   *   associated with the request. Typically the bc for which the subresource
 | 
						|
   *   request is initiated, if any. For document requests, this is the parent
 | 
						|
   *   (i.e. the parent frame for sub_frame, null for main_frame).
 | 
						|
   */
 | 
						|
  constructor({
 | 
						|
    requestURI,
 | 
						|
    initiatorURI,
 | 
						|
    type,
 | 
						|
    method,
 | 
						|
    tabId,
 | 
						|
    browsingContext,
 | 
						|
  }) {
 | 
						|
    this.requestURI = requestURI;
 | 
						|
    this.initiatorURI = initiatorURI;
 | 
						|
    this.type = type;
 | 
						|
    this.method = method;
 | 
						|
    this.tabId = tabId;
 | 
						|
    this.browsingContext = browsingContext;
 | 
						|
 | 
						|
    this.requestDomain = this.#domainFromURI(requestURI);
 | 
						|
    this.initiatorDomain = initiatorURI
 | 
						|
      ? this.#domainFromURI(initiatorURI)
 | 
						|
      : null;
 | 
						|
 | 
						|
    this.requestURIspec = requestURI.spec;
 | 
						|
    this.requestDataForUrlFilter = new RequestDataForUrlFilter(
 | 
						|
      this.requestURIspec
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  static fromChannelWrapper(channel) {
 | 
						|
    let tabId = -1;
 | 
						|
    if (gHasAnyTabIdConditions) {
 | 
						|
      tabId = lazy.WebRequest.getTabIdForChannelWrapper(channel);
 | 
						|
    }
 | 
						|
    return new RequestDetails({
 | 
						|
      requestURI: channel.finalURI,
 | 
						|
      // Note: originURI may be null, if missing or null principal, as desired.
 | 
						|
      initiatorURI: channel.originURI,
 | 
						|
      type: channel.type,
 | 
						|
      method: channel.method.toLowerCase(),
 | 
						|
      tabId,
 | 
						|
      browsingContext: channel.loadInfo.browsingContext,
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  #ancestorRequestDetails;
 | 
						|
  get ancestorRequestDetails() {
 | 
						|
    if (this.#ancestorRequestDetails) {
 | 
						|
      return this.#ancestorRequestDetails;
 | 
						|
    }
 | 
						|
    this.#ancestorRequestDetails = [];
 | 
						|
    if (!this.browsingContext?.ancestorsAreCurrent) {
 | 
						|
      // this.browsingContext is set for real requests (via fromChannelWrapper).
 | 
						|
      // It may be void for testMatchOutcome and for the ancestor requests
 | 
						|
      // simulated below.
 | 
						|
      //
 | 
						|
      // ancestorsAreCurrent being false is unexpected, but could theoretically
 | 
						|
      // happen if the request is triggered from an unloaded (sub)frame. In that
 | 
						|
      // case we don't want to use potentially incorrect ancestor information.
 | 
						|
      //
 | 
						|
      // In any case, nothing left to do.
 | 
						|
      return this.#ancestorRequestDetails;
 | 
						|
    }
 | 
						|
    // Reconstruct the frame hierarchy of the request's document, in order to
 | 
						|
    // retroactively recompute the relevant matches of allowAllRequests rules.
 | 
						|
    //
 | 
						|
    // The allowAllRequests rule is supposedly applying to all subresource
 | 
						|
    // requests. For non-document requests, this is usually the document if any.
 | 
						|
    // In case of document requests, there is some ambiguity:
 | 
						|
    // - Usually, the initiator is the parent document that created the frame.
 | 
						|
    // - Sometimes, the initiator is a different frame or even another window.
 | 
						|
    //
 | 
						|
    // In RequestDetails.fromChannelWrapper, the actual initiator is used and
 | 
						|
    // reflected in initiatorURI, but here we use the document's parent. This
 | 
						|
    // is done because the chain of initiators is unstable (e.g. an opener can
 | 
						|
    // navigate/unload), whereas frame ancestor chain is constant as long as
 | 
						|
    // the leaf BrowsingContext is current. Moreover, allowAllRequests was
 | 
						|
    // originally designed to operate on frame hierarchies (crbug.com/1038831).
 | 
						|
    //
 | 
						|
    // This implementation of "initiator" for "allowAllRequests" is consistent
 | 
						|
    // with Chrome and Safari.
 | 
						|
    for (let bc = this.browsingContext; bc; bc = bc.parent) {
 | 
						|
      // Note: requestURI may differ from the document's initial requestURI,
 | 
						|
      // e.g. due to same-document navigations.
 | 
						|
      const requestURI = bc.currentURI;
 | 
						|
      if (!requestURI.schemeIs("https") && !requestURI.schemeIs("http")) {
 | 
						|
        // DNR is currently only hooked up to http(s) requests. Ignore other
 | 
						|
        // URLs, e.g. about:, blob:, moz-extension:, data:, etc.
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
      const isTop = !bc.parent;
 | 
						|
      const parentPrin = bc.parentWindowContext?.documentPrincipal;
 | 
						|
      const requestDetails = new RequestDetails({
 | 
						|
        requestURI,
 | 
						|
        // Note: initiatorURI differs from RequestDetails.fromChannelWrapper;
 | 
						|
        // See the above comment for more info.
 | 
						|
        initiatorURI: parentPrin?.isContentPrincipal ? parentPrin.URI : null,
 | 
						|
        type: isTop ? "main_frame" : "sub_frame",
 | 
						|
        method: bc.activeSessionHistoryEntry?.hasPostData ? "post" : "get",
 | 
						|
        tabId: this.tabId,
 | 
						|
        // In this loop we are already explicitly accounting for ancestors, so
 | 
						|
        // we intentionally omit browsingContext even though we have |bc|. If
 | 
						|
        // we were to set `browsingContext: bc`, the output would be the same,
 | 
						|
        // but be derived from unnecessarily repeated request evaluations.
 | 
						|
        browsingContext: null,
 | 
						|
      });
 | 
						|
      this.#ancestorRequestDetails.unshift(requestDetails);
 | 
						|
    }
 | 
						|
    return this.#ancestorRequestDetails;
 | 
						|
  }
 | 
						|
 | 
						|
  canExtensionModify(extension) {
 | 
						|
    const policy = extension.policy;
 | 
						|
    if (!policy.canAccessURI(this.requestURI)) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    if (
 | 
						|
      this.initiatorURI &&
 | 
						|
      this.type !== "main_frame" &&
 | 
						|
      this.type !== "sub_frame" &&
 | 
						|
      !policy.canAccessURI(this.initiatorURI)
 | 
						|
    ) {
 | 
						|
      // Host permissions for the initiator is required except for navigation
 | 
						|
      // requests: https://bugzilla.mozilla.org/show_bug.cgi?id=1825824#c2
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    return true;
 | 
						|
  }
 | 
						|
 | 
						|
  #domainFromURI(uri) {
 | 
						|
    try {
 | 
						|
      let hostname = uri.host;
 | 
						|
      // nsIURI omits brackets from IPv6 addresses. But the canonical form of an
 | 
						|
      // IPv6 address is with brackets, so add them.
 | 
						|
      return hostname.includes(":") ? `[${hostname}]` : hostname;
 | 
						|
    } catch (e) {
 | 
						|
      // uri.host throws for some schemes (e.g. about:). In practice we won't
 | 
						|
      // encounter this for network (via NetworkIntegration.startDNREvaluation)
 | 
						|
      // because isRestrictedPrincipalURI filters the initiatorURI. Furthermore,
 | 
						|
      // because only http(s) requests are observed, requestURI is http(s).
 | 
						|
      //
 | 
						|
      // declarativeNetRequest.testMatchOutcome can pass arbitrary URIs and thus
 | 
						|
      // trigger the error in nsIURI::GetHost.
 | 
						|
      Cu.reportError(e);
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * This RequestEvaluator class's logic is documented at the top of this file.
 | 
						|
 */
 | 
						|
class RequestEvaluator {
 | 
						|
  // private constructor, only used by RequestEvaluator.evaluateRequest.
 | 
						|
  constructor(request, ruleManager) {
 | 
						|
    this.req = request;
 | 
						|
    this.ruleManager = ruleManager;
 | 
						|
    this.canModify = request.canExtensionModify(ruleManager.extension);
 | 
						|
 | 
						|
    // These values are initialized by findMatchingRules():
 | 
						|
    this.matchedRule = null;
 | 
						|
    this.matchedModifyHeadersRules = [];
 | 
						|
    this.didCheckAncestors = false;
 | 
						|
    this.findMatchingRules();
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Finds the matched rules for the given request and extensions,
 | 
						|
   * according to the logic documented at the top of this file.
 | 
						|
   *
 | 
						|
   * @param {RequestDetails} request
 | 
						|
   * @param {RuleManager[]} ruleManagers
 | 
						|
   *    The list of RuleManagers, ordered by importance of its extension.
 | 
						|
   * @returns {MatchedRule[]}
 | 
						|
   */
 | 
						|
  static evaluateRequest(request, ruleManagers) {
 | 
						|
    // Helper to determine precedence of rules from different extensions.
 | 
						|
    function precedence(matchedRule) {
 | 
						|
      switch (matchedRule.rule.action.type) {
 | 
						|
        case "block":
 | 
						|
          return 1;
 | 
						|
        case "redirect":
 | 
						|
        case "upgradeScheme":
 | 
						|
          return 2;
 | 
						|
        case "allow":
 | 
						|
        case "allowAllRequests":
 | 
						|
          return 3;
 | 
						|
        // case "modifyHeaders": not comparable after the first pass.
 | 
						|
        default:
 | 
						|
          throw new Error(`Unexpected action: ${matchedRule.rule.action.type}`);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    let requestEvaluators = [];
 | 
						|
    let finalMatch;
 | 
						|
    for (let ruleManager of ruleManagers) {
 | 
						|
      // Evaluate request with findMatchingRules():
 | 
						|
      const requestEvaluator = new RequestEvaluator(request, ruleManager);
 | 
						|
      // RequestEvaluator may be used after the loop when the request is
 | 
						|
      // accepted, to collect modifyHeaders/allow/allowAllRequests actions.
 | 
						|
      requestEvaluators.push(requestEvaluator);
 | 
						|
      let matchedRule = requestEvaluator.matchedRule;
 | 
						|
      if (
 | 
						|
        matchedRule &&
 | 
						|
        (!finalMatch || precedence(matchedRule) < precedence(finalMatch))
 | 
						|
      ) {
 | 
						|
        // Before choosing the matched rule as finalMatch, check whether there
 | 
						|
        // is an allowAllRequests rule override among the ancestors.
 | 
						|
        requestEvaluator.findAncestorRuleOverride();
 | 
						|
        matchedRule = requestEvaluator.matchedRule;
 | 
						|
        if (!finalMatch || precedence(matchedRule) < precedence(finalMatch)) {
 | 
						|
          finalMatch = matchedRule;
 | 
						|
          if (finalMatch.rule.action.type === "block") {
 | 
						|
            break;
 | 
						|
          }
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
    if (finalMatch && !finalMatch.rule.isAllowOrAllowAllRequestsAction()) {
 | 
						|
      // Found block/redirect/upgradeScheme, request will be replaced.
 | 
						|
      return [finalMatch];
 | 
						|
    }
 | 
						|
    // Request not canceled, collect all modifyHeaders actions:
 | 
						|
    let matchedRules = requestEvaluators
 | 
						|
      .map(re => re.getMatchingModifyHeadersRules())
 | 
						|
      .flat(1);
 | 
						|
 | 
						|
    // ... and collect the allowAllRequests actions:
 | 
						|
    // Note: Only needed for testMatchOutcome, getMatchedRules (bug 1745765) and
 | 
						|
    // onRuleMatchedDebug (bug 1745773). Not for regular requests, since regular
 | 
						|
    // requests do not distinguish between no rule vs allow vs allowAllRequests.
 | 
						|
    let finalAllowAllRequestsMatches = [];
 | 
						|
    for (let requestEvaluator of requestEvaluators) {
 | 
						|
      // TODO bug 1745765 / bug 1745773: Uncomment findAncestorRuleOverride()
 | 
						|
      // when getMatchedRules() or onRuleMatchedDebug are implemented.
 | 
						|
      // requestEvaluator.findAncestorRuleOverride();
 | 
						|
      let matchedRule = requestEvaluator.matchedRule;
 | 
						|
      if (matchedRule && matchedRule.rule.action.type === "allowAllRequests") {
 | 
						|
        // Even if a different extension wins the final match, an extension
 | 
						|
        // may want to record the "allowAllRequests" action for the future.
 | 
						|
        finalAllowAllRequestsMatches.push(matchedRule);
 | 
						|
      }
 | 
						|
    }
 | 
						|
    if (finalAllowAllRequestsMatches.length) {
 | 
						|
      matchedRules = finalAllowAllRequestsMatches.concat(matchedRules);
 | 
						|
    }
 | 
						|
 | 
						|
    // ... and collect the "allow" action. At this point, finalMatch could also
 | 
						|
    // be a modifyHeaders or allowAllRequests action, but these would already
 | 
						|
    // have been added to the matchedRules result before.
 | 
						|
    if (finalMatch && finalMatch.rule.action.type === "allow") {
 | 
						|
      matchedRules.unshift(finalMatch);
 | 
						|
    }
 | 
						|
    return matchedRules;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Finds the matching rules, as documented in the comment before the class.
 | 
						|
   */
 | 
						|
  findMatchingRules() {
 | 
						|
    if (!this.canModify && !this.ruleManager.hasBlockPermission) {
 | 
						|
      // If the extension cannot apply any action, don't bother.
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    this.#collectMatchInRuleset(this.ruleManager.sessionRules);
 | 
						|
    this.#collectMatchInRuleset(this.ruleManager.dynamicRules);
 | 
						|
    for (let ruleset of this.ruleManager.enabledStaticRules) {
 | 
						|
      this.#collectMatchInRuleset(ruleset);
 | 
						|
    }
 | 
						|
 | 
						|
    if (this.matchedRule && !this.#isRuleActionAllowed(this.matchedRule.rule)) {
 | 
						|
      this.matchedRule = null;
 | 
						|
      // Note: this.matchedModifyHeadersRules is [] because canModify access is
 | 
						|
      // checked before populating the list.
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Find an "allowAllRequests" rule among the ancestors that may override the
 | 
						|
   * current matchedRule and/or matchedModifyHeadersRules rules.
 | 
						|
   */
 | 
						|
  findAncestorRuleOverride() {
 | 
						|
    if (this.didCheckAncestors) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    this.didCheckAncestors = true;
 | 
						|
 | 
						|
    if (!this.ruleManager.hasRulesWithAllowAllRequests) {
 | 
						|
      // Optimization: Skip ancestorRequestDetails lookup and/or request
 | 
						|
      // evaluation if there are no allowAllRequests rules.
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // Now we need to check whether any of the ancestor frames had a matching
 | 
						|
    // allowAllRequests rule. matchedRule and/or matchedModifyHeadersRules
 | 
						|
    // results may be ignored if their priority is lower or equal to the
 | 
						|
    // highest-priority allowAllRequests rule among the frame ancestors.
 | 
						|
    //
 | 
						|
    // In theory, every ancestor may potentially yield an allowAllRequests rule,
 | 
						|
    // and should therefore be checked unconditionally. But logically, if there
 | 
						|
    // are no existing matches, then any matching allowAllRequests rules will
 | 
						|
    // not have any effect on the request outcome. As an optimization, we
 | 
						|
    // therefore skip ancestor checks in this case.
 | 
						|
    if (
 | 
						|
      (!this.matchedRule ||
 | 
						|
        this.matchedRule.rule.isAllowOrAllowAllRequestsAction()) &&
 | 
						|
      !this.matchedModifyHeadersRules.length
 | 
						|
    ) {
 | 
						|
      // Optimization: Do not look up ancestors if no rules were matched.
 | 
						|
      //
 | 
						|
      // TODO bug 1745773: onRuleMatchedDebug is supposed to report when a rule
 | 
						|
      // has been matched. To be pedantic, when there is an onRuleMatchedDebug
 | 
						|
      // listener, the parents need to be checked unconditionally, in order to
 | 
						|
      // report potential allowAllRequests matches among ancestors.
 | 
						|
      // TODO bug 1745765: the above may also apply to getMatchedRules().
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    for (let request of this.req.ancestorRequestDetails) {
 | 
						|
      // TODO: Optimize by only evaluating allow/allowAllRequests rules, because
 | 
						|
      // the request being seen here implies that the request was not canceled,
 | 
						|
      // i.e. that there were no block/redirect/upgradeScheme rules in any of
 | 
						|
      // the ancestors (across all extensions!).
 | 
						|
      let requestEvaluator = new RequestEvaluator(request, this.ruleManager);
 | 
						|
      let ancestorMatchedRule = requestEvaluator.matchedRule;
 | 
						|
      if (
 | 
						|
        ancestorMatchedRule &&
 | 
						|
        ancestorMatchedRule.rule.action.type === "allowAllRequests" &&
 | 
						|
        (!this.matchedRule ||
 | 
						|
          compareRule(
 | 
						|
            this.matchedRule.rule,
 | 
						|
            ancestorMatchedRule.rule,
 | 
						|
            this.matchedRule.ruleset,
 | 
						|
            ancestorMatchedRule.ruleset
 | 
						|
          ) > 0)
 | 
						|
      ) {
 | 
						|
        // Found an allowAllRequests rule that takes precedence over whatever
 | 
						|
        // the current rule was.
 | 
						|
        this.matchedRule = ancestorMatchedRule;
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Retrieves the list of matched modifyHeaders rules that should apply.
 | 
						|
   *
 | 
						|
   * @returns {MatchedRule[]}
 | 
						|
   */
 | 
						|
  getMatchingModifyHeadersRules() {
 | 
						|
    if (this.matchedModifyHeadersRules.length) {
 | 
						|
      // Find parent allowAllRequests rules, if any, to make sure that we can
 | 
						|
      // appropriately ignore same-or-lower-priority modifyHeaders rules.
 | 
						|
      this.findAncestorRuleOverride();
 | 
						|
    }
 | 
						|
    // The minimum priority is 1. Defaulting to 0 = include all.
 | 
						|
    let priorityThreshold = 0;
 | 
						|
    if (this.matchedRule?.rule.isAllowOrAllowAllRequestsAction()) {
 | 
						|
      priorityThreshold = this.matchedRule.rule.priority;
 | 
						|
    }
 | 
						|
    // Note: the result cannot be non-empty if this.matchedRule is a non-allow
 | 
						|
    // action, because if that were to be the case, then the request would have
 | 
						|
    // been canceled, and therefore there would not be any header to modify.
 | 
						|
    // Even if another extension were to override the action, it could only be
 | 
						|
    // any other non-allow action, which would still cancel the request.
 | 
						|
    let matchedRules = this.matchedModifyHeadersRules.filter(matchedRule => {
 | 
						|
      return matchedRule.rule.priority > priorityThreshold;
 | 
						|
    });
 | 
						|
    // Sort output for a deterministic order.
 | 
						|
    // NOTE: Sorting rules at registration (in RuleManagers) would avoid the
 | 
						|
    // need to sort here. Since the number of matched modifyHeaders rules are
 | 
						|
    // expected to be small, we don't bother optimizing.
 | 
						|
    matchedRules.sort((a, b) => {
 | 
						|
      return compareRule(a.rule, b.rule, a.ruleset, b.ruleset);
 | 
						|
    });
 | 
						|
    return matchedRules;
 | 
						|
  }
 | 
						|
 | 
						|
  /** @param {Ruleset} ruleset */
 | 
						|
  #collectMatchInRuleset(ruleset) {
 | 
						|
    for (let rule of ruleset.rules) {
 | 
						|
      if (!this.#matchesRuleCondition(rule.condition)) {
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
      if (rule.action.type === "modifyHeaders") {
 | 
						|
        if (this.canModify) {
 | 
						|
          this.matchedModifyHeadersRules.push(new MatchedRule(rule, ruleset));
 | 
						|
        }
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
      if (
 | 
						|
        this.matchedRule &&
 | 
						|
        compareRule(
 | 
						|
          this.matchedRule.rule,
 | 
						|
          rule,
 | 
						|
          this.matchedRule.ruleset,
 | 
						|
          ruleset
 | 
						|
        ) <= 0
 | 
						|
      ) {
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
      this.matchedRule = new MatchedRule(rule, ruleset);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * @param {RuleCondition} cond
 | 
						|
   * @returns {boolean} Whether the condition matched.
 | 
						|
   */
 | 
						|
  #matchesRuleCondition(cond) {
 | 
						|
    if (cond.resourceTypes) {
 | 
						|
      if (!cond.resourceTypes.includes(this.req.type)) {
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
    } else if (cond.excludedResourceTypes) {
 | 
						|
      if (cond.excludedResourceTypes.includes(this.req.type)) {
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
    } else if (this.req.type === "main_frame") {
 | 
						|
      // When resourceTypes/excludedResourceTypes are not specified, the
 | 
						|
      // documented behavior is to ignore main_frame requests.
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    // Check this.req.requestURI:
 | 
						|
    if (cond.urlFilter) {
 | 
						|
      if (!cond.urlFilterMatches(this.req.requestDataForUrlFilter)) {
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
    } else if (cond.regexFilter) {
 | 
						|
      if (!cond.getCompiledRegexFilter().test(this.req.requestURIspec)) {
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    if (
 | 
						|
      cond.excludedRequestDomains &&
 | 
						|
      this.#matchesDomains(cond.excludedRequestDomains, this.req.requestDomain)
 | 
						|
    ) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    if (
 | 
						|
      cond.requestDomains &&
 | 
						|
      !this.#matchesDomains(cond.requestDomains, this.req.requestDomain)
 | 
						|
    ) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    if (
 | 
						|
      cond.excludedInitiatorDomains &&
 | 
						|
      // Note: unable to only match null principals (bug 1798225).
 | 
						|
      this.req.initiatorDomain &&
 | 
						|
      this.#matchesDomains(
 | 
						|
        cond.excludedInitiatorDomains,
 | 
						|
        this.req.initiatorDomain
 | 
						|
      )
 | 
						|
    ) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    if (
 | 
						|
      cond.initiatorDomains &&
 | 
						|
      // Note: unable to only match null principals (bug 1798225).
 | 
						|
      (!this.req.initiatorDomain ||
 | 
						|
        !this.#matchesDomains(cond.initiatorDomains, this.req.initiatorDomain))
 | 
						|
    ) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    // TODO bug 1797408: domainType
 | 
						|
 | 
						|
    if (cond.requestMethods) {
 | 
						|
      if (!cond.requestMethods.includes(this.req.method)) {
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
    } else if (cond.excludedRequestMethods?.includes(this.req.method)) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    if (cond.tabIds) {
 | 
						|
      if (!cond.tabIds.includes(this.req.tabId)) {
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
    } else if (cond.excludedTabIds?.includes(this.req.tabId)) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    return true;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * @param {string[]} domains - A list of canonicalized domain patterns.
 | 
						|
   *   Canonical means punycode, no ports, and IPv6 without brackets, and not
 | 
						|
   *   starting with a dot. May end with a dot if it is a FQDN.
 | 
						|
   * @param {string} host - The canonical representation of the host of a URL.
 | 
						|
   * @returns {boolean} Whether the given host is a (sub)domain of any of the
 | 
						|
   *   given domains.
 | 
						|
   */
 | 
						|
  #matchesDomains(domains, host) {
 | 
						|
    return domains.some(domain => {
 | 
						|
      return (
 | 
						|
        host.endsWith(domain) &&
 | 
						|
        // either host === domain
 | 
						|
        (host.length === domain.length ||
 | 
						|
          // or host = "something." + domain (WITH a domain separator).
 | 
						|
          host.charAt(host.length - domain.length - 1) === ".")
 | 
						|
      );
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * @param {Rule} rule - The final rule from the first pass.
 | 
						|
   * @returns {boolean} Whether the extension is allowed to execute the rule.
 | 
						|
   */
 | 
						|
  #isRuleActionAllowed(rule) {
 | 
						|
    if (this.canModify) {
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
    switch (rule.action.type) {
 | 
						|
      case "allow":
 | 
						|
      case "allowAllRequests":
 | 
						|
      case "block":
 | 
						|
      case "upgradeScheme":
 | 
						|
        return this.ruleManager.hasBlockPermission;
 | 
						|
      case "redirect":
 | 
						|
        return false;
 | 
						|
      // case "modifyHeaders" is never an action for this.matchedRule.
 | 
						|
      default:
 | 
						|
        throw new Error(`Unexpected action type: ${rule.action.type}`);
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Checks whether a request from a document with the given URI is allowed to
 | 
						|
 * be modified by an unprivileged extension (e.g. an extension without host
 | 
						|
 * permissions but the "declarativeNetRequest" permission).
 | 
						|
 * The output is comparable to WebExtensionPolicy::CanAccessURI for an extension
 | 
						|
 * with the `<all_urls>` permission, for consistency with the webRequest API.
 | 
						|
 *
 | 
						|
 * @param {nsIURI} [uri] The URI of a request's loadingPrincipal. May be void
 | 
						|
 *   if missing (e.g. top-level requests) or not a content principal.
 | 
						|
 * @returns {boolean} Whether there is any extension that is allowed to see
 | 
						|
 *   requests from a document with the given URI. Callers are expected to:
 | 
						|
 *   - check system requests (and treat as true).
 | 
						|
 *   - check WebExtensionPolicy.isRestrictedURI (and treat as true).
 | 
						|
 */
 | 
						|
function isRestrictedPrincipalURI(uri) {
 | 
						|
  if (!uri) {
 | 
						|
    // No URI, could be:
 | 
						|
    // - System principal (caller should have checked and disallowed access).
 | 
						|
    // - Expanded principal, typically content script in documents. If an
 | 
						|
    //   extension content script managed to run there, that implies that an
 | 
						|
    //   extension was able to access it.
 | 
						|
    // - Null principal (e.g. sandboxed document, about:blank, data:).
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
 | 
						|
  // An unprivileged extension with maximal host permissions has allowedOrigins
 | 
						|
  // set to [`<all_urls>`, `moz-extension://extensions-own-uuid-here`].
 | 
						|
  // `<all_urls>` matches PermittedSchemes from MatchPattern.cpp:
 | 
						|
  // https://searchfox.org/mozilla-central/rev/55d5c4b9dffe5e59eb6b019c1a930ec9ada47e10/toolkit/components/extensions/MatchPattern.cpp#209-211
 | 
						|
  // i.e. "http", "https", "ws", "wss", "file", "ftp", "data".
 | 
						|
  // - It is not possible to have a loadingPrincipal for: ws, wss, ftp.
 | 
						|
  // - data:-URIs always have an opaque origin, i.e. the principal is not a
 | 
						|
  //   content principal, thus void here.
 | 
						|
  // - The remaining schemes from `<all_urls>` are: http, https, file, data,
 | 
						|
  //   and checked below.
 | 
						|
  //
 | 
						|
  // Privileged addons can also access resource: and about:, but we do not need
 | 
						|
  // to support these now.
 | 
						|
 | 
						|
  // http(s) are common, and allowed, except for some restricted domains. The
 | 
						|
  // caller is expected to check WebExtensionPolicy.isRestrictedURI.
 | 
						|
  if (uri.schemeIs("http") || uri.schemeIs("https")) {
 | 
						|
    return false; // Very common.
 | 
						|
  }
 | 
						|
 | 
						|
  // moz-extension: is not restricted because an extension always has permission
 | 
						|
  // to its own moz-extension:-origin. The caller is expected to verify that an
 | 
						|
  // extension can only access its own URI.
 | 
						|
  if (uri.schemeIs("moz-extension")) {
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
 | 
						|
  // Requests from local files are intentionally allowed (bug 1621935).
 | 
						|
  if (uri.schemeIs("file")) {
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
 | 
						|
  // Anything else (e.g. resource:, about:newtab, etc.) is not allowed.
 | 
						|
  return true;
 | 
						|
}
 | 
						|
 | 
						|
const NetworkIntegration = {
 | 
						|
  maxEvaluatedRulesCount: 0,
 | 
						|
 | 
						|
  register() {
 | 
						|
    // We register via WebRequest.sys.mjs to ensure predictable ordering of DNR and
 | 
						|
    // WebRequest behavior.
 | 
						|
    lazy.WebRequest.setDNRHandlingEnabled(true);
 | 
						|
  },
 | 
						|
  unregister() {
 | 
						|
    lazy.WebRequest.setDNRHandlingEnabled(false);
 | 
						|
  },
 | 
						|
  maybeUpdateTabIdChecker() {
 | 
						|
    gHasAnyTabIdConditions = gRuleManagers.some(rm => rm.hasRulesWithTabIds);
 | 
						|
  },
 | 
						|
 | 
						|
  startDNREvaluation(channel) {
 | 
						|
    let ruleManagers = gRuleManagers;
 | 
						|
    // TODO bug 1827422: Merge isRestrictedPrincipalURI with canModify.
 | 
						|
    if (!channel.canModify || isRestrictedPrincipalURI(channel.documentURI)) {
 | 
						|
      // Ignore system requests or requests to restricted domains.
 | 
						|
      ruleManagers = [];
 | 
						|
    }
 | 
						|
    if (channel.loadInfo.originAttributes.privateBrowsingId > 0) {
 | 
						|
      ruleManagers = ruleManagers.filter(
 | 
						|
        rm => rm.extension.privateBrowsingAllowed
 | 
						|
      );
 | 
						|
    }
 | 
						|
    if (ruleManagers.length && !lazy.gMatchRequestsFromOtherExtensions) {
 | 
						|
      const policy = channel.loadInfo.loadingPrincipal?.addonPolicy;
 | 
						|
      if (policy) {
 | 
						|
        ruleManagers = ruleManagers.filter(
 | 
						|
          rm => rm.extension.policy === policy
 | 
						|
        );
 | 
						|
      }
 | 
						|
    }
 | 
						|
    let matchedRules;
 | 
						|
    if (ruleManagers.length) {
 | 
						|
      const evaluateRulesTimerId =
 | 
						|
        Glean.extensionsApisDnr.evaluateRulesTime.start();
 | 
						|
      try {
 | 
						|
        const request = RequestDetails.fromChannelWrapper(channel);
 | 
						|
        matchedRules = RequestEvaluator.evaluateRequest(request, ruleManagers);
 | 
						|
      } finally {
 | 
						|
        if (evaluateRulesTimerId !== undefined) {
 | 
						|
          Glean.extensionsApisDnr.evaluateRulesTime.stopAndAccumulate(
 | 
						|
            evaluateRulesTimerId
 | 
						|
          );
 | 
						|
        }
 | 
						|
      }
 | 
						|
      const evaluateRulesCount = ruleManagers.reduce(
 | 
						|
        (sum, ruleManager) => sum + ruleManager.getRulesCount(),
 | 
						|
        0
 | 
						|
      );
 | 
						|
      if (evaluateRulesCount > this.maxEvaluatedRulesCount) {
 | 
						|
        Glean.extensionsApisDnr.evaluateRulesCountMax.set(evaluateRulesCount);
 | 
						|
        this.maxEvaluatedRulesCount = evaluateRulesCount;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    // Cache for later. In case of redirects, _dnrMatchedRules may exist for
 | 
						|
    // the pre-redirect HTTP channel, and is overwritten here again.
 | 
						|
    channel._dnrMatchedRules = matchedRules;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Applies the actions of the DNR rules.
 | 
						|
   *
 | 
						|
   * @typedef {ChannelWrapper & { _dnrMatchedRules?: MatchedRule[] }}
 | 
						|
   *          ChannelWrapperViaDNR
 | 
						|
   *
 | 
						|
   * @param {ChannelWrapperViaDNR} channel
 | 
						|
   * @returns {boolean} Whether to ignore any responses from the webRequest API.
 | 
						|
   */
 | 
						|
  onBeforeRequest(channel) {
 | 
						|
    let matchedRules = channel._dnrMatchedRules;
 | 
						|
    if (!matchedRules?.length) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    // If a matched rule closes the channel, it is the sole match.
 | 
						|
    const finalMatch = matchedRules[0];
 | 
						|
    switch (finalMatch.rule.action.type) {
 | 
						|
      case "block":
 | 
						|
        this.applyBlock(channel, finalMatch);
 | 
						|
        return true;
 | 
						|
      case "redirect":
 | 
						|
        this.applyRedirect(channel, finalMatch);
 | 
						|
        return true;
 | 
						|
      case "upgradeScheme":
 | 
						|
        this.applyUpgradeScheme(channel);
 | 
						|
        return true;
 | 
						|
    }
 | 
						|
    // If there are multiple rules, then it may be a combination of allow,
 | 
						|
    // allowAllRequests and/or modifyHeaders.
 | 
						|
 | 
						|
    // "modifyHeaders" is handled by onBeforeSendHeaders/onHeadersReceived.
 | 
						|
    // "allow" and "allowAllRequests" require no further action now.
 | 
						|
    // "allowAllRequests" is applied to new requests in the future (if any)
 | 
						|
    // through RequestEvaluator's findAncestorRuleOverride().
 | 
						|
 | 
						|
    return false;
 | 
						|
  },
 | 
						|
 | 
						|
  onBeforeSendHeaders(channel) {
 | 
						|
    let matchedRules = channel._dnrMatchedRules;
 | 
						|
    if (!matchedRules?.length) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    ModifyRequestHeaders.maybeApplyModifyHeaders(channel, matchedRules);
 | 
						|
  },
 | 
						|
 | 
						|
  onHeadersReceived(channel) {
 | 
						|
    let matchedRules = channel._dnrMatchedRules;
 | 
						|
    if (!matchedRules?.length) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    ModifyResponseHeaders.maybeApplyModifyHeaders(channel, matchedRules);
 | 
						|
  },
 | 
						|
 | 
						|
  applyBlock(channel, matchedRule) {
 | 
						|
    // TODO bug 1802259: Consider a DNR-specific reason.
 | 
						|
    channel.cancel(
 | 
						|
      Cr.NS_ERROR_ABORT,
 | 
						|
      Ci.nsILoadInfo.BLOCKING_REASON_EXTENSION_WEBREQUEST
 | 
						|
    );
 | 
						|
    const addonId = matchedRule.ruleManager.extension.id;
 | 
						|
    let properties = channel.channel.QueryInterface(Ci.nsIWritablePropertyBag);
 | 
						|
    properties.setProperty("cancelledByExtension", addonId);
 | 
						|
  },
 | 
						|
 | 
						|
  applyUpgradeScheme(channel) {
 | 
						|
    // Request upgrade. No-op if already secure (i.e. https).
 | 
						|
    channel.upgradeToSecure();
 | 
						|
  },
 | 
						|
 | 
						|
  applyRedirect(channel, matchedRule) {
 | 
						|
    // Ambiguity resolution order of redirect dict keys, consistent with Chrome:
 | 
						|
    // - url > extensionPath > transform > regexSubstitution
 | 
						|
    const redirect = matchedRule.rule.action.redirect;
 | 
						|
    const extension = matchedRule.ruleManager.extension;
 | 
						|
    const preRedirectUri = channel.finalURI;
 | 
						|
    let redirectUri;
 | 
						|
    if (redirect.url) {
 | 
						|
      // redirect.url already validated by checkActionRedirect.
 | 
						|
      redirectUri = Services.io.newURI(redirect.url);
 | 
						|
    } else if (redirect.extensionPath) {
 | 
						|
      redirectUri = extension.baseURI
 | 
						|
        .mutate()
 | 
						|
        .setPathQueryRef(redirect.extensionPath)
 | 
						|
        .finalize();
 | 
						|
    } else if (redirect.transform) {
 | 
						|
      redirectUri = applyURLTransform(preRedirectUri, redirect.transform);
 | 
						|
    } else if (redirect.regexSubstitution) {
 | 
						|
      // Note: may throw if regexSubstitution results in an invalid redirect.
 | 
						|
      // The error propagates up to handleRequest, which will just allow the
 | 
						|
      // request to continue.
 | 
						|
      redirectUri = applyRegexSubstitution(preRedirectUri, matchedRule);
 | 
						|
    } else {
 | 
						|
      // #checkActionRedirect ensures that the redirect action is non-empty.
 | 
						|
    }
 | 
						|
 | 
						|
    if (preRedirectUri.equals(redirectUri)) {
 | 
						|
      // URL did not change. Sometimes it is a bug in the extension, but there
 | 
						|
      // are also cases where the result is unavoidable. E.g. redirect.transform
 | 
						|
      // with queryTransform.removeParams that does not remove anything.
 | 
						|
      // TODO: consider logging to help with debugging.
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    channel.redirectTo(redirectUri);
 | 
						|
 | 
						|
    let properties = channel.channel.QueryInterface(Ci.nsIWritablePropertyBag);
 | 
						|
    properties.setProperty("redirectedByExtension", extension.id);
 | 
						|
 | 
						|
    let origin = channel.getRequestHeader("Origin");
 | 
						|
    if (origin) {
 | 
						|
      channel.setResponseHeader("Access-Control-Allow-Origin", origin);
 | 
						|
      channel.setResponseHeader("Access-Control-Allow-Credentials", "true");
 | 
						|
      channel.setResponseHeader("Access-Control-Max-Age", "0");
 | 
						|
    }
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
class RuleManager {
 | 
						|
  constructor(extension) {
 | 
						|
    this.extension = extension;
 | 
						|
    this.sessionRules = this.makeRuleset(
 | 
						|
      "_session",
 | 
						|
      PRECEDENCE_SESSION_RULESET
 | 
						|
    );
 | 
						|
    this.dynamicRules = this.makeRuleset(
 | 
						|
      "_dynamic",
 | 
						|
      PRECEDENCE_DYNAMIC_RULESET
 | 
						|
    );
 | 
						|
    this.enabledStaticRules = [];
 | 
						|
 | 
						|
    this.hasBlockPermission = extension.hasPermission("declarativeNetRequest");
 | 
						|
    this.hasRulesWithTabIds = false;
 | 
						|
    this.hasRulesWithAllowAllRequests = false;
 | 
						|
    this.totalRulesCount = 0;
 | 
						|
  }
 | 
						|
 | 
						|
  get availableStaticRuleCount() {
 | 
						|
    return Math.max(
 | 
						|
      lazy.ExtensionDNRLimits.GUARANTEED_MINIMUM_STATIC_RULES -
 | 
						|
        this.enabledStaticRules.reduce(
 | 
						|
          (acc, ruleset) => acc + ruleset.rules.length,
 | 
						|
          0
 | 
						|
        ),
 | 
						|
      0
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  get enabledStaticRulesetIds() {
 | 
						|
    return this.enabledStaticRules.map(ruleset => ruleset.id);
 | 
						|
  }
 | 
						|
 | 
						|
  makeRuleset(rulesetId, rulesetPrecedence, rules = []) {
 | 
						|
    return new Ruleset(rulesetId, rulesetPrecedence, rules, this);
 | 
						|
  }
 | 
						|
 | 
						|
  setSessionRules(validatedSessionRules) {
 | 
						|
    let oldRulesCount = this.sessionRules.rules.length;
 | 
						|
    let newRulesCount = validatedSessionRules.length;
 | 
						|
    this.sessionRules.rules = validatedSessionRules;
 | 
						|
    this.totalRulesCount += newRulesCount - oldRulesCount;
 | 
						|
    this.hasRulesWithTabIds = !!this.sessionRules.rules.find(rule => {
 | 
						|
      return rule.condition.tabIds || rule.condition.excludedTabIds;
 | 
						|
    });
 | 
						|
    this.#updateAllowAllRequestRules();
 | 
						|
    NetworkIntegration.maybeUpdateTabIdChecker();
 | 
						|
  }
 | 
						|
 | 
						|
  setDynamicRules(validatedDynamicRules) {
 | 
						|
    let oldRulesCount = this.dynamicRules.rules.length;
 | 
						|
    let newRulesCount = validatedDynamicRules.length;
 | 
						|
    this.dynamicRules.rules = validatedDynamicRules;
 | 
						|
    this.totalRulesCount += newRulesCount - oldRulesCount;
 | 
						|
    this.#updateAllowAllRequestRules();
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Set the enabled static rulesets.
 | 
						|
   *
 | 
						|
   * @param {Array<{ id, rules }>} enabledStaticRulesets
 | 
						|
   *        Array of objects including the ruleset id and rules.
 | 
						|
   *        The order of the rulesets in the Array is expected to
 | 
						|
   *        match the order of the rulesets in the extension manifest.
 | 
						|
   */
 | 
						|
  setEnabledStaticRulesets(enabledStaticRulesets) {
 | 
						|
    const rulesets = [];
 | 
						|
    for (const [idx, { id, rules }] of enabledStaticRulesets.entries()) {
 | 
						|
      rulesets.push(
 | 
						|
        this.makeRuleset(id, idx + PRECEDENCE_STATIC_RULESETS_BASE, rules)
 | 
						|
      );
 | 
						|
    }
 | 
						|
    const countRules = rulesets =>
 | 
						|
      rulesets.reduce((sum, ruleset) => sum + ruleset.rules.length, 0);
 | 
						|
    const oldRulesCount = countRules(this.enabledStaticRules);
 | 
						|
    const newRulesCount = countRules(rulesets);
 | 
						|
    this.enabledStaticRules = rulesets;
 | 
						|
    this.totalRulesCount += newRulesCount - oldRulesCount;
 | 
						|
    this.#updateAllowAllRequestRules();
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Get the session scoped rules.
 | 
						|
   *
 | 
						|
   * @param {Array<integer>|null} ruleIds
 | 
						|
            Optional array of rule IDs to return. By default, all the session
 | 
						|
            scoped rules are returned.
 | 
						|
   */
 | 
						|
  getSessionRules(ruleIds = null) {
 | 
						|
    if (!ruleIds) {
 | 
						|
      return this.sessionRules.rules;
 | 
						|
    }
 | 
						|
 | 
						|
    return this.sessionRules.rules.filter(rule => ruleIds.includes(rule.id));
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Get the dynamic rules.
 | 
						|
   *
 | 
						|
   * @param {Array<integer>|null} ruleIds
 | 
						|
            Optional array of rule IDs to return. By default, all the dynamic
 | 
						|
            rules are returned.
 | 
						|
   */
 | 
						|
  getDynamicRules(ruleIds = null) {
 | 
						|
    if (!ruleIds) {
 | 
						|
      return this.dynamicRules.rules;
 | 
						|
    }
 | 
						|
 | 
						|
    return this.dynamicRules.rules.filter(rule => ruleIds.includes(rule.id));
 | 
						|
  }
 | 
						|
 | 
						|
  getRulesCount() {
 | 
						|
    return this.totalRulesCount;
 | 
						|
  }
 | 
						|
 | 
						|
  #updateAllowAllRequestRules() {
 | 
						|
    const filterAAR = rule => rule.action.type === "allowAllRequests";
 | 
						|
    this.hasRulesWithAllowAllRequests =
 | 
						|
      this.sessionRules.rules.some(filterAAR) ||
 | 
						|
      this.dynamicRules.rules.some(filterAAR) ||
 | 
						|
      this.enabledStaticRules.some(ruleset => ruleset.rules.some(filterAAR));
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function getRuleManager(extension, createIfMissing = true) {
 | 
						|
  let ruleManager = gRuleManagers.find(rm => rm.extension === extension);
 | 
						|
  if (!ruleManager && createIfMissing) {
 | 
						|
    if (extension.hasShutdown) {
 | 
						|
      throw new Error(
 | 
						|
        `Error on creating new DNR RuleManager after extension shutdown: ${extension.id}`
 | 
						|
      );
 | 
						|
    }
 | 
						|
    ruleManager = new RuleManager(extension);
 | 
						|
    // The most recently installed extension gets priority, i.e. appears at the
 | 
						|
    // start of the gRuleManagers list. It is not yet possible to determine the
 | 
						|
    // installation time of a given Extension, so currently the last to
 | 
						|
    // instantiate a RuleManager claims the highest priority.
 | 
						|
    // TODO bug 1786059: order extensions by "installation time".
 | 
						|
    gRuleManagers.unshift(ruleManager);
 | 
						|
    if (gRuleManagers.length === 1) {
 | 
						|
      // The first DNR registration.
 | 
						|
      NetworkIntegration.register();
 | 
						|
    }
 | 
						|
  }
 | 
						|
  return ruleManager;
 | 
						|
}
 | 
						|
 | 
						|
function clearRuleManager(extension) {
 | 
						|
  let i = gRuleManagers.findIndex(rm => rm.extension === extension);
 | 
						|
  if (i !== -1) {
 | 
						|
    gRuleManagers.splice(i, 1);
 | 
						|
    NetworkIntegration.maybeUpdateTabIdChecker();
 | 
						|
    if (gRuleManagers.length === 0) {
 | 
						|
      // The last DNR registration.
 | 
						|
      NetworkIntegration.unregister();
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Finds all matching rules for a request, optionally restricted to one
 | 
						|
 * extension. Used by declarativeNetRequest.testMatchOutcome.
 | 
						|
 *
 | 
						|
 * @param {object|RequestDetails} request
 | 
						|
 * @param {Extension} [extension]
 | 
						|
 * @returns {MatchedRule[]}
 | 
						|
 */
 | 
						|
function getMatchedRulesForRequest(request, extension) {
 | 
						|
  let requestDetails = new RequestDetails(request);
 | 
						|
  const { requestURI, initiatorURI } = requestDetails;
 | 
						|
  let ruleManagers = gRuleManagers;
 | 
						|
  if (extension) {
 | 
						|
    ruleManagers = ruleManagers.filter(rm => rm.extension === extension);
 | 
						|
  }
 | 
						|
  if (
 | 
						|
    // NetworkIntegration.startDNREvaluation does not check requestURI, but we
 | 
						|
    // do that here to filter URIs that are obviously disallowed. In practice,
 | 
						|
    // anything other than http(s) is bogus and unsupported in DNR.
 | 
						|
    isRestrictedPrincipalURI(requestURI) ||
 | 
						|
    // Equivalent to NetworkIntegration.startDNREvaluation's channel.canModify
 | 
						|
    // check, which excludes system requests and restricted domains.
 | 
						|
    WebExtensionPolicy.isRestrictedURI(requestURI) ||
 | 
						|
    (initiatorURI && WebExtensionPolicy.isRestrictedURI(initiatorURI)) ||
 | 
						|
    isRestrictedPrincipalURI(initiatorURI)
 | 
						|
  ) {
 | 
						|
    ruleManagers = [];
 | 
						|
  }
 | 
						|
  // While this simulated request is not really from another extension, apply
 | 
						|
  // the same access control checks from NetworkIntegration.startDNREvaluation
 | 
						|
  // for consistency.
 | 
						|
  if (
 | 
						|
    !lazy.gMatchRequestsFromOtherExtensions &&
 | 
						|
    initiatorURI?.schemeIs("moz-extension")
 | 
						|
  ) {
 | 
						|
    const extUuid = initiatorURI.host;
 | 
						|
    ruleManagers = ruleManagers.filter(rm => rm.extension.uuid === extUuid);
 | 
						|
  }
 | 
						|
  return RequestEvaluator.evaluateRequest(requestDetails, ruleManagers);
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Runs before any webRequest event is notified. Headers may be modified, but
 | 
						|
 * the request should not be canceled (see handleRequest instead).
 | 
						|
 *
 | 
						|
 * @param {ChannelWrapper} channel
 | 
						|
 * @param {string} kind - The name of the webRequest event.
 | 
						|
 */
 | 
						|
function beforeWebRequestEvent(channel, kind) {
 | 
						|
  try {
 | 
						|
    switch (kind) {
 | 
						|
      case "onBeforeRequest":
 | 
						|
        NetworkIntegration.startDNREvaluation(channel);
 | 
						|
        break;
 | 
						|
      case "onBeforeSendHeaders":
 | 
						|
        NetworkIntegration.onBeforeSendHeaders(channel);
 | 
						|
        break;
 | 
						|
      case "onHeadersReceived":
 | 
						|
        NetworkIntegration.onHeadersReceived(channel);
 | 
						|
        break;
 | 
						|
    }
 | 
						|
  } catch (e) {
 | 
						|
    Cu.reportError(e);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Applies matching DNR rules, some of which may potentially cancel the request.
 | 
						|
 *
 | 
						|
 * @param {ChannelWrapperViaDNR} channel
 | 
						|
 * @param {string} kind - The name of the webRequest event.
 | 
						|
 * @returns {boolean} Whether to ignore any responses from the webRequest API.
 | 
						|
 */
 | 
						|
function handleRequest(channel, kind) {
 | 
						|
  try {
 | 
						|
    if (kind === "onBeforeRequest") {
 | 
						|
      return NetworkIntegration.onBeforeRequest(channel);
 | 
						|
    }
 | 
						|
  } catch (e) {
 | 
						|
    Cu.reportError(e);
 | 
						|
  }
 | 
						|
  return false;
 | 
						|
}
 | 
						|
 | 
						|
async function initExtension(extension) {
 | 
						|
  // These permissions are NOT an OptionalPermission, so their status can be
 | 
						|
  // assumed to be constant for the lifetime of the extension.
 | 
						|
  if (
 | 
						|
    extension.hasPermission("declarativeNetRequest") ||
 | 
						|
    extension.hasPermission("declarativeNetRequestWithHostAccess")
 | 
						|
  ) {
 | 
						|
    if (extension.hasShutdown) {
 | 
						|
      throw new Error(
 | 
						|
        `Aborted ExtensionDNR.initExtension call, extension "${extension.id}" is not active anymore`
 | 
						|
      );
 | 
						|
    }
 | 
						|
    extension.once("shutdown", () => clearRuleManager(extension));
 | 
						|
    await lazy.ExtensionDNRStore.initExtension(extension);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function ensureInitialized(extension) {
 | 
						|
  return (extension._dnrReady ??= initExtension(extension));
 | 
						|
}
 | 
						|
 | 
						|
function validateManifestEntry(extension) {
 | 
						|
  const ruleResourcesArray =
 | 
						|
    extension.manifest.declarative_net_request.rule_resources;
 | 
						|
 | 
						|
  const getWarningMessage = msg =>
 | 
						|
    `Warning processing declarative_net_request: ${msg}`;
 | 
						|
 | 
						|
  const { MAX_NUMBER_OF_STATIC_RULESETS } = lazy.ExtensionDNRLimits;
 | 
						|
  if (ruleResourcesArray.length > MAX_NUMBER_OF_STATIC_RULESETS) {
 | 
						|
    extension.manifestWarning(
 | 
						|
      getWarningMessage(
 | 
						|
        `Static rulesets are exceeding the MAX_NUMBER_OF_STATIC_RULESETS limit (${MAX_NUMBER_OF_STATIC_RULESETS}).`
 | 
						|
      )
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  const seenRulesetIds = new Set();
 | 
						|
  const seenRulesetPaths = new Set();
 | 
						|
  const duplicatedRulesetIds = [];
 | 
						|
  const duplicatedRulesetPaths = [];
 | 
						|
  for (const [idx, { id, path }] of ruleResourcesArray.entries()) {
 | 
						|
    if (seenRulesetIds.has(id)) {
 | 
						|
      duplicatedRulesetIds.push({ idx, id });
 | 
						|
    }
 | 
						|
    if (seenRulesetPaths.has(path)) {
 | 
						|
      duplicatedRulesetPaths.push({ idx, path });
 | 
						|
    }
 | 
						|
    seenRulesetIds.add(id);
 | 
						|
    seenRulesetPaths.add(path);
 | 
						|
  }
 | 
						|
 | 
						|
  if (duplicatedRulesetIds.length) {
 | 
						|
    const errorDetails = duplicatedRulesetIds
 | 
						|
      .map(({ idx, id }) => `"${id}" at index ${idx}`)
 | 
						|
      .join(", ");
 | 
						|
    extension.manifestWarning(
 | 
						|
      getWarningMessage(
 | 
						|
        `Static ruleset ids should be unique, duplicated ruleset ids: ${errorDetails}.`
 | 
						|
      )
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  if (duplicatedRulesetPaths.length) {
 | 
						|
    // NOTE: technically Chrome allows duplicated paths without any manifest
 | 
						|
    // validation warnings or errors, but if this happens it not unlikely to be
 | 
						|
    // actually a mistake in the manifest that may have been missed.
 | 
						|
    //
 | 
						|
    // In Firefox we decided to allow the same behavior to avoid introducing a chrome
 | 
						|
    // incompatibility, but we still warn about it to avoid extension developers
 | 
						|
    // to investigate more easily issue that may be due to duplicated rulesets
 | 
						|
    // paths.
 | 
						|
    const errorDetails = duplicatedRulesetPaths
 | 
						|
      .map(({ idx, path }) => `"${path}" at index ${idx}`)
 | 
						|
      .join(", ");
 | 
						|
    extension.manifestWarning(
 | 
						|
      getWarningMessage(
 | 
						|
        `Static rulesets paths are not unique, duplicated ruleset paths: ${errorDetails}.`
 | 
						|
      )
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  const { MAX_NUMBER_OF_ENABLED_STATIC_RULESETS } = lazy.ExtensionDNRLimits;
 | 
						|
 | 
						|
  const enabledRulesets = ruleResourcesArray.filter(rs => rs.enabled);
 | 
						|
  if (enabledRulesets.length > MAX_NUMBER_OF_ENABLED_STATIC_RULESETS) {
 | 
						|
    const exceedingRulesetIds = enabledRulesets
 | 
						|
      .slice(MAX_NUMBER_OF_ENABLED_STATIC_RULESETS)
 | 
						|
      .map(ruleset => `"${ruleset.id}"`)
 | 
						|
      .join(", ");
 | 
						|
    extension.manifestWarning(
 | 
						|
      getWarningMessage(
 | 
						|
        `Enabled static rulesets are exceeding the MAX_NUMBER_OF_ENABLED_STATIC_RULESETS limit (${MAX_NUMBER_OF_ENABLED_STATIC_RULESETS}): ${exceedingRulesetIds}.`
 | 
						|
      )
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
async function updateEnabledStaticRulesets(extension, updateRulesetOptions) {
 | 
						|
  await ensureInitialized(extension);
 | 
						|
  await lazy.ExtensionDNRStore.updateEnabledStaticRulesets(
 | 
						|
    extension,
 | 
						|
    updateRulesetOptions
 | 
						|
  );
 | 
						|
}
 | 
						|
 | 
						|
async function updateDynamicRules(extension, updateRuleOptions) {
 | 
						|
  await ensureInitialized(extension);
 | 
						|
  await lazy.ExtensionDNRStore.updateDynamicRules(extension, updateRuleOptions);
 | 
						|
}
 | 
						|
 | 
						|
// exports used by the DNR API implementation.
 | 
						|
export const ExtensionDNR = {
 | 
						|
  RuleValidator,
 | 
						|
  RuleQuotaCounter,
 | 
						|
  clearRuleManager,
 | 
						|
  ensureInitialized,
 | 
						|
  getMatchedRulesForRequest,
 | 
						|
  getRuleManager,
 | 
						|
  updateDynamicRules,
 | 
						|
  updateEnabledStaticRulesets,
 | 
						|
  validateManifestEntry,
 | 
						|
  beforeWebRequestEvent,
 | 
						|
  handleRequest,
 | 
						|
};
 |