r;
  }

  bootup() {
    browser.aboutConfigPrefs.onPrefChange.addListener(() => {
      this.checkInterventionPref();
    }, this.INTERVENTION_PREF);
    this.checkInterventionPref();
  }

  async updateInterventions(_data) {
    const data = structuredClone(_data);
    await this.disableInterventions(data);
    await this.enableInterventions(data);
    for (const intervention of data) {
      const { id } = intervention;
      const i = this._availableInterventions.findIndex(v => v.id === id);
      if (i > -1) {
        this._availableInterventions[i] = intervention;
      } else {
        this._availableInterventions.push(intervention);
      }
    }
    return data;
  }

  checkInterventionPref() {
    navigator.locks.request("pref_check_lock", async () => {
      const value = browser.aboutConfigPrefs.getPref(this.INTERVENTION_PREF);
      if (value === undefined) {
        await browser.aboutConfigPrefs.setPref(this.INTERVENTION_PREF, true);
      } else if (value === false) {
        await this.disableInterventions();
      } else {
        await this.enableInterventions();
      }
    });
  }

  getAvailableInterventions() {
    return this._availableInterventions;
  }

  _getActiveInterventionById(whichId) {
    return this._availableInterventions.find(({ id }) => id === whichId);
  }

  isEnabled() {
    return this._interventionsEnabled;
  }

  async enableInterventions(whichInterventions = this._availableInterventions) {
    return navigator.locks.request("intervention_lock", async () => {
      await this._enableInterventionsNow(whichInterventions);
    });
  }

  async disableInterventions(
    whichInterventions = this._availableInterventions
  ) {
    return navigator.locks.request("intervention_lock", async () => {
      for (const config of whichInterventions) {
        const disabling_pref_listener = this._disabledPrefListeners[config.id];
        if (disabling_pref_listener) {
          browser.aboutConfigPrefs.onPrefChange.removeListener(
            disabling_pref_listener
          );
          delete this._disabledPrefListeners[config.id];
        }

        await this._disableInterventionNow(config);
      }

      this._interventionsEnabled = false;
      this._aboutCompatBroker.portsToAboutCompatTabs.broadcast({
        interventionsChanged: false,
      });
    });
  }

  #checkedPrefListeners = new Map();
  #checkedPrefCache = new Map();

  async onCheckedPrefChanged(pref) {
    navigator.locks.request("pref_check_lock", async () => {
      this.#checkedPrefCache.delete(pref);
      const toRecheck = this._availableInterventions.filter(cfg =>
        cfg.interventions.find(i => i.pref_check && pref in i.pref_check)
      );
      await this.updateInterventions(toRecheck);
    });
  }

  _check_for_needed_prefs(intervention) {
    if (!intervention.pref_check) {
      return true;
    }
    for (const pref of Object.keys(intervention.pref_check ?? {})) {
      if (!this.#checkedPrefListeners.has(pref)) {
        const listener = () => this.onCheckedPrefChanged(pref);
        this.#checkedPrefListeners.set(pref, listener);
        browser.aboutConfigPrefs.onPrefChange.addListener(listener, pref);
      }
    }
    for (const [pref, value] of Object.entries(intervention.pref_check ?? {})) {
      if (!this.#checkedPrefCache.has(pref)) {
        this.#checkedPrefCache.set(
          pref,
          browser.aboutConfigPrefs.getPref(pref)
        );
      }
      if (value !== this.#checkedPrefCache.get(pref)) {
        return false;
      }
    }
    return true;
  }

  async _enableInterventionsNow(whichInterventions) {
    // resolveReady may change while we're updating
    const resolveReady = this._resolveReady;

    const skipped = [];

    const channel = await browser.appConstants.getEffectiveUpdateChannel();
    const version =
      this.versionForTesting ??
      (await browser.runtime.getBrowserInfo()).version;
    const cleanVersion = parseFloat(version.match(/\d+(\.\d+)?/)[0]);

    const os = await InterventionHelpers.getOS();
    this.currentPlatform = os;

    const customFunctionNames = new Set(Object.keys(this._customFunctions));

    const contentScriptsToRegister = [];
    for (const config of whichInterventions) {
      config.active = false;

      if (config.isMissingFiles) {
        skipped.push(config.label);
        continue;
      }

      config.DISABLING_PREF = `disabled_interventions.${config.id}`;
      const disabledPrefListener = () => {
        navigator.locks.request("pref_check_lock", async () => {
          const value = browser.aboutConfigPrefs.getPref(config.DISABLING_PREF);
          if (value === true) {
            await this.disableIntervention(config);
            debugLog(
              `Webcompat intervention for ${config.label} disabled by pref`
            );
          } else {
            await this.enableIntervention(config);
            debugLog(
              `Webcompat intervention for ${config.label} enabled by pref`
            );
          }
          this._aboutCompatBroker.portsToAboutCompatTabs.broadcast({
            interventionsChanged:
              this._aboutCompatBroker.filterInterventions(whichInterventions),
          });
        });
      };
      this._disabledPrefListeners[config.id] = disabledPrefListener;
      browser.aboutConfigPrefs.onPrefChange.addListener(
        disabledPrefListener,
        config.DISABLING_PREF
      );

      const disablingPrefValue = browser.aboutConfigPrefs.getPref(
        config.DISABLING_PREF
      );

      for (const intervention of config.interventions) {
        intervention.enabled = false;
        if (!this._check_for_needed_prefs(intervention)) {
          continue;
        }
        if (
          InterventionHelpers.shouldSkip(intervention, cleanVersion, channel)
        ) {
          continue;
        }
        if (
          InterventionHelpers.isMissingCustomFunctions(
            intervention,
            customFunctionNames
          )
        ) {
          continue;
        }
        if (!(await InterventionHelpers.checkPlatformMatches(intervention))) {
          // special case: allow platforms=[] to indicate "disabled by default"
          if (
            intervention.platforms &&
            !intervention.platforms.length &&
            !intervention.not_platforms
          ) {
            config.availableOnPlatform = true;
          }
          continue;
        }
        intervention.enabled = true;
        config.availableOnPlatform = true;
      }

      if (!config.availableOnPlatform) {
        skipped.push(config.label);
        continue;
      }
      if (disablingPrefValue === true) {
        skipped.push(config.label);
        continue;
      }

      try {
        contentScriptsToRegister.push(
          ...(await this._enableInterventionNow(config))
        );
      } catch (e) {
        console.error("Error enabling intervention(s) for", config.label, e);
      }
    }
    this._registerContentScripts(contentScriptsToRegister);

    if (skipped.length) {
      debugLog(
        "Skipping",
        skipped.length,
        "un-needed interventions",
        skipped.sort()
      );
    }

    this._interventionsEnabled = true;
    this._aboutCompatBroker.portsToAboutCompatTabs.broadcast({
      interventionsChanged:
        this._aboutCompatBroker.filterInterventions(whichInterventions),
    });

    resolveReady();
  }

  async enableIntervention(config, force = false) {
    return navigator.locks.request("intervention_lock", async () => {
      await this._enableInterventionNow(config, {
        force,
        registerContentScripts: true,
      });
    });
  }

  async disableIntervention(config) {
    return navigator.locks.request("intervention_lock", async () => {
      await this._disableInterventionNow(config);
    });
  }

  async _enableInterventionNow(config, options = {}) {
    const { force = false, registerContentScripts } = options;
    if (config.active) {
      return [];
    }

    const { bugs, label } = config;
    const blocks = Object.values(bugs)
      .map(bug => bug.blocks)
      .flat()
      .filter(v => v !== undefined);
    const matches = Object.values(bugs)
      .map(bug => bug.matches)
      .flat()
      .filter(v => v !== undefined);

    let somethingWasEnabled = false;
    let contentScriptsToRegister = [];
    for (const intervention of config.interventions) {
      if (!intervention.enabled && !force) {
        continue;
      }

      await this._changeCustomFuncs("enable", label, intervention, config);
      if (intervention.content_scripts) {
        const contentScriptsForIntervention =
          this._buildContentScriptRegistrations(label, intervention, matches);
        this._contentScriptsPerIntervention.set(
          intervention,
          contentScriptsForIntervention
        );
        contentScriptsToRegister.push(...contentScriptsForIntervention);
      }
      await this._enableUAOverrides(label, intervention, matches);
      await this._enableRequestBlocks(label, intervention, blocks);
      somethingWasEnabled = true;
      intervention.enabled = true;
    }
    if (registerContentScripts) {
      this._registerContentScripts(contentScriptsToRegister);
    }

    if (!this._getActiveInterventionById(config.id)) {
      this._availableInterventions.push(config);
      debugLog("Added webcompat intervention", config.id, config);
    } else {
      for (const [index, oldConfig] of this._availableInterventions.entries()) {
        if (oldConfig.id === config.id && oldConfig !== config) {
          debugLog("Replaced webcompat intervention", oldConfig.id, config);
          this._availableInterventions[index] = config;
        }
      }
    }

    config.active = somethingWasEnabled;
    return contentScriptsToRegister;
  }

  async _disableInterventionNow(_config) {
    const config = this._getActiveInterventionById(_config?.id ?? _config);
    if (!config) {
      return;
    }

    const { active, label, interventions } = config;

    if (!active) {
      return;
    }

    for (const intervention of interventions) {
      if (!intervention.enabled) {
        continue;
      }

      await this._changeCustomFuncs("disable", label, intervention, config);
      if (intervention.content_scripts) {
        await this._disableContentScripts(label, intervention);
      }

      // This covers both request blocks and ua_string cases
      const listeners = this._activeListenersPerIntervention.get(intervention);
      if (listeners) {
        for (const [name, listener] of Object.entries(listeners)) {
          browser.webRequest[name].removeListener(listener);
        }
        this._activeListenersPerIntervention.delete(intervention);
      }
    }

    config.active = false;
  }

  async _changeCustomFuncs(action, label, intervention, config) {
    for (const [customFuncName, customFunc] of Object.entries(
      this._customFunctions
    )) {
      if (customFuncName in intervention) {
        for (const details of intervention[customFuncName]) {
          try {
            await customFunc[action](details, config);
          } catch (e) {
            console.trace(
              `Error while calling custom function ${customFuncName}.${action} for ${label}:`,
              e
            );
          }
        }
      }
    }
  }

  async _enableUAOverrides(label, intervention, matches) {
    if (!("ua_string" in intervention)) {
      return;
    }

    let listeners = this._activeListenersPerIntervention.get(intervention);
    if (!listeners) {
      listeners = {};
      this._activeListenersPerIntervention.set(intervention, listeners);
    }

    const listener = details => {
      const { enabled, ua_string } = intervention;

      // Don't actually override the UA for an experiment if the user is not
      // part of the experiment (unless they force-enabed the override).
      if (
        enabled &&
        (!intervention.experiment || intervention.permanentPrefEnabled === true)
      ) {
        for (const header of details.requestHeaders) {
          if (header.name.toLowerCase() !== "user-agent") {
            continue;
          }

          // Don't override the UA if we're on a mobile device that has the
          // "Request Desktop Site" mode enabled. The UA for the desktop mode
          // is set inside Gecko with a simple string replace, so we can use
          // that as a check, see https://searchfox.org/mozilla-central/rev/89d33e1c3b0a57a9377b4815c2f4b58d933b7c32/mobile/android/chrome/geckoview/GeckoViewSettingsChild.js#23-28
          let isMobileWithDesktopMode =
            this.currentPlatform == "android" &&
            header.value.includes("X11; Linux x86_64");
          if (isMobileWithDesktopMode) {
            continue;
          }

          header.value = InterventionHelpers.applyUAChanges(
            header.value,
            ua_string
          );
        }
      }
      return { requestHeaders: details.requestHeaders };
    };

    browser.webRequest.onBeforeSendHeaders.addListener(
      listener,
      { urls: matches },
      ["blocking", "requestHeaders"]
    );

    listeners.onBeforeSendHeaders = listener;

    debugLog(`Enabled UA override for ${label}`);
  }

  async _enableRequestBlocks(label, intervention, blocks) {
    if (!blocks.length) {
      return;
    }

    let listeners = this._activeListenersPerIntervention.get(intervention);
    if (!listeners) {
      listeners = {};
      this._activeListenersPerIntervention.set(intervention, listeners);
    }

    const listener = () => {
      return { cancel: true };
    };

    browser.webRequest.onBeforeRequest.addListener(listener, { urls: blocks }, [
      "blocking",
    ]);

    listeners.onBeforeRequest = listener;
    debugLog(`Blocking requests as specified for ${label}`);
  }

  async _registerContentScripts(scriptsToReg) {
    // Try to avoid re-registering scripts already registered
    // (e.g. if the webcompat background page is restarted
    // after an extension process crash, after having registered
    // the content scripts already once), but do not prevent
    // to try registering them again if the getRegisteredContentScripts
    // method returns an unexpected rejection.

    const ids = scriptsToReg.map(s => s.id);
    if (!ids.length) {
      return;
    }
    try {
      const alreadyRegged = await browser.scripting.getRegisteredContentScripts(
        { ids }
      );
      const alreadyReggedIds = alreadyRegged.map(script => script.id);
      const stillNeeded = scriptsToReg.filter(
        ({ id }) => !alreadyReggedIds.includes(id)
      );
      await browser.scripting.registerContentScripts(stillNeeded);
      debugLog(
        `Registered still-not-active webcompat content scripts`,
        stillNeeded
      );
    } catch (e) {
      try {
        await browser.scripting.registerContentScripts(scriptsToReg);
        debugLog(
          `Registered all webcompat content scripts after error registering just non-active ones`,
          scriptsToReg,
          e
        );
      } catch (e2) {
        console.error(
          `Error while registering webcompat content scripts:`,
          e2,
          scriptsToReg
        );
      }
    }
  }

  async _disableContentScripts(label, intervention) {
    const contentScripts =
      this._contentScriptsPerIntervention.get(intervention);
    if (contentScripts) {
      for (const id of contentScripts.map(s => s.id)) {
        try {
          await browser.scripting.unregisterContentScripts({ ids: [id] });
        } catch (_) {}
      }
    }
  }

  _buildContentScriptRegistrations(label, intervention, matches) {
    const registration = {
      id: `webcompat intervention for ${label}: ${JSON.stringify(intervention.content_scripts)}`,
      matches,
      persistAcrossSessions: false,
    };

    let { all_frames, css, js, run_at } = intervention.content_scripts;
    if (!css && !js) {
      console.error(`Missing js or css for content_script in ${label}`);
      return [];
    }
    if (all_frames) {
      registration.allFrames = true;
    }
    if (css) {
      registration.css = css.map(item => {
        if (item.includes("/")) {
          return item;
        }
        return `injections/css/${item}`;
      });
    }
    if (js) {
      registration.js = js.map(item => {
        if (item.includes("/")) {
          return item;
        }
        return `injections/js/${item}`;
      });
    }
    if (run_at) {
      registration.runAt = run_at;
    } else {
      registration.runAt = "document_start";
    }

    return [registration];
  }
}
PK