tor-browser

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

bg.js (11181B)


      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 /* global browser, ConditionFactory */
      6 
      7 /**
      8 * The main class for the IPP activator add-on.
      9 */
     10 class IPPAddonActivator {
     11  #initialized = false;
     12 
     13  #tabBaseBreakages;
     14  #webrequestBaseBreakages;
     15 
     16  #tabBreakages;
     17  #webrequestBreakages;
     18 
     19  #pendingTabs = new Set(); // pending due to tab URL change while inactive
     20  #pendingWebRequests = new Map(); // tabId -> Set of pending request URLs
     21  #shownDomainByTab = new Map(); // tabId -> baseDomain of currently shown notification
     22 
     23  constructor() {
     24    this.tabUpdated = this.#tabUpdated.bind(this);
     25    this.tabActivated = this.#tabActivated.bind(this);
     26    this.tabRemoved = this.#tabRemoved.bind(this);
     27    this.onRequest = this.#onRequest.bind(this);
     28 
     29    browser.ippActivator.isTesting().then(async isTesting => {
     30      await this.#loadAndRebuildBreakages();
     31      browser.ippActivator.onDynamicTabBreakagesUpdated.addListener(() =>
     32        this.#loadAndRebuildBreakages()
     33      );
     34      browser.ippActivator.onDynamicWebRequestBreakagesUpdated.addListener(() =>
     35        this.#loadAndRebuildBreakages()
     36      );
     37 
     38      if (isTesting) {
     39        this.#init();
     40        return;
     41      }
     42 
     43      // Initialize only when IPP is active, keep in sync with activation.
     44      if (await browser.ippActivator.isIPPActive()) {
     45        this.#init();
     46      }
     47 
     48      // IPP start event: initialize when service starts.
     49      browser.ippActivator.onIPPActivated.addListener(async () => {
     50        if (await browser.ippActivator.isIPPActive()) {
     51          this.#init();
     52        } else {
     53          this.#uninit();
     54        }
     55      });
     56    });
     57  }
     58 
     59  async #init() {
     60    if (this.#initialized) {
     61      return;
     62    }
     63 
     64    // Register only the listeners that are needed for existing breakages.
     65    this.#registerListeners();
     66 
     67    this.#initialized = true;
     68  }
     69 
     70  async #uninit() {
     71    if (!this.#initialized) {
     72      return;
     73    }
     74 
     75    this.#unregisterListeners();
     76 
     77    // When IPP is deactivated, mark currently shown banners as consumed
     78    const uniqueDomains = new Set(this.#shownDomainByTab.values());
     79    await Promise.allSettled(
     80      Array.from(uniqueDomains).map(d =>
     81        browser.ippActivator.addNotifiedDomain(d)
     82      )
     83    );
     84 
     85    const ids = Array.from(this.#shownDomainByTab.keys());
     86    await Promise.allSettled(
     87      ids.map(id => browser.ippActivator.hideMessage(id))
     88    );
     89 
     90    this.#shownDomainByTab.clear();
     91 
     92    this.#initialized = false;
     93  }
     94 
     95  async #loadAndRebuildBreakages() {
     96    if (!this.#tabBaseBreakages) {
     97      try {
     98        const url = browser.runtime.getURL("breakages/tab.json");
     99        const res = await fetch(url);
    100        const base = await res.json();
    101        this.#tabBaseBreakages = Array.isArray(base) ? base : [];
    102      } catch (e) {
    103        this.#tabBaseBreakages = [];
    104      }
    105    }
    106 
    107    if (!this.#webrequestBaseBreakages) {
    108      try {
    109        const url = browser.runtime.getURL("breakages/webrequest.json");
    110        const res = await fetch(url);
    111        const base = await res.json();
    112        this.#webrequestBaseBreakages = Array.isArray(base) ? base : [];
    113      } catch (e) {
    114        this.#webrequestBaseBreakages = [];
    115      }
    116    }
    117 
    118    let dynamicTab = [];
    119    try {
    120      const dynT = await browser.ippActivator.getDynamicTabBreakages();
    121      dynamicTab = Array.isArray(dynT) ? dynT : [];
    122    } catch (_) {
    123      console.warn("Unable to retrieve dynamicTabBreakages");
    124    }
    125 
    126    let dynamicWr = [];
    127    try {
    128      const dynW = await browser.ippActivator.getDynamicWebRequestBreakages();
    129      dynamicWr = Array.isArray(dynW) ? dynW : [];
    130    } catch (_) {
    131      console.warn("Unable to retrieve dynamicWebRequestBreakages");
    132    }
    133 
    134    this.#tabBreakages = [...(this.#tabBaseBreakages || []), ...dynamicTab];
    135    this.#webrequestBreakages = [
    136      ...(this.#webrequestBaseBreakages || []),
    137      ...dynamicWr,
    138    ];
    139 
    140    // Adjust listeners if we've already initialized.
    141    if (this.#initialized) {
    142      this.#registerListeners();
    143    }
    144  }
    145 
    146  #registerListeners() {
    147    this.#unregisterListeners();
    148 
    149    const needTabUpdated =
    150      Array.isArray(this.#tabBreakages) && !!this.#tabBreakages.length;
    151    const needWebRequest =
    152      Array.isArray(this.#webrequestBreakages) &&
    153      !!this.#webrequestBreakages.length;
    154    const needActivation = needTabUpdated || needWebRequest;
    155 
    156    // tabs.onUpdated (only if there are tab breakages)
    157    if (needTabUpdated) {
    158      browser.tabs.onUpdated.addListener(this.tabUpdated, {
    159        properties: ["url", "status"],
    160      });
    161    }
    162 
    163    // webRequest.onBeforeRequest (only if there are webRequest breakages)
    164    if (needWebRequest) {
    165      browser.webRequest.onBeforeRequest.addListener(
    166        this.onRequest,
    167        {
    168          urls: ["<all_urls>"],
    169          types: ["media", "sub_frame", "xmlhttprequest"],
    170        },
    171        []
    172      );
    173    }
    174 
    175    // tabs.onActivated and tabs.onRemoved are needed when either above is needed
    176    if (needActivation) {
    177      browser.tabs.onActivated.addListener(this.tabActivated);
    178      browser.tabs.onRemoved.addListener(this.tabRemoved);
    179    }
    180  }
    181 
    182  #unregisterListeners() {
    183    if (browser.tabs.onUpdated.hasListener(this.tabUpdated)) {
    184      browser.tabs.onUpdated.removeListener(this.tabUpdated);
    185    }
    186 
    187    if (browser.tabs.onActivated.hasListener(this.tabActivated)) {
    188      browser.tabs.onActivated.removeListener(this.tabActivated);
    189    }
    190 
    191    if (browser.tabs.onRemoved.hasListener(this.tabRemoved)) {
    192      browser.tabs.onRemoved.removeListener(this.tabRemoved);
    193    }
    194 
    195    if (browser.webRequest.onBeforeRequest.hasListener(this.onRequest)) {
    196      browser.webRequest.onBeforeRequest.removeListener(this.onRequest);
    197    }
    198 
    199    this.#pendingTabs.clear();
    200    this.#pendingWebRequests.clear();
    201  }
    202 
    203  async #tabUpdated(tabId, changeInfo, tab) {
    204    // React only to URL changes and to load completion; avoid showing during 'loading'
    205    if (!("url" in changeInfo) && changeInfo.status !== "complete") {
    206      return;
    207    }
    208 
    209    // If the tab URL changed, reset any pending web requests for this tab
    210    if ("url" in changeInfo) {
    211      try {
    212        // If we had a notification for a different base domain, hide it
    213        const info = await browser.ippActivator.getBaseDomainFromURL(
    214          changeInfo.url || tab?.url || ""
    215        );
    216        const shownBase = this.#shownDomainByTab.get(tabId);
    217        if (
    218          shownBase &&
    219          shownBase !== info.baseDomain &&
    220          shownBase !== info.host
    221        ) {
    222          await browser.ippActivator.hideMessage(tabId);
    223          this.#shownDomainByTab.delete(tabId);
    224        }
    225      } catch (_) {
    226        // ignore lookup issues
    227      }
    228      this.#pendingWebRequests.delete(tabId);
    229    }
    230 
    231    // If we haven't reached load completion yet, wait for later events
    232    if (changeInfo.status && changeInfo.status !== "complete") {
    233      if (!tab.active) {
    234        this.#pendingTabs.add(tabId);
    235      }
    236      return;
    237    }
    238 
    239    // At this point, either the URL changed and load already completed, or
    240    // we received the 'complete' status: handle only if tab is active
    241    if (!tab.active) {
    242      this.#pendingTabs.add(tabId);
    243      return;
    244    }
    245 
    246    await this.#maybeNotify(tab, this.#tabBreakages, tab.url);
    247  }
    248 
    249  async #tabActivated(activeInfo) {
    250    const { tabId } = activeInfo || {};
    251 
    252    const hadTabPending = this.#pendingTabs.has(tabId);
    253    const wrSet = this.#pendingWebRequests.get(tabId);
    254    const pendingWrUrls = wrSet ? Array.from(wrSet) : [];
    255    if (!hadTabPending && pendingWrUrls.length === 0) {
    256      return;
    257    }
    258 
    259    this.#pendingTabs.delete(tabId);
    260    this.#pendingWebRequests.delete(tabId);
    261 
    262    let tab;
    263    try {
    264      tab = await browser.tabs.get(tabId);
    265      if (!tab || !tab.active) {
    266        return;
    267      }
    268    } catch (_) {
    269      // Tab might have been closed; ignore.
    270      return;
    271    }
    272 
    273    if (
    274      hadTabPending &&
    275      (await this.#maybeNotify(tab, this.#tabBreakages, tab.url))
    276    ) {
    277      return;
    278    }
    279 
    280    for (const url of pendingWrUrls) {
    281      if (await this.#maybeNotify(tab, this.#webrequestBreakages, url)) {
    282        return;
    283      }
    284    }
    285  }
    286 
    287  async #maybeNotify(tab, breakages, url) {
    288    const info = await browser.ippActivator.getBaseDomainFromURL(url);
    289    if (!info.baseDomain && !info.host) {
    290      return false;
    291    }
    292 
    293    if (await browser.ippActivator.hasExclusion(url)) {
    294      return false;
    295    }
    296 
    297    let domain = info.baseDomain;
    298    let breakage = breakages.find(
    299      b => Array.isArray(b.domains) && b.domains.includes(info.baseDomain)
    300    );
    301    if (!breakage) {
    302      breakage = breakages.find(
    303        b => Array.isArray(b.domains) && b.domains.includes(info.host)
    304      );
    305      if (!breakage) {
    306        return false;
    307      }
    308 
    309      domain = info.host;
    310    }
    311 
    312    // Do not show the same notification again for the same base domain.
    313    const shown = await browser.ippActivator.getNotifiedDomains();
    314    if (Array.isArray(shown) && shown.includes(domain)) {
    315      return false;
    316    }
    317 
    318    if (
    319      !(await ConditionFactory.run(breakage.condition, { tabId: tab.id, url }))
    320    ) {
    321      return false;
    322    }
    323 
    324    // Track which base domain this tab is showing a notification for
    325    this.#shownDomainByTab.set(tab.id, domain);
    326 
    327    // This function returns when the notification is dismissed. We don't want
    328    // to wait for that to happen.
    329    browser.ippActivator
    330      .showMessage(breakage.message, tab.id)
    331      .then(async dismissed => {
    332        if (!dismissed) {
    333          return;
    334        }
    335 
    336        await browser.ippActivator.addNotifiedDomain(domain);
    337 
    338        // Close all notifications currently shown for the same base domain
    339        // across all tabs and clean up tracking state.
    340        const toClose = [];
    341        for (const [tid, base] of this.#shownDomainByTab.entries()) {
    342          if (base === domain) {
    343            toClose.push(tid);
    344          }
    345        }
    346 
    347        await Promise.allSettled(
    348          toClose.map(id => browser.ippActivator.hideMessage(id))
    349        );
    350 
    351        for (const id of toClose) {
    352          this.#shownDomainByTab.delete(id);
    353        }
    354      });
    355 
    356    return true;
    357  }
    358 
    359  async #onRequest(details) {
    360    if (
    361      typeof details.tabId !== "number" ||
    362      details.tabId < 0 ||
    363      !details.url
    364    ) {
    365      return;
    366    }
    367 
    368    try {
    369      const tab = await browser.tabs.get(details.tabId);
    370      if (!tab) {
    371        return;
    372      }
    373 
    374      if (tab.active) {
    375        await this.#maybeNotify(tab, this.#webrequestBreakages, details.url);
    376      } else {
    377        const set = this.#pendingWebRequests.get(details.tabId) || new Set();
    378        set.add(details.url);
    379 
    380        this.#pendingWebRequests.set(details.tabId, set);
    381      }
    382    } catch (_) {
    383      // tab may not exist
    384    }
    385  }
    386 
    387  async #tabRemoved(tabId, _removeInfo) {
    388    // Clean up any pending state associated with the closed tab
    389    this.#pendingTabs.delete(tabId);
    390    this.#pendingWebRequests.delete(tabId);
    391    this.#shownDomainByTab.delete(tabId);
    392  }
    393 }
    394 
    395 /* This object is kept alive by listeners */
    396 new IPPAddonActivator();