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

const PREFS_CHANGING_CATEGORY = new Set([
  "network.cookie.cookieBehavior",
  "network.cookie.cookieBehavior.pbmode",
  "network.http.referer.disallowCrossSiteRelaxingDefault",
  "network.http.referer.disallowCrossSiteRelaxingDefault.top_navigation",
  "privacy.partition.network_state.ocsp_cache",
  "privacy.query_stripping.enabled",
  "privacy.query_stripping.enabled.pbmode",
  "privacy.fingerprintingProtection",
  "privacy.fingerprintingProtection.pbmode",
]);

/**
 * @class ContentBlockingPrefs
 *
 * Manages how the content blocking and anti-tracking preferences relate to the
 * broad Tracking Protection categories (standard, strict and custom).
 *
 * @typedef {"standard"|"strict"|"custom"} CBCategory
 */
export let ContentBlockingPrefs = {
  PREF_CB_CATEGORY: "browser.contentblocking.category",
  PREF_STRICT_DEF: "browser.contentblocking.features.strict",
  PREF_ALLOW_LIST_BASELINE:
    "privacy.trackingprotection.allow_list.baseline.enabled",
  PREF_ALLOW_LIST_CONVENIENCE:
    "privacy.trackingprotection.allow_list.convenience.enabled",
  PREF_LNA_ETP_ENABLED: "network.lna.etp.enabled",

  switchingCategory: false,

  /**
   * Apply a category preference rule to update preference expectations.   *
   *
   * @param {string} item - The rule to apply (e.g., "tp", "-fp", "lna")
   * @param {string} type - The category type ("strict", "standard")
   */
  // eslint-disable-next-line complexity
  applyCategoryPref(item, type) {
    switch (item) {
      case "tp":
        this.CATEGORY_PREFS[type]["privacy.trackingprotection.enabled"] = true;
        break;
      case "-tp":
        this.CATEGORY_PREFS[type]["privacy.trackingprotection.enabled"] = false;
        break;
      case "tpPrivate":
        this.CATEGORY_PREFS[type]["privacy.trackingprotection.pbmode.enabled"] =
          true;
        break;
      case "-tpPrivate":
        this.CATEGORY_PREFS[type]["privacy.trackingprotection.pbmode.enabled"] =
          false;
        break;
      case "fp":
        this.CATEGORY_PREFS[type][
          "privacy.trackingprotection.fingerprinting.enabled"
        ] = true;
        break;
      case "-fp":
        this.CATEGORY_PREFS[type][
          "privacy.trackingprotection.fingerprinting.enabled"
        ] = false;
        break;
      case "cryptoTP":
        this.CATEGORY_PREFS[type][
          "privacy.trackingprotection.cryptomining.enabled"
        ] = true;
        break;
      case "-cryptoTP":
        this.CATEGORY_PREFS[type][
          "privacy.trackingprotection.cryptomining.enabled"
        ] = false;
        break;
      case "stp":
        this.CATEGORY_PREFS[type][
          "privacy.trackingprotection.socialtracking.enabled"
        ] = true;
        break;
      case "-stp":
        this.CATEGORY_PREFS[type][
          "privacy.trackingprotection.socialtracking.enabled"
        ] = false;
        break;
      case "emailTP":
        this.CATEGORY_PREFS[type][
          "privacy.trackingprotection.emailtracking.enabled"
        ] = true;
        break;
      case "-emailTP":
        this.CATEGORY_PREFS[type][
          "privacy.trackingprotection.emailtracking.enabled"
        ] = false;
        break;
      case "emailTPPrivate":
        this.CATEGORY_PREFS[type][
          "privacy.trackingprotection.emailtracking.pbmode.enabled"
        ] = true;
        break;
      case "-emailTPPrivate":
        this.CATEGORY_PREFS[type][
          "privacy.trackingprotection.emailtracking.pbmode.enabled"
        ] = false;
        break;
      case "consentmanagerSkip":
        this.CATEGORY_PREFS[type][
          "privacy.trackingprotection.consentmanager.skip.enabled"
        ] = true;
        break;
      case "-consentmanagerSkip":
        this.CATEGORY_PREFS[type][
          "privacy.trackingprotection.consentmanager.skip.enabled"
        ] = false;
        break;
      case "consentmanagerSkipPrivate":
        this.CATEGORY_PREFS[type][
          "privacy.trackingprotection.consentmanager.skip.pbmode.enabled"
        ] = true;
        break;
      case "-consentmanagerSkipPrivate":
        this.CATEGORY_PREFS[type][
          "privacy.trackingprotection.consentmanager.skip.pbmode.enabled"
        ] = false;
        break;
      case "lvl2":
        this.CATEGORY_PREFS[type][
          "privacy.annotate_channels.strict_list.enabled"
        ] = true;
        break;
      case "-lvl2":
        this.CATEGORY_PREFS[type][
          "privacy.annotate_channels.strict_list.enabled"
        ] = false;
        break;
      case "rp":
        this.CATEGORY_PREFS[type][
          "network.http.referer.disallowCrossSiteRelaxingDefault"
        ] = true;
        break;
      case "-rp":
        this.CATEGORY_PREFS[type][
          "network.http.referer.disallowCrossSiteRelaxingDefault"
        ] = false;
        break;
      case "rpTop":
        this.CATEGORY_PREFS[type][
          "network.http.referer.disallowCrossSiteRelaxingDefault.top_navigation"
        ] = true;
        break;
      case "-rpTop":
        this.CATEGORY_PREFS[type][
          "network.http.referer.disallowCrossSiteRelaxingDefault.top_navigation"
        ] = false;
        break;
      case "ocsp":
        this.CATEGORY_PREFS[type][
          "privacy.partition.network_state.ocsp_cache"
        ] = true;
        break;
      case "-ocsp":
        this.CATEGORY_PREFS[type][
          "privacy.partition.network_state.ocsp_cache"
        ] = false;
        break;
      case "qps":
        this.CATEGORY_PREFS[type]["privacy.query_stripping.enabled"] = true;
        break;
      case "-qps":
        this.CATEGORY_PREFS[type]["privacy.query_stripping.enabled"] = false;
        break;
      case "qpsPBM":
        this.CATEGORY_PREFS[type]["privacy.query_stripping.enabled.pbmode"] =
          true;
        break;
      case "-qpsPBM":
        this.CATEGORY_PREFS[type]["privacy.query_stripping.enabled.pbmode"] =
          false;
        break;
      case "fpp":
        this.CATEGORY_PREFS[type]["privacy.fingerprintingProtection"] = true;
        break;
      case "-fpp":
        this.CATEGORY_PREFS[type]["privacy.fingerprintingProtection"] = false;
        break;
      case "fppPrivate":
        this.CATEGORY_PREFS[type]["privacy.fingerprintingProtection.pbmode"] =
          true;
        break;
      case "-fppPrivate":
        this.CATEGORY_PREFS[type]["privacy.fingerprintingProtection.pbmode"] =
          false;
        break;
      case "cookieBehavior0":
        this.CATEGORY_PREFS[type]["network.cookie.cookieBehavior"] =
          Ci.nsICookieService.BEHAVIOR_ACCEPT;
        break;
      case "cookieBehavior1":
        this.CATEGORY_PREFS[type]["network.cookie.cookieBehavior"] =
          Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN;
        break;
      case "cookieBehavior2":
        this.CATEGORY_PREFS[type]["network.cookie.cookieBehavior"] =
          Ci.nsICookieService.BEHAVIOR_REJECT;
        break;
      case "cookieBehavior3":
        this.CATEGORY_PREFS[type]["network.cookie.cookieBehavior"] =
          Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN;
        break;
      case "cookieBehavior4":
        this.CATEGORY_PREFS[type]["network.cookie.cookieBehavior"] =
          Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER;
        break;
      case "cookieBehavior5":
        this.CATEGORY_PREFS[type]["network.cookie.cookieBehavior"] =
          Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN;
        break;
      case "cookieBehaviorPBM0":
        this.CATEGORY_PREFS[type]["network.cookie.cookieBehavior.pbmode"] =
          Ci.nsICookieService.BEHAVIOR_ACCEPT;
        break;
      case "cookieBehaviorPBM1":
        this.CATEGORY_PREFS[type]["network.cookie.cookieBehavior.pbmode"] =
          Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN;
        break;
      case "cookieBehaviorPBM2":
        this.CATEGORY_PREFS[type]["network.cookie.cookieBehavior.pbmode"] =
          Ci.nsICookieService.BEHAVIOR_REJECT;
        break;
      case "cookieBehaviorPBM3":
        this.CATEGORY_PREFS[type]["network.cookie.cookieBehavior.pbmode"] =
          Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN;
        break;
      case "cookieBehaviorPBM4":
        this.CATEGORY_PREFS[type]["network.cookie.cookieBehavior.pbmode"] =
          Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER;
        break;
      case "cookieBehaviorPBM5":
        this.CATEGORY_PREFS[type]["network.cookie.cookieBehavior.pbmode"] =
          Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN;
        break;
      case "3pcd":
        this.CATEGORY_PREFS[type][
          "network.cookie.cookieBehavior.optInPartitioning"
        ] = true;
        break;
      case "-3pcd":
        this.CATEGORY_PREFS[type][
          "network.cookie.cookieBehavior.optInPartitioning"
        ] = false;
        break;
      case "btp":
        this.CATEGORY_PREFS[type]["privacy.bounceTrackingProtection.mode"] =
          Ci.nsIBounceTrackingProtection.MODE_ENABLED;
        break;
      case "-btp":
        // We currently consider MODE_ENABLED_DRY_RUN the "off" state. See
        // nsIBounceTrackingProtection.idl for details.
        this.CATEGORY_PREFS[type]["privacy.bounceTrackingProtection.mode"] =
          Ci.nsIBounceTrackingProtection.MODE_ENABLED_DRY_RUN;
        break;
      case "lna":
        // turn on LNA for etp strict only if network.lna.etp.enabled
        // network.lna.etp.enabled is controlled by nimbus
        if (Services.prefs.getBoolPref(this.PREF_LNA_ETP_ENABLED, false)) {
          this.CATEGORY_PREFS[type]["network.lna.blocking"] = true;
        }
        break;
      case "-lna":
        // currently LNA is only enabled with ETP strict mode with pref network.lna.etp.enabled
        if (Services.prefs.getBoolPref(this.PREF_LNA_ETP_ENABLED, false)) {
          this.CATEGORY_PREFS[type]["network.lna.blocking"] = false;
        }
        break;
      default:
        console.error(`Error: Unknown rule observed ${item}`);
    }
  },

  setPrefExpectations() {
    // The prefs inside CATEGORY_PREFS are initial values.
    // If the pref remains null, then it will expect the default value.
    // The "standard" category is defined as expecting default values of the
    // listed prefs. The "strict" category lists all prefs that will be set
    // according to the strict feature pref.
    this.CATEGORY_PREFS = {
      strict: {
        "network.cookie.cookieBehavior": null,
        "network.cookie.cookieBehavior.pbmode": null,
        "privacy.trackingprotection.pbmode.enabled": null,
        "privacy.trackingprotection.enabled": null,
        "privacy.trackingprotection.socialtracking.enabled": null,
        "privacy.trackingprotection.fingerprinting.enabled": null,
        "privacy.trackingprotection.cryptomining.enabled": null,
        "privacy.trackingprotection.emailtracking.enabled": null,
        "privacy.trackingprotection.emailtracking.pbmode.enabled": null,
        "privacy.trackingprotection.consentmanager.skip.enabled": null,
        "privacy.trackingprotection.consentmanager.skip.pbmode.enabled": null,
        "privacy.annotate_channels.strict_list.enabled": null,
        "network.http.referer.disallowCrossSiteRelaxingDefault": null,
        "network.http.referer.disallowCrossSiteRelaxingDefault.top_navigation":
          null,
        "privacy.partition.network_state.ocsp_cache": null,
        "privacy.query_stripping.enabled": null,
        "privacy.query_stripping.enabled.pbmode": null,
        "privacy.fingerprintingProtection": null,
        "privacy.fingerprintingProtection.pbmode": null,
        "network.cookie.cookieBehavior.optInPartitioning": null,
        "privacy.bounceTrackingProtection.mode": null,
        "network.lna.blocking": null,
        [this.PREF_ALLOW_LIST_BASELINE]: true,
        [this.PREF_ALLOW_LIST_CONVENIENCE]: false,
      },
      standard: {
        "network.cookie.cookieBehavior": null,
        "network.cookie.cookieBehavior.pbmode": null,
        "privacy.trackingprotection.pbmode.enabled": null,
        "privacy.trackingprotection.enabled": null,
        "privacy.trackingprotection.socialtracking.enabled": null,
        "privacy.trackingprotection.fingerprinting.enabled": null,
        "privacy.trackingprotection.cryptomining.enabled": null,
        "privacy.trackingprotection.emailtracking.enabled": null,
        "privacy.trackingprotection.emailtracking.pbmode.enabled": null,
        "privacy.trackingprotection.consentmanager.skip.enabled": null,
        "privacy.trackingprotection.consentmanager.skip.pbmode.enabled": null,
        "privacy.annotate_channels.strict_list.enabled": null,
        "network.http.referer.disallowCrossSiteRelaxingDefault": null,
        "network.http.referer.disallowCrossSiteRelaxingDefault.top_navigation":
          null,
        "privacy.partition.network_state.ocsp_cache": null,
        "privacy.query_stripping.enabled": null,
        "privacy.query_stripping.enabled.pbmode": null,
        "privacy.fingerprintingProtection": null,
        "privacy.fingerprintingProtection.pbmode": null,
        "network.cookie.cookieBehavior.optInPartitioning": null,
        "privacy.bounceTrackingProtection.mode": null,
        "network.lna.blocking": null,
        [this.PREF_ALLOW_LIST_BASELINE]: null,
        [this.PREF_ALLOW_LIST_CONVENIENCE]: null,
      },
    };
    let type = "strict";
    let rulesArray = Services.prefs
      .getStringPref(this.PREF_STRICT_DEF)
      .split(",");
    for (let item of rulesArray) {
      this.applyCategoryPref(item, type);
    }
  },

  /**
   * Checks if CB prefs match perfectly with one of our pre-defined categories.
   *
   * @param {CBCategory} category
   */
  prefsMatch(category) {
    // The category pref must be either unset, or match.
    if (
      Services.prefs.prefHasUserValue(this.PREF_CB_CATEGORY) &&
      Services.prefs.getStringPref(this.PREF_CB_CATEGORY) != category
    ) {
      return false;
    }
    for (let pref in this.CATEGORY_PREFS[category]) {
      let value = this.CATEGORY_PREFS[category][pref];
      // Ignore allow list prefs, since user is allowed to change them in strict mode.
      if (
        pref == this.PREF_ALLOW_LIST_BASELINE ||
        pref == this.PREF_ALLOW_LIST_CONVENIENCE
      ) {
        continue;
      }
      if (value == null) {
        if (Services.prefs.prefHasUserValue(pref)) {
          return false;
        }
      } else {
        let prefType = Services.prefs.getPrefType(pref);
        if (
          (prefType == Services.prefs.PREF_BOOL &&
            Services.prefs.getBoolPref(pref) != value) ||
          (prefType == Services.prefs.PREF_INT &&
            Services.prefs.getIntPref(pref) != value) ||
          (prefType == Services.prefs.PREF_STRING &&
            Services.prefs.getStringPref(pref) != value)
        ) {
          return false;
        }
      }
    }
    return true;
  },

  matchCBCategory() {
    if (this.switchingCategory) {
      return;
    }
    // If PREF_CB_CATEGORY is not set match users to a Content Blocking category. Check if prefs fit
    // perfectly into strict or standard, otherwise match with custom. If PREF_CB_CATEGORY has previously been set,
    // a change of one of these prefs necessarily puts us in "custom".
    if (this.prefsMatch("standard")) {
      Services.prefs.setStringPref(this.PREF_CB_CATEGORY, "standard");
    } else if (this.prefsMatch("strict")) {
      Services.prefs.setStringPref(this.PREF_CB_CATEGORY, "strict");
    } else {
      Services.prefs.setStringPref(this.PREF_CB_CATEGORY, "custom");
    }

    // If there is a custom policy which changes a related pref, then put the user in custom so
    // they still have access to other content blocking prefs, and to keep our default definitions
    // from changing.
    let policy = Services.policies.getActivePolicies();
    if (
      policy &&
      ((policy.EnableTrackingProtection &&
        !policy.EnableTrackingProtection.Category) ||
        policy.Cookies)
    ) {
      Services.prefs.setStringPref(this.PREF_CB_CATEGORY, "custom");
    }
  },

  updateCBCategory(preserveAllowListSettings = true) {
    if (
      this.switchingCategory ||
      !Services.prefs.prefHasUserValue(this.PREF_CB_CATEGORY)
    ) {
      return;
    }
    // Turn on switchingCategory flag, to ensure that when the individual prefs that change as a result
    // of the category change do not trigger yet another category change.
    this.switchingCategory = true;
    let value = Services.prefs.getStringPref(this.PREF_CB_CATEGORY);
    this.setPrefsToCategory(value, null, preserveAllowListSettings);
    this.switchingCategory = false;
  },

  /**
   * Sets all user-exposed content blocking preferences to values that match the selected category.
   *
   * @param {CBCategory} category
   * @param {boolean} lockPrefs - Whether to lock prefs after setting them
   * @param {boolean} preserveAllowListSettings - Whether to preserve existing allow list baseline and
   * convenience settings
   */
  setPrefsToCategory(category, lockPrefs, preserveAllowListSettings) {
    // Leave prefs as they were if we are switching to "custom" category.
    if (category == "custom") {
      return;
    }

    let prefBranch = lockPrefs
      ? Services.prefs.getDefaultBranch(null)
      : Services.prefs.getBranch(null);

    for (let pref in this.CATEGORY_PREFS[category]) {
      let value = this.CATEGORY_PREFS[category][pref];
      if (!Services.prefs.prefIsLocked(pref)) {
        if (value == null) {
          Services.prefs.clearUserPref(pref);
        } else {
          // On initialization, do not update "PREF_ALLOW_LIST_BASELINE" and "PREF_ALLOW_LIST_CONVENIENCE"
          // to make sure user's settings are preserved
          if (
            preserveAllowListSettings &&
            (pref == this.PREF_ALLOW_LIST_BASELINE ||
              pref == this.PREF_ALLOW_LIST_CONVENIENCE)
          ) {
            continue;
          }
          switch (Services.prefs.getPrefType(pref)) {
            case Services.prefs.PREF_BOOL:
              prefBranch.setBoolPref(pref, value);
              break;
            case Services.prefs.PREF_INT:
              prefBranch.setIntPref(pref, value);
              break;
            case Services.prefs.PREF_STRING:
              prefBranch.setStringPref(pref, value);
              break;
          }
          if (lockPrefs) {
            Services.prefs.lockPref(pref);
          }
        }
      }
    }
  },

  setPrefExpectationsAndUpdate() {
    this.setPrefExpectations();
    this.updateCBCategory();
  },

  observe(subject, topic, data) {
    if (topic != "nsPref:changed") {
      return;
    }

    // We need this early return to avoid updating the CB category prematurely. Specifically when
    // user changes the CB category, the observer for hasUserInteractedWithETPSettings is triggered
    // before the category is actually changed. If we do not return here, the category may be updated
    // incorrectly (for example, to custom when it should be strict).
    if (
      data ===
      "privacy.trackingprotection.allow_list.hasUserInteractedWithETPSettings"
    ) {
      return;
    }

    if (
      data.startsWith("privacy.trackingprotection") ||
      PREFS_CHANGING_CATEGORY.has(data)
    ) {
      this.matchCBCategory();
    }

    if (data.startsWith("privacy.trackingprotection")) {
      this.setPrefExpectations();
    } else if (data == this.PREF_CB_CATEGORY) {
      this.updateCBCategory(false);
    } else if (data == "browser.contentblocking.features.strict") {
      this.setPrefExpectationsAndUpdate();
    } else if (data == this.PREF_LNA_ETP_ENABLED) {
      this.setPrefExpectationsAndUpdate();
    }
  },

  init() {
    this.setPrefExpectationsAndUpdate();
    this.matchCBCategory();

    for (let prefix of PREF_PREFIXES_TO_OBSERVE) {
      Services.prefs.addObserver(prefix, this);
    }
  },

  uninit() {
    for (let prefix of PREF_PREFIXES_TO_OBSERVE) {
      Services.prefs.removeObserver(prefix, this);
    }
  },
};

const PREF_PREFIXES_TO_OBSERVE = new Set([
  "privacy.trackingprotection",
  "network.cookie.cookieBehavior",
  "network.http.referer.disallowCrossSiteRelaxingDefault",
  "privacy.partition.network_state.ocsp_cache",
  "privacy.query_stripping.enabled",
  "privacy.fingerprintingProtection",
  ContentBlockingPrefs.PREF_CB_CATEGORY,
  ContentBlockingPrefs.PREF_STRICT_DEF,
  ContentBlockingPrefs.PREF_LNA_ETP_ENABLED,
]);

ContentBlockingPrefs.QueryInterface = ChromeUtils.generateQI([Ci.nsIObserver]);
