tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

shims.js (41875B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 /* globals browser, module, onMessageFromTab */
      8 
      9 // To grant shims access to bundled logo images without risking
     10 // exposing our moz-extension URL, we have the shim request them via
     11 // nonsense URLs which we then redirect to the actual files (but only
     12 // on tabs where a shim using a given logo happens to be active).
     13 const LogosBaseURL = "https://smartblock.firefox.etp/";
     14 
     15 const loggingPrefValue = browser.aboutConfigPrefs.getPref(
     16  "disable_debug_logging"
     17 );
     18 
     19 const releaseBranchPromise = browser.appConstants.getReleaseBranch();
     20 
     21 const platformPromise = browser.runtime.getPlatformInfo().then(info => {
     22  return info.os === "android" ? "android" : "desktop";
     23 });
     24 
     25 let debug = async function () {
     26  if (
     27    loggingPrefValue !== true &&
     28    (await releaseBranchPromise) !== "release_or_beta"
     29  ) {
     30    console.debug.apply(this, arguments);
     31  }
     32 };
     33 let error = async function () {
     34  if ((await releaseBranchPromise) !== "release_or_beta") {
     35    console.error.apply(this, arguments);
     36  }
     37 };
     38 let warn = async function () {
     39  if ((await releaseBranchPromise) !== "release_or_beta") {
     40    console.warn.apply(this, arguments);
     41  }
     42 };
     43 
     44 class Shim {
     45  constructor(opts, manager) {
     46    this.manager = manager;
     47 
     48    const { contentScripts, matches, unblocksOnOptIn } = opts;
     49 
     50    this.branches = opts.branches;
     51    this.bug = opts.bug;
     52    this.isGoogleTrendsDFPIFix = opts.custom == "google-trends-dfpi-fix";
     53    this.file = opts.file;
     54    this.hiddenInAboutCompat = opts.hiddenInAboutCompat;
     55    this.hosts = opts.hosts;
     56    this.id = opts.id;
     57    this.isMissingFiles = opts.isMissingFiles;
     58    this.logos = opts.logos || [];
     59    this.matches = [];
     60    this.name = opts.name;
     61    this.notHosts = opts.notHosts;
     62    this.onlyIfBlockedByETP = opts.onlyIfBlockedByETP;
     63    this.onlyIfDFPIActive = opts.onlyIfDFPIActive;
     64    this.onlyIfPrivateBrowsing = opts.onlyIfPrivateBrowsing;
     65    this._options = opts.options || {};
     66    this.webExposedShimHelpers = opts.webExposedShimHelpers;
     67    this.needsShimHelpers = opts.needsShimHelpers;
     68    this.platform = opts.platform || "all";
     69    this.runFirst = opts.runFirst;
     70    this.unblocksOnOptIn = unblocksOnOptIn;
     71    this.requestStorageAccessForRedirect = opts.requestStorageAccessForRedirect;
     72    this.shouldUseScriptingAPI = browser.aboutConfigPrefs.getPref(
     73      "useScriptingAPI",
     74      false
     75    );
     76    this.isSmartblockEmbedShim = opts.isSmartblockEmbedShim || false;
     77    debug(
     78      `WebCompat Shim ${this.id} will be injected using ${
     79        this.shouldUseScriptingAPI ? "scripting" : "contentScripts"
     80      } API`
     81    );
     82 
     83    this._hostOptIns = new Set();
     84    this._pBModeHostOptIns = new Set();
     85 
     86    this._disabledByConfig = opts.disabled;
     87    this._disabledGlobally = false;
     88    this._disabledForSession = false;
     89    this._disabledByPlatform = false;
     90    this._disabledByReleaseBranch = false;
     91    this._disabledBySmartblockEmbedPref = false;
     92 
     93    this._activeOnTabs = new Set();
     94    this._showedOptInOnTabs = new Set();
     95 
     96    const pref = `disabled_shims.${this.id}`;
     97 
     98    this.redirectsRequests = !!this.file && matches?.length;
     99 
    100    // NOTE: _contentScriptRegistrations is an array of string ids when
    101    // shouldUseScriptingAPI is true and an array of script handles returned
    102    // by contentScripts.register otherwise.
    103    this._contentScriptRegistrations = [];
    104 
    105    this.contentScripts = contentScripts || [];
    106    for (const script of this.contentScripts) {
    107      if (typeof script.css === "string") {
    108        script.css = [
    109          this.shouldUseScriptingAPI
    110            ? `/shims/${script.css}`
    111            : { file: `/shims/${script.css}` },
    112        ];
    113      }
    114      if (typeof script.js === "string") {
    115        script.js = [
    116          this.shouldUseScriptingAPI
    117            ? `/shims/${script.js}`
    118            : { file: `/shims/${script.js}` },
    119        ];
    120      }
    121    }
    122 
    123    for (const match of matches || []) {
    124      if (!match.types) {
    125        this.matches.push({ patterns: [match], types: ["script"] });
    126      } else {
    127        this.matches.push(match);
    128      }
    129      if (match.target) {
    130        this.redirectsRequests = true;
    131      }
    132    }
    133 
    134    browser.aboutConfigPrefs.onPrefChange.addListener(async () => {
    135      const value = browser.aboutConfigPrefs.getPref(pref);
    136      this._disabledPrefValue = value;
    137      this._onEnabledStateChanged({ alsoClearResourceCache: true });
    138    }, pref);
    139 
    140    this._disabledPrefValue = browser.aboutConfigPrefs.getPref(pref);
    141    this.ready = Promise.all([platformPromise, releaseBranchPromise]).then(
    142      ([platform, branch]) => {
    143        this._disabledByPlatform =
    144          this.platform !== "all" && this.platform !== platform;
    145 
    146        this._disabledByReleaseBranch = false;
    147        for (const supportedBranchAndPlatform of this.branches || []) {
    148          const [supportedBranch, supportedPlatform] =
    149            supportedBranchAndPlatform.split(":");
    150          if (
    151            (!supportedPlatform || supportedPlatform == platform) &&
    152            supportedBranch != branch
    153          ) {
    154            this._disabledByReleaseBranch = true;
    155          }
    156        }
    157 
    158        this._preprocessOptions(platform, branch);
    159        this._onEnabledStateChanged();
    160      }
    161    );
    162  }
    163 
    164  _preprocessOptions(platform, branch) {
    165    // options may be any value, but can optionally be gated for specified
    166    // platform/branches, if in the format `{value, branches, platform}`
    167    this.options = {};
    168    for (const [k, v] of Object.entries(this._options)) {
    169      if (v?.value) {
    170        if (
    171          (!v.platform || v.platform === platform) &&
    172          (!v.branches || v.branches.includes(branch))
    173        ) {
    174          this.options[k] = v.value;
    175        }
    176      } else {
    177        this.options[k] = v;
    178      }
    179    }
    180  }
    181 
    182  get enabled() {
    183    if (this.isMissingFiles) {
    184      return false;
    185    }
    186 
    187    if (this._disabledGlobally || this._disabledForSession) {
    188      return false;
    189    }
    190 
    191    if (this._disabledPrefValue !== undefined) {
    192      return !this._disabledPrefValue;
    193    }
    194 
    195    if (this.isSmartblockEmbedShim && this._disabledBySmartblockEmbedPref) {
    196      return false;
    197    }
    198 
    199    return (
    200      !this._disabledByConfig &&
    201      !this._disabledByPlatform &&
    202      !this._disabledByReleaseBranch
    203    );
    204  }
    205 
    206  get disabledReason() {
    207    if (this.isMissingFiles) {
    208      return "missingFiles";
    209    }
    210 
    211    if (this._disabledGlobally) {
    212      return "globalPref";
    213    }
    214 
    215    if (this._disabledForSession) {
    216      return "session";
    217    }
    218 
    219    if (this._disabledPrefValue !== undefined) {
    220      if (this._disabledPrefValue === true) {
    221        return "pref";
    222      }
    223      return false;
    224    }
    225 
    226    if (this.isSmartblockEmbedShim && this._disabledBySmartblockEmbedPref) {
    227      return "smartblockEmbedDisabledByPref";
    228    }
    229 
    230    if (this._disabledByConfig) {
    231      return "config";
    232    }
    233 
    234    if (this._disabledByPlatform) {
    235      return "platform";
    236    }
    237 
    238    if (this._disabledByReleaseBranch) {
    239      return "releaseBranch";
    240    }
    241 
    242    return false;
    243  }
    244 
    245  get disabledBySmartblockEmbedPref() {
    246    return this._disabledBySmartblockEmbedPref;
    247  }
    248 
    249  set disabledBySmartblockEmbedPref(value) {
    250    this._disabledBySmartblockEmbedPref = value;
    251    this._onEnabledStateChanged({ alsoClearResourceCache: true });
    252  }
    253 
    254  onAllShimsEnabled() {
    255    const wasEnabled = this.enabled;
    256    this._disabledGlobally = false;
    257    if (!wasEnabled) {
    258      this._onEnabledStateChanged({ alsoClearResourceCache: true });
    259    }
    260  }
    261 
    262  onAllShimsDisabled() {
    263    const wasEnabled = this.enabled;
    264    this._disabledGlobally = true;
    265    if (wasEnabled) {
    266      this._onEnabledStateChanged({ alsoClearResourceCache: true });
    267    }
    268  }
    269 
    270  enableForSession() {
    271    const wasEnabled = this.enabled;
    272    this._disabledForSession = false;
    273    if (!wasEnabled) {
    274      this._onEnabledStateChanged({ alsoClearResourceCache: true });
    275    }
    276  }
    277 
    278  disableForSession() {
    279    const wasEnabled = this.enabled;
    280    this._disabledForSession = true;
    281    if (wasEnabled) {
    282      this._onEnabledStateChanged({ alsoClearResourceCache: true });
    283    }
    284  }
    285 
    286  async _onEnabledStateChanged({ alsoClearResourceCache = false } = {}) {
    287    this.manager?.onShimStateChanged(this.id);
    288    if (!this.enabled) {
    289      await this._unregisterContentScripts();
    290      return this._revokeRequestsInETP(alsoClearResourceCache);
    291    }
    292    await this._registerContentScripts();
    293    return this._allowRequestsInETP(alsoClearResourceCache);
    294  }
    295 
    296  async _registerContentScripts() {
    297    if (
    298      this.contentScripts.length &&
    299      !this._contentScriptRegistrations.length
    300    ) {
    301      const matches = [];
    302      let idx = 0;
    303      for (const options of this.contentScripts) {
    304        matches.push(options.matches);
    305        if (this.shouldUseScriptingAPI) {
    306          // Some shims includes more than one script (e.g. Blogger one contains
    307          // a content script to be run on document_start and one to be run
    308          // on document_end.
    309          options.id = `shim-${this.id}-${idx++}`;
    310          options.persistAcrossSessions = false;
    311          // Having to call getRegisteredContentScripts each time we are going to
    312          // register a Shim content script is suboptimal, but avoiding that
    313          // may require a bit more changes (e.g. rework both Injections, Shim and Shims
    314          // classes to more easily register all content scripts with a single
    315          // call to the scripting API methods when the background script page is loading
    316          // and one per injection or shim being enabled from the AboutCompatBroker).
    317          // In the short term we call getRegisteredContentScripts and restrict it to
    318          // the script id we are about to register.
    319          let isAlreadyRegistered = false;
    320          try {
    321            const registeredScripts =
    322              await browser.scripting.getRegisteredContentScripts({
    323                ids: [options.id],
    324              });
    325            isAlreadyRegistered = !!registeredScripts.length;
    326          } catch (ex) {
    327            console.error(
    328              "Retrieve WebCompat GoFaster registered content scripts failed: ",
    329              ex
    330            );
    331          }
    332          try {
    333            if (!isAlreadyRegistered) {
    334              await browser.scripting.registerContentScripts([options]);
    335            }
    336            this._contentScriptRegistrations.push(options.id);
    337          } catch (ex) {
    338            console.error(
    339              "Registering WebCompat Shim content scripts failed: ",
    340              options,
    341              ex
    342            );
    343          }
    344        } else {
    345          const reg = await browser.contentScripts.register(options);
    346          this._contentScriptRegistrations.push(reg);
    347        }
    348      }
    349      const urls = Array.from(new Set(matches.flat()));
    350      debug("Enabling content scripts for these URLs:", urls);
    351    }
    352  }
    353 
    354  async _unregisterContentScripts() {
    355    if (this.shouldUseScriptingAPI) {
    356      for (const id of this._contentScriptRegistrations) {
    357        try {
    358          await browser.scripting.unregisterContentScripts({ ids: [id] });
    359        } catch (_) {}
    360      }
    361    } else {
    362      for (const registration of this._contentScriptRegistrations) {
    363        registration.unregister();
    364      }
    365    }
    366    this._contentScriptRegistrations = [];
    367  }
    368 
    369  async _allowRequestsInETP(alsoClearResourceCache) {
    370    let modified = false;
    371    const matches = this.matches.map(m => m.patterns).flat();
    372    if (matches.length) {
    373      // ensure requests shimmed in both PB and non-PB modes
    374      await browser.trackingProtection.shim(this.id, matches);
    375      modified = true;
    376    }
    377 
    378    if (this._hostOptIns.size) {
    379      const optIns = this.getApplicableOptIns();
    380      if (optIns.length) {
    381        await browser.trackingProtection.allow(
    382          this.id,
    383          this._optInPatterns,
    384          false,
    385          Array.from(this._hostOptIns)
    386        );
    387        modified = true;
    388      }
    389    }
    390 
    391    if (this._pBModeHostOptIns.size) {
    392      const optIns = this.getApplicableOptIns();
    393      if (optIns.length) {
    394        await browser.trackingProtection.allow(
    395          this.id,
    396          this._optInPatterns,
    397          true,
    398          Array.from(this._pBModeHostOptIns)
    399        );
    400        modified = true;
    401      }
    402    }
    403 
    404    if (this._haveCheckedEnabledPrefs && alsoClearResourceCache && modified) {
    405      this.clearResourceCache();
    406    }
    407  }
    408 
    409  async _revokeRequestsInETP(alsoClearResourceCache) {
    410    await browser.trackingProtection.revoke(this.id);
    411    if (this._haveCheckedEnabledPrefs && alsoClearResourceCache) {
    412      this.clearResourceCache();
    413    }
    414  }
    415 
    416  setActiveOnTab(tabId, active = true) {
    417    if (active) {
    418      this._activeOnTabs.add(tabId);
    419    } else {
    420      this._activeOnTabs.delete(tabId);
    421      this._showedOptInOnTabs.delete(tabId);
    422    }
    423  }
    424 
    425  isActiveOnTab(tabId) {
    426    return this._activeOnTabs.has(tabId);
    427  }
    428 
    429  meantForHost(host) {
    430    const { hosts, notHosts } = this;
    431    if (hosts || notHosts) {
    432      if (
    433        (notHosts && notHosts.includes(host)) ||
    434        (hosts && !hosts.includes(host))
    435      ) {
    436        return false;
    437      }
    438    }
    439    return true;
    440  }
    441 
    442  async unblocksURLOnOptIn(url) {
    443    if (!this._optInPatterns) {
    444      this._optInPatterns = await this.getApplicableOptIns();
    445    }
    446 
    447    if (!this._optInMatcher) {
    448      this._optInMatcher = browser.matchPatterns.getMatcher(
    449        Array.from(this._optInPatterns)
    450      );
    451    }
    452 
    453    return this._optInMatcher.matches(url);
    454  }
    455 
    456  isTriggeredByURLAndType(url, type) {
    457    for (const entry of this.matches || []) {
    458      if (!entry.types.includes(type)) {
    459        continue;
    460      }
    461      if (!entry.matcher) {
    462        entry.matcher = browser.matchPatterns.getMatcher(
    463          Array.from(entry.patterns)
    464        );
    465      }
    466      if (entry.matcher.matches(url)) {
    467        return entry;
    468      }
    469    }
    470 
    471    return undefined;
    472  }
    473 
    474  async getApplicableOptIns() {
    475    if (this._applicableOptIns) {
    476      return this._applicableOptIns;
    477    }
    478    const optins = [];
    479    for (const unblock of this.unblocksOnOptIn || []) {
    480      if (typeof unblock === "string") {
    481        optins.push(unblock);
    482        continue;
    483      }
    484      const { branches, patterns, platforms } = unblock;
    485      if (platforms?.length) {
    486        const platform = await platformPromise;
    487        if (platform !== "all" && !platforms.includes(platform)) {
    488          continue;
    489        }
    490      }
    491      if (branches?.length) {
    492        const branch = await releaseBranchPromise;
    493        if (!branches.includes(branch)) {
    494          continue;
    495        }
    496      }
    497      optins.push.apply(optins, patterns);
    498    }
    499    this._applicableOptIns = optins;
    500    return optins;
    501  }
    502 
    503  async onUserOptIn(host, isPrivateMode) {
    504    const optins = await this.getApplicableOptIns();
    505    const activeHostOptIns = isPrivateMode
    506      ? this._pBModeHostOptIns
    507      : this._hostOptIns;
    508    if (optins.length) {
    509      activeHostOptIns.add(host);
    510      await browser.trackingProtection.allow(
    511        this.id,
    512        optins,
    513        isPrivateMode,
    514        Array.from(activeHostOptIns)
    515      );
    516      this.clearResourceCache();
    517    }
    518  }
    519 
    520  hasUserOptedInAlready(host, isPrivateMode) {
    521    const activeHostOptIns = isPrivateMode
    522      ? this._pBModeHostOptIns
    523      : this._hostOptIns;
    524    return activeHostOptIns.has(host);
    525  }
    526 
    527  showOptInWarningOnce(tabId, origin) {
    528    if (this._showedOptInOnTabs.has(tabId)) {
    529      return Promise.resolve();
    530    }
    531    this._showedOptInOnTabs.add(tabId);
    532 
    533    const { bug, name } = this;
    534    const warning = `${name} is allowed on ${origin} for this browsing session due to user opt-in. See https://bugzilla.mozilla.org/show_bug.cgi?id=${bug} for details.`;
    535    return browser.tabs
    536      .executeScript(tabId, {
    537        code: `console.warn(${JSON.stringify(warning)})`,
    538        runAt: "document_start",
    539      })
    540      .catch(() => {});
    541  }
    542 
    543  async onUserOptOut(host, isPrivateMode) {
    544    const optIns = await this.getApplicableOptIns();
    545    const activeHostOptIns = isPrivateMode
    546      ? this._pBModeHostOptIns
    547      : this._hostOptIns;
    548    if (optIns.length) {
    549      activeHostOptIns.delete(host);
    550      await browser.trackingProtection.allow(
    551        this.id,
    552        optIns,
    553        isPrivateMode,
    554        Array.from(activeHostOptIns)
    555      );
    556      this.clearResourceCache();
    557    }
    558  }
    559 
    560  async clearUserOptIns(forPrivateMode) {
    561    const optIns = await this.getApplicableOptIns();
    562    const activeHostOptIns = forPrivateMode
    563      ? this._pBModeHostOptIns
    564      : this._hostOptIns;
    565    if (optIns.length) {
    566      activeHostOptIns.clear();
    567      await browser.trackingProtection.allow(
    568        this.id,
    569        optIns,
    570        forPrivateMode,
    571        Array.from(activeHostOptIns)
    572      );
    573      this.clearResourceCache();
    574    }
    575  }
    576 
    577  clearResourceCache() {
    578    return browser.trackingProtection.clearResourceCache();
    579  }
    580 }
    581 
    582 class Shims {
    583  constructor(availableShims) {
    584    this._originalShims = availableShims;
    585 
    586    if (!browser.trackingProtection) {
    587      console.error("Required experimental add-on APIs for shims unavailable");
    588      return;
    589    }
    590 
    591    this._readyPromise = new Promise(done => (this._resolveReady = done));
    592    this._registerShims(availableShims);
    593 
    594    onMessageFromTab(this._onMessageFromShim.bind(this));
    595 
    596    this.ENABLED_PREF = "enable_shims";
    597    browser.aboutConfigPrefs.onPrefChange.addListener(() => {
    598      this._checkEnabledPref();
    599    }, this.ENABLED_PREF);
    600 
    601    this.SMARTBLOCK_EMBEDS_ENABLED_PREF = `smartblockEmbeds.enabled`;
    602    browser.aboutConfigPrefs.onPrefChange.addListener(() => {
    603      this._checkSmartblockEmbedsEnabledPref();
    604    }, this.SMARTBLOCK_EMBEDS_ENABLED_PREF);
    605 
    606    // NOTE: Methods that uses the prefs should await
    607    //       _haveCheckedEnabledPrefsPromise, in order to make sure the
    608    //       prefs are all read.
    609    //       Methods that potentially clears the resource cache should check
    610    //       _haveCheckedEnabledPrefs, in order to avoid clearing the
    611    //       resource cache during the startup.
    612    this._haveCheckedEnabledPrefs = false;
    613    this._haveCheckedEnabledPrefsPromise = Promise.all([
    614      this._checkEnabledPref(),
    615      this._checkSmartblockEmbedsEnabledPref(),
    616    ]);
    617    this._haveCheckedEnabledPrefsPromise.then(() => {
    618      this._haveCheckedEnabledPrefs = true;
    619    });
    620 
    621    // handles unblock message coming in from protections panel
    622    browser.trackingProtection.onSmartBlockEmbedUnblock.addListener(
    623      async (tabId, shimId, hostname) => {
    624        const shim = this.shims.get(shimId);
    625        if (!shim) {
    626          console.warn("Smartblock shim not found", { tabId, shimId });
    627          return;
    628        }
    629        const isPB = (await browser.tabs.get(tabId)).incognito;
    630        await shim.onUserOptIn(hostname, isPB);
    631 
    632        // send request to shim to remove placeholders and replace with original embeds
    633        await browser.tabs.sendMessage(tabId, {
    634          shimId,
    635          topic: "smartblock:unblock-embed",
    636        });
    637      }
    638    );
    639 
    640    // handles reblock message coming in from protections panel
    641    browser.trackingProtection.onSmartBlockEmbedReblock.addListener(
    642      async (tabId, shimId, hostname) => {
    643        const shim = this.shims.get(shimId);
    644        if (!shim) {
    645          console.warn("Smartblock shim not found", { tabId, shimId });
    646          return;
    647        }
    648        const isPB = (await browser.tabs.get(tabId)).incognito;
    649        await shim.onUserOptOut(hostname, isPB);
    650 
    651        // a browser reload is required to reload the shim in the case where the shim gets unloaded
    652        // i.e. after user unblocks, then closes and revisits the page while shim is still allowed
    653        browser.tabs.reload(tabId);
    654      }
    655    );
    656 
    657    // handles data clearing on private browsing mode end
    658    browser.trackingProtection.onPrivateSessionEnd.addListener(() => {
    659      for (const shim of this.shims.values()) {
    660        shim.clearUserOptIns(true);
    661      }
    662    });
    663  }
    664 
    665  ready() {
    666    return this._readyPromise;
    667  }
    668 
    669  bindAboutCompatBroker(broker) {
    670    this._aboutCompatBroker = broker;
    671  }
    672 
    673  getShimInfoForAboutCompat(shim) {
    674    const { bug, disabledReason, hiddenInAboutCompat, id, name } = shim;
    675    const type = "smartblock";
    676    return { bug, disabledReason, hidden: hiddenInAboutCompat, id, name, type };
    677  }
    678 
    679  disableShimForSession(id) {
    680    const shim = this.shims.get(id);
    681    shim?.disableForSession();
    682  }
    683 
    684  enableShimForSession(id) {
    685    const shim = this.shims.get(id);
    686    shim?.enableForSession();
    687  }
    688 
    689  onShimStateChanged(id) {
    690    if (!this._aboutCompatBroker) {
    691      return;
    692    }
    693 
    694    const shim = this.shims.get(id);
    695    if (!shim) {
    696      return;
    697    }
    698 
    699    const shimsChanged = [this.getShimInfoForAboutCompat(shim)];
    700    this._aboutCompatBroker.portsToAboutCompatTabs.broadcast({ shimsChanged });
    701  }
    702 
    703  getAvailableShims() {
    704    const shims = Array.from(this.shims.values()).map(
    705      this.getShimInfoForAboutCompat
    706    );
    707    shims.sort((a, b) => a.name.localeCompare(b.name));
    708    return shims;
    709  }
    710 
    711  async onRemoteSettingsUpdate(updatedShims) {
    712    const oldReadyPromise = this._readyPromise;
    713    this._readyPromise = new Promise(done => (this._resolveReady = done));
    714    await oldReadyPromise;
    715    this._updateShims(updatedShims);
    716  }
    717 
    718  async _updateShims(updatedShims) {
    719    await this._unregisterShims();
    720    this._registerShims(updatedShims);
    721    this._checkEnabledPref();
    722    await this.ready();
    723  }
    724 
    725  async _resetToDefaultShims() {
    726    await this._updateShims(this._originalShims);
    727  }
    728 
    729  _registerShims(shims) {
    730    if (this.shims) {
    731      throw new Error("_registerShims has already been called");
    732    }
    733 
    734    this._registeredShimListeners = [];
    735    const registerShimListener = (api, listener, ...args) => {
    736      api.addListener(listener, ...args);
    737      this._registeredShimListeners.push([api, listener]);
    738    };
    739 
    740    this.shims = new Map();
    741    for (const shimOpts of shims) {
    742      const { id } = shimOpts;
    743      if (!this.shims.has(id)) {
    744        this.shims.set(shimOpts.id, new Shim(shimOpts, this));
    745      }
    746    }
    747 
    748    // Register onBeforeRequest listener which handles storage access requests
    749    // on matching redirects.
    750    let redirectTargetUrls = Array.from(shims.values())
    751      .filter(
    752        shim => !shim.isMissingFiles && shim.requestStorageAccessForRedirect
    753      )
    754      .flatMap(shim => shim.requestStorageAccessForRedirect)
    755      .map(([, dstUrl]) => dstUrl);
    756 
    757    // Unique target urls.
    758    redirectTargetUrls = Array.from(new Set(redirectTargetUrls));
    759 
    760    if (redirectTargetUrls.length) {
    761      debug("Registering redirect listener for requestStorageAccess helper", {
    762        redirectTargetUrls,
    763      });
    764      registerShimListener(
    765        browser.webRequest.onBeforeRequest,
    766        this._onRequestStorageAccessRedirect.bind(this),
    767        { urls: redirectTargetUrls, types: ["main_frame"] },
    768        ["blocking"]
    769      );
    770    }
    771 
    772    function addTypePatterns(type, patterns, set) {
    773      if (!set.has(type)) {
    774        set.set(type, { patterns: new Set() });
    775      }
    776      const allSet = set.get(type).patterns;
    777      for (const pattern of patterns) {
    778        allSet.add(pattern);
    779      }
    780    }
    781 
    782    const allMatchTypePatterns = new Map();
    783    const allHeaderChangingMatchTypePatterns = new Map();
    784    const allLogos = [];
    785    for (const shim of this.shims.values()) {
    786      if (shim.isMissingFiles) {
    787        continue;
    788      }
    789      const { logos, matches } = shim;
    790      allLogos.push(...logos);
    791      for (const { patterns, target, types } of matches || []) {
    792        for (const type of types) {
    793          if (shim.isGoogleTrendsDFPIFix) {
    794            addTypePatterns(type, patterns, allHeaderChangingMatchTypePatterns);
    795          }
    796          if (target || shim.file || shim.runFirst) {
    797            addTypePatterns(type, patterns, allMatchTypePatterns);
    798          }
    799        }
    800      }
    801    }
    802 
    803    if (allLogos.length) {
    804      const urls = Array.from(new Set(allLogos)).map(l => {
    805        return `${LogosBaseURL}${l}`;
    806      });
    807      debug("Allowing access to these logos:", urls);
    808      const unmarkShimsActive = tabId => {
    809        for (const shim of this.shims.values()) {
    810          shim.setActiveOnTab(tabId, false);
    811        }
    812      };
    813      registerShimListener(browser.tabs.onRemoved, unmarkShimsActive);
    814      registerShimListener(browser.tabs.onUpdated, (tabId, changeInfo) => {
    815        if (changeInfo.discarded || changeInfo.url) {
    816          unmarkShimsActive(tabId);
    817        }
    818      });
    819      registerShimListener(
    820        browser.webRequest.onBeforeRequest,
    821        this._redirectLogos.bind(this),
    822        { urls, types: ["image"] },
    823        ["blocking"]
    824      );
    825    }
    826 
    827    if (allHeaderChangingMatchTypePatterns) {
    828      for (const [
    829        type,
    830        { patterns },
    831      ] of allHeaderChangingMatchTypePatterns.entries()) {
    832        const urls = Array.from(patterns);
    833        debug("Shimming these", type, "URLs:", urls);
    834        registerShimListener(
    835          browser.webRequest.onBeforeSendHeaders,
    836          this._onBeforeSendHeaders.bind(this),
    837          { urls, types: [type] },
    838          ["blocking", "requestHeaders"]
    839        );
    840        registerShimListener(
    841          browser.webRequest.onHeadersReceived,
    842          this._onHeadersReceived.bind(this),
    843          { urls, types: [type] },
    844          ["blocking", "responseHeaders"]
    845        );
    846      }
    847    }
    848 
    849    if (!allMatchTypePatterns.size) {
    850      debug("Skipping shims; none enabled");
    851      return;
    852    }
    853 
    854    for (const [type, { patterns }] of allMatchTypePatterns.entries()) {
    855      const urls = Array.from(patterns);
    856      debug("Shimming these", type, "URLs:", urls);
    857 
    858      registerShimListener(
    859        browser.webRequest.onBeforeRequest,
    860        this._ensureShimForRequestOnTab.bind(this),
    861        { urls, types: [type] },
    862        ["blocking"]
    863      );
    864    }
    865  }
    866 
    867  _unregisterShims() {
    868    this.enabled = false;
    869    if (this._registeredShimListeners) {
    870      for (let [api, listener] of this._registeredShimListeners) {
    871        api.removeListener(listener);
    872      }
    873      this._registeredShimListeners = undefined;
    874    }
    875    this.shims = undefined;
    876  }
    877 
    878  async _checkEnabledPref() {
    879    const value = browser.aboutConfigPrefs.getPref(this.ENABLED_PREF);
    880    if (value === undefined) {
    881      await browser.aboutConfigPrefs.setPref(this.ENABLED_PREF, true);
    882    } else if (value === false) {
    883      this.enabled = false;
    884    } else {
    885      this.enabled = true;
    886    }
    887  }
    888 
    889  get enabled() {
    890    return this._enabled;
    891  }
    892 
    893  set enabled(enabled) {
    894    if (enabled === this._enabled) {
    895      return;
    896    }
    897 
    898    // resolveReady may change while we're updating
    899    const resolveReady = this._resolveReady;
    900    this._enabled = enabled;
    901 
    902    for (const shim of this.shims.values()) {
    903      if (enabled) {
    904        shim.onAllShimsEnabled();
    905      } else {
    906        shim.onAllShimsDisabled();
    907      }
    908    }
    909    resolveReady();
    910  }
    911 
    912  async _checkSmartblockEmbedsEnabledPref() {
    913    const value = browser.aboutConfigPrefs.getPref(
    914      this.SMARTBLOCK_EMBEDS_ENABLED_PREF
    915    );
    916    if (value === undefined) {
    917      await browser.aboutConfigPrefs.setPref(
    918        this.SMARTBLOCK_EMBEDS_ENABLED_PREF,
    919        true
    920      );
    921    } else if (value === false) {
    922      this.smartblockEmbedsEnabled = false;
    923    } else {
    924      this.smartblockEmbedsEnabled = true;
    925    }
    926  }
    927 
    928  get smartblockEmbedsEnabled() {
    929    return this._smartblockEmbedsEnabled;
    930  }
    931 
    932  set smartblockEmbedsEnabled(value) {
    933    if (value === this._smartblockEmbedsEnabled) {
    934      return;
    935    }
    936 
    937    this._smartblockEmbedsEnabled = value;
    938 
    939    for (const shim of this.shims.values()) {
    940      if (shim.isSmartblockEmbedShim) {
    941        shim.disabledBySmartblockEmbedPref = !this._smartblockEmbedsEnabled;
    942      }
    943    }
    944  }
    945 
    946  async _onRequestStorageAccessRedirect({
    947    originUrl: srcUrl,
    948    url: dstUrl,
    949    tabId,
    950  }) {
    951    debug("Detected redirect", { srcUrl, dstUrl, tabId });
    952 
    953    // Check if a shim needs to request storage access for this redirect. This
    954    // handler is called when the *source url* matches a shims redirect pattern,
    955    // but we still need to check if the *destination url* matches.
    956    const matchingShims = Array.from(this.shims.values()).filter(shim => {
    957      const { enabled, requestStorageAccessForRedirect } = shim;
    958 
    959      if (!enabled || !requestStorageAccessForRedirect) {
    960        return false;
    961      }
    962 
    963      return requestStorageAccessForRedirect.some(
    964        ([srcPattern, dstPattern]) =>
    965          browser.matchPatterns.getMatcher([srcPattern]).matches(srcUrl) &&
    966          browser.matchPatterns.getMatcher([dstPattern]).matches(dstUrl)
    967      );
    968    });
    969 
    970    // For each matching shim, find out if its enabled in regard to dFPI state.
    971    const bugNumbers = new Set();
    972    let isDFPIActive = null;
    973    await Promise.all(
    974      matchingShims.map(async shim => {
    975        if (shim.onlyIfDFPIActive) {
    976          // Only get the dFPI state for the first shim which requires it.
    977          if (isDFPIActive === null) {
    978            const tabIsPB = (await browser.tabs.get(tabId)).incognito;
    979            isDFPIActive =
    980              await browser.trackingProtection.isDFPIActive(tabIsPB);
    981          }
    982          if (!isDFPIActive) {
    983            return;
    984          }
    985        }
    986        bugNumbers.add(shim.bug);
    987      })
    988    );
    989 
    990    // If there is no shim which needs storage access for this redirect src/dst
    991    // pair, resume it.
    992    if (!bugNumbers.size) {
    993      return;
    994    }
    995 
    996    // Inject the helper to call requestStorageAccessForOrigin on the document.
    997    await browser.tabs.executeScript(tabId, {
    998      file: "/lib/requestStorageAccess_helper.js",
    999      runAt: "document_start",
   1000    });
   1001 
   1002    const bugUrls = Array.from(bugNumbers)
   1003      .map(bugNo => `https://bugzilla.mozilla.org/show_bug.cgi?id=${bugNo}`)
   1004      .join(", ");
   1005    const warning = `Firefox calls the Storage Access API for ${dstUrl} on behalf of ${srcUrl}. See the following bugs for details: ${bugUrls}`;
   1006 
   1007    // Request storage access for the origin of the destination url of the
   1008    // redirect.
   1009    const { origin: requestStorageAccessOrigin } = new URL(dstUrl);
   1010 
   1011    // Wait for the requestStorageAccess request to finish before resuming the
   1012    // redirect.
   1013    const { success } = await browser.tabs.sendMessage(tabId, {
   1014      requestStorageAccessOrigin,
   1015      warning,
   1016    });
   1017    debug("requestStorageAccess callback", {
   1018      success,
   1019      requestStorageAccessOrigin,
   1020      srcUrl,
   1021      dstUrl,
   1022      bugNumbers,
   1023    });
   1024  }
   1025 
   1026  async _onMessageFromShim(payload, sender) {
   1027    const { tab, frameId } = sender;
   1028    const { id, url } = tab;
   1029    const { shimId, message } = payload;
   1030 
   1031    // Ignore unknown messages (for instance, from about:compat).
   1032    if (
   1033      message !== "getOptions" &&
   1034      message !== "optIn" &&
   1035      message !== "embedClicked" &&
   1036      message !== "smartblockEmbedReplaced" &&
   1037      message !== "smartblockGetFluentString" &&
   1038      message !== "checkFacebookLoginStatus"
   1039    ) {
   1040      return undefined;
   1041    }
   1042 
   1043    if (sender.id !== browser.runtime.id || id === -1) {
   1044      throw new Error("not allowed");
   1045    }
   1046 
   1047    // Important! It is entirely possible for sites to spoof
   1048    // these messages, due to shims allowing web pages to
   1049    // communicate with the extension.
   1050 
   1051    const shim = this.shims.get(shimId);
   1052    if (!shim?.needsShimHelpers?.includes(message)) {
   1053      throw new Error("not allowed");
   1054    }
   1055 
   1056    if (message === "getOptions") {
   1057      return Object.assign(
   1058        {
   1059          platform: await platformPromise,
   1060          releaseBranch: await releaseBranchPromise,
   1061        },
   1062        shim.options
   1063      );
   1064    } else if (message === "optIn") {
   1065      try {
   1066        await shim.onUserOptIn(new URL(url).hostname, tab.incognito);
   1067        const origin = new URL(tab.url).origin;
   1068        warn(
   1069          "** User opted in for",
   1070          shim.name,
   1071          "shim on",
   1072          origin,
   1073          "on tab",
   1074          id,
   1075          "frame",
   1076          frameId
   1077        );
   1078        await shim.showOptInWarningOnce(id, origin);
   1079      } catch (err) {
   1080        console.error(err);
   1081        throw new Error("error");
   1082      }
   1083    } else if (message === "embedClicked") {
   1084      browser.trackingProtection.openProtectionsPanel(id);
   1085    } else if (message === "smartblockEmbedReplaced") {
   1086      browser.trackingProtection.incrementSmartblockEmbedShownTelemetry();
   1087    } else if (message === "smartblockGetFluentString") {
   1088      return await browser.trackingProtection.getSmartBlockEmbedFluentString(
   1089        id,
   1090        shimId,
   1091        new URL(url).hostname
   1092      );
   1093    } else if (message === "checkFacebookLoginStatus") {
   1094      // Verify that the user is logged in to Facebook by checking the c_user
   1095      // cookie.
   1096      let cookie = await browser.cookies.get({
   1097        url: "https://www.facebook.com",
   1098        name: "c_user",
   1099      });
   1100 
   1101      // If the cookie is found, the user is logged in to Facebook.
   1102      return cookie != null;
   1103    }
   1104 
   1105    return undefined;
   1106  }
   1107 
   1108  async _redirectLogos(details) {
   1109    await this._haveCheckedEnabledPrefsPromise;
   1110 
   1111    if (!this.enabled) {
   1112      return { cancel: true };
   1113    }
   1114 
   1115    const { tabId, url } = details;
   1116    const logo = new URL(url).pathname.slice(1);
   1117 
   1118    for (const shim of this.shims.values()) {
   1119      await shim.ready;
   1120 
   1121      if (!shim.enabled) {
   1122        continue;
   1123      }
   1124 
   1125      if (shim.onlyIfDFPIActive) {
   1126        const isPB = (await browser.tabs.get(details.tabId)).incognito;
   1127        if (!(await browser.trackingProtection.isDFPIActive(isPB))) {
   1128          continue;
   1129        }
   1130      }
   1131 
   1132      if (!shim.logos.includes(logo)) {
   1133        continue;
   1134      }
   1135 
   1136      if (shim.isActiveOnTab(tabId)) {
   1137        return { redirectUrl: browser.runtime.getURL(`shims/${logo}`) };
   1138      }
   1139    }
   1140 
   1141    return { cancel: true };
   1142  }
   1143 
   1144  async _onHeadersReceived(details) {
   1145    await this._haveCheckedEnabledPrefsPromise;
   1146 
   1147    for (const shim of this.shims.values()) {
   1148      await shim.ready;
   1149 
   1150      if (!shim.enabled) {
   1151        continue;
   1152      }
   1153 
   1154      if (shim.onlyIfDFPIActive) {
   1155        const isPB = (await browser.tabs.get(details.tabId)).incognito;
   1156        if (!(await browser.trackingProtection.isDFPIActive(isPB))) {
   1157          continue;
   1158        }
   1159      }
   1160 
   1161      if (shim.isGoogleTrendsDFPIFix) {
   1162        if (shim.GoogleNidCookieToUse) {
   1163          continue;
   1164        }
   1165 
   1166        for (const header of details.responseHeaders) {
   1167          if (header.name == "set-cookie") {
   1168            shim.GoogleNidCookieToUse = header.value;
   1169            return { redirectUrl: details.url };
   1170          }
   1171        }
   1172      }
   1173    }
   1174 
   1175    return undefined;
   1176  }
   1177 
   1178  async _onBeforeSendHeaders(details) {
   1179    await this._haveCheckedEnabledPrefsPromise;
   1180 
   1181    const { frameId, requestHeaders, tabId } = details;
   1182 
   1183    if (!this.enabled) {
   1184      return { requestHeaders };
   1185    }
   1186 
   1187    for (const shim of this.shims.values()) {
   1188      await shim.ready;
   1189 
   1190      if (!shim.enabled) {
   1191        continue;
   1192      }
   1193 
   1194      if (shim.isGoogleTrendsDFPIFix) {
   1195        const value = shim.GoogleNidCookieToUse;
   1196 
   1197        if (!value) {
   1198          continue;
   1199        }
   1200 
   1201        let found;
   1202        for (let header of requestHeaders) {
   1203          if (header.name.toLowerCase() === "cookie") {
   1204            header.value = value;
   1205            found = true;
   1206          }
   1207        }
   1208        if (!found) {
   1209          requestHeaders.push({ name: "Cookie", value });
   1210        }
   1211 
   1212        browser.tabs
   1213          .get(tabId)
   1214          .then(({ url }) => {
   1215            debug(
   1216              `Google Trends dFPI fix used on tab ${tabId} frame ${frameId} (${url})`
   1217            );
   1218          })
   1219          .catch(() => {});
   1220 
   1221        const warning = `Working around Google Trends tracking protection breakage. See https://bugzilla.mozilla.org/show_bug.cgi?id=${shim.bug} for details.`;
   1222        browser.tabs
   1223          .executeScript(tabId, {
   1224            code: `console.warn(${JSON.stringify(warning)})`,
   1225            runAt: "document_start",
   1226          })
   1227          .catch(() => {});
   1228      }
   1229    }
   1230 
   1231    return { requestHeaders };
   1232  }
   1233 
   1234  // eslint-disable-next-line complexity
   1235  async _ensureShimForRequestOnTab(details) {
   1236    await this._haveCheckedEnabledPrefsPromise;
   1237 
   1238    if (!this.enabled) {
   1239      return undefined;
   1240    }
   1241 
   1242    // We only ever reach this point if a request is for a URL which ought to
   1243    // be shimmed. We never get here if a request is blocked, and we only
   1244    // unblock requests if at least one shim matches it.
   1245 
   1246    const { frameId, originUrl, requestId, tabId, type, url } = details;
   1247 
   1248    // Ignore requests unrelated to tabs
   1249    if (tabId < 0) {
   1250      return undefined;
   1251    }
   1252 
   1253    // We need to base our checks not on the frame's host, but the tab's.
   1254    const topHost = new URL((await browser.tabs.get(tabId)).url).hostname;
   1255    const isPB = (await browser.tabs.get(details.tabId)).incognito;
   1256    const unblocked = await browser.trackingProtection.wasRequestUnblocked(
   1257      requestId,
   1258      isPB
   1259    );
   1260 
   1261    let match;
   1262    let shimToApply;
   1263    for (const shim of this.shims.values()) {
   1264      await shim.ready;
   1265 
   1266      if (!shim.enabled || (!shim.redirectsRequests && !shim.runFirst)) {
   1267        continue;
   1268      }
   1269 
   1270      if (shim.onlyIfDFPIActive || shim.onlyIfPrivateBrowsing) {
   1271        if (!isPB && shim.onlyIfPrivateBrowsing) {
   1272          continue;
   1273        }
   1274        if (
   1275          shim.onlyIfDFPIActive &&
   1276          !(await browser.trackingProtection.isDFPIActive(isPB))
   1277        ) {
   1278          continue;
   1279        }
   1280      }
   1281 
   1282      // Do not apply the shim if it is only meant to apply when strict mode ETP
   1283      // (content blocking) was going to block the request.
   1284      if (!unblocked && shim.onlyIfBlockedByETP) {
   1285        continue;
   1286      }
   1287 
   1288      if (!shim.meantForHost(topHost)) {
   1289        continue;
   1290      }
   1291 
   1292      // If this URL and content type isn't meant for this shim, don't apply it.
   1293      match = shim.isTriggeredByURLAndType(url, type);
   1294      if (match) {
   1295        if (!unblocked && match.onlyIfBlockedByETP) {
   1296          continue;
   1297        }
   1298 
   1299        // If the user has already opted in for this shim, all requests it covers
   1300        // should be allowed; no need for a shim anymore.
   1301        if (shim.hasUserOptedInAlready(topHost, isPB)) {
   1302          warn(
   1303            `Allowing tracking ${type} ${url} on tab ${tabId} frame ${frameId} due to opt-in`
   1304          );
   1305          shim.showOptInWarningOnce(tabId, new URL(originUrl).origin);
   1306          return undefined;
   1307        }
   1308        shimToApply = shim;
   1309        break;
   1310      }
   1311    }
   1312 
   1313    let runFirst = false;
   1314 
   1315    if (shimToApply) {
   1316      // Note that sites may request the same shim twice, but because the requests
   1317      // may differ enough for some to fail (CSP/CORS/etc), we always let the request
   1318      // complete via local redirect. Shims should gracefully handle this as well.
   1319 
   1320      const { target } = match;
   1321      const { bug, file, id, name } = shimToApply;
   1322 
   1323      // Determine whether we should inject helper scripts into the page.
   1324      // webExposedShimHelpers is an optional list of helpers to provide
   1325      // directly to the website (see script injection below). If not used shims
   1326      // should pass an empty array to disable this functionality.
   1327      const needsShimHelpers =
   1328        shimToApply.webExposedShimHelpers || shimToApply.needsShimHelpers;
   1329 
   1330      runFirst = shimToApply.runFirst;
   1331 
   1332      const redirect = target || file;
   1333 
   1334      warn(
   1335        `Shimming tracking ${type} ${url} on tab ${tabId} frame ${frameId} with ${
   1336          redirect || runFirst
   1337        }`
   1338      );
   1339 
   1340      const warning = `${name} is being shimmed by Firefox. See https://bugzilla.mozilla.org/show_bug.cgi?id=${bug} for details.`;
   1341 
   1342      let needConsoleMessage = true;
   1343 
   1344      if (shimToApply.isSmartblockEmbedShim) {
   1345        try {
   1346          await browser.tabs.executeScript(tabId, {
   1347            file: `/lib/smartblock_embeds_helper.js`,
   1348            frameId,
   1349            runAt: "document_start",
   1350          });
   1351        } catch (_) {}
   1352      }
   1353 
   1354      if (runFirst) {
   1355        try {
   1356          await browser.tabs.executeScript(tabId, {
   1357            file: `/shims/${runFirst}`,
   1358            frameId,
   1359            runAt: "document_start",
   1360          });
   1361          shimToApply.setActiveOnTab(tabId);
   1362        } catch (_) {}
   1363      }
   1364 
   1365      // For scripts, we also set up any needed shim helpers.
   1366      if (type === "script" && needsShimHelpers?.length) {
   1367        try {
   1368          await browser.tabs.executeScript(tabId, {
   1369            file: "/lib/shim_messaging_helper.js",
   1370            frameId,
   1371            runAt: "document_start",
   1372          });
   1373          const origin = new URL(originUrl).origin;
   1374          await browser.tabs.sendMessage(
   1375            tabId,
   1376            { origin, shimId: id, needsShimHelpers, warning },
   1377            { frameId }
   1378          );
   1379          needConsoleMessage = false;
   1380          shimToApply.setActiveOnTab(tabId);
   1381        } catch (_) {}
   1382      }
   1383 
   1384      if (needConsoleMessage) {
   1385        try {
   1386          await browser.tabs.executeScript(tabId, {
   1387            code: `console.warn(${JSON.stringify(warning)})`,
   1388            runAt: "document_start",
   1389          });
   1390        } catch (_) {}
   1391      }
   1392 
   1393      if (!redirect.indexOf("http://") || !redirect.indexOf("https://")) {
   1394        return { redirectUrl: redirect };
   1395      }
   1396 
   1397      // If any shims matched the request to replace it, then redirect to the local
   1398      // file bundled with SmartBlock, so the request never hits the network.
   1399      return { redirectUrl: browser.runtime.getURL(`shims/${redirect}`) };
   1400    }
   1401 
   1402    // Sanity check: if no shims end up handling this request,
   1403    // yet it was meant to be blocked by ETP, then block it now.
   1404    if (unblocked) {
   1405      error(`unexpected: ${url} not shimmed on tab ${tabId} frame ${frameId}`);
   1406      return { cancel: true };
   1407    }
   1408 
   1409    if (!runFirst) {
   1410      debug(`ignoring ${url} on tab ${tabId} frame ${frameId}`);
   1411    }
   1412    return undefined;
   1413  }
   1414 }
   1415 
   1416 if (typeof module !== "undefined") {
   1417  module.exports = Shims;
   1418 }