tor-browser

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

DownloadSpamProtection.sys.mjs (11238B)


      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 /**
      6 * Provides functions to prevent multiple automatic downloads.
      7 */
      8 
      9 import {
     10  Download,
     11  DownloadError,
     12 } from "resource://gre/modules/DownloadCore.sys.mjs";
     13 
     14 const lazy = {};
     15 
     16 ChromeUtils.defineESModuleGetters(lazy, {
     17  BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
     18  DownloadList: "resource://gre/modules/DownloadList.sys.mjs",
     19  Downloads: "resource://gre/modules/Downloads.sys.mjs",
     20  DownloadsCommon:
     21    "moz-src:///browser/components/downloads/DownloadsCommon.sys.mjs",
     22 });
     23 
     24 /**
     25 * Each window tracks download spam independently, so one of these objects is
     26 * constructed for each window. This is responsible for tracking the spam and
     27 * updating the window's downloads UI accordingly.
     28 */
     29 class WindowSpamProtection {
     30  constructor(window) {
     31    this._window = window;
     32  }
     33 
     34  /**
     35   * This map stores blocked spam downloads for the window, keyed by the
     36   * download's source URL. This is done so we can track the number of times a
     37   * given download has been blocked.
     38   *
     39   * @type {Map<string, DownloadSpam>}
     40   */
     41  _downloadSpamForUrl = new Map();
     42 
     43  /**
     44   * This set stores views that are waiting to have download notification
     45   * listeners attached. They will be attached when the spamList is created
     46   * (i.e. when the first spam download is blocked).
     47   *
     48   * @type {Set<object>}
     49   */
     50  _pendingViews = new Set();
     51 
     52  /**
     53   * Set to true when we first start _blocking downloads in the window. This is
     54   * used to lazily load the spamList. Spam downloads are rare enough that many
     55   * sessions will have no blocked downloads. So we don't want to create a
     56   * DownloadList unless we actually need it.
     57   *
     58   * @type {boolean}
     59   */
     60  _blocking = false;
     61 
     62  /**
     63   * A per-window DownloadList for blocked spam downloads. Registered views will
     64   * be sent notifications about downloads in this list, so that blocked spam
     65   * downloads can be represented in the UI. If spam downloads haven't been
     66   * blocked in the window, this will be undefined. See DownloadList.sys.mjs.
     67   *
     68   * @type {DownloadList | undefined}
     69   */
     70  get spamList() {
     71    if (!this._blocking) {
     72      return undefined;
     73    }
     74    if (!this._spamList) {
     75      this._spamList = new lazy.DownloadList();
     76    }
     77    return this._spamList;
     78  }
     79 
     80  /**
     81   * A per-window downloads indicator whose state depends on notifications from
     82   * DownloadLists registered in the window (for example, the visual state of
     83   * the downloads toolbar button). See DownloadsCommon.sys.mjs for more details.
     84   *
     85   * @type {DownloadsIndicatorData}
     86   */
     87  get indicator() {
     88    if (!this._indicator) {
     89      this._indicator = lazy.DownloadsCommon.getIndicatorData(this._window);
     90    }
     91    return this._indicator;
     92  }
     93 
     94  /**
     95   * Add a blocked download to the spamList or increment the count of an
     96   * existing blocked download, then notify listeners about this.
     97   *
     98   * @param {string} url
     99   * @param {DownloadSpamEnabler} enabler
    100   */
    101  addDownloadSpam(url, enabler) {
    102    this._blocking = true;
    103    // Start listening on registered downloads views, if any exist.
    104    this._maybeAddViews();
    105    // If this URL is already paired with a DownloadSpam object, increment its
    106    // blocked downloads count by 1 and don't open the downloads panel.
    107    if (this._downloadSpamForUrl.has(url)) {
    108      let downloadSpam = this._downloadSpamForUrl.get(url);
    109      downloadSpam.blockedDownloadsCount += 1;
    110      this.indicator.onDownloadStateChanged(downloadSpam);
    111      return;
    112    }
    113    // Otherwise, create a new DownloadSpam object for the URL, add it to the
    114    // spamList, and open the downloads panel.
    115    let downloadSpam = new DownloadSpam(url, enabler);
    116    this.spamList.add(downloadSpam);
    117    this._downloadSpamForUrl.set(url, downloadSpam);
    118    this._notifyDownloadSpamAdded(downloadSpam);
    119  }
    120 
    121  /**
    122   * Notify the downloads panel that a new download has been added to the
    123   * spamList. This is invoked when a new DownloadSpam object is created.
    124   *
    125   * @param {DownloadSpam} downloadSpam
    126   */
    127  _notifyDownloadSpamAdded(downloadSpam) {
    128    let hasActiveDownloads = lazy.DownloadsCommon.summarizeDownloads(
    129      this.indicator._activeDownloads()
    130    ).numDownloading;
    131    if (
    132      !hasActiveDownloads &&
    133      this._window === lazy.BrowserWindowTracker.getTopWindow()
    134    ) {
    135      // If there are no active downloads, open the downloads panel.
    136      this._window.DownloadsPanel.showPanel();
    137    } else {
    138      // Otherwise, flash a taskbar/dock icon notification if available.
    139      this._window.getAttention();
    140    }
    141    this.indicator.onDownloadAdded(downloadSpam);
    142  }
    143 
    144  /**
    145   * Remove the download spam data for a given source URL.
    146   *
    147   * @param {string} url
    148   */
    149  removeDownloadSpamForUrl(url) {
    150    if (this._downloadSpamForUrl.has(url)) {
    151      let downloadSpam = this._downloadSpamForUrl.get(url);
    152      this.spamList.remove(downloadSpam);
    153      this.indicator.onDownloadRemoved(downloadSpam);
    154      this._downloadSpamForUrl.delete(url);
    155    }
    156  }
    157 
    158  /**
    159   * Set up a downloads view (e.g. the downloads panel) to receive notifications
    160   * about downloads in the spamList.
    161   *
    162   * @param {object} view An object that implements handlers for download
    163   *                      related notifications, like onDownloadAdded.
    164   */
    165  registerView(view) {
    166    if (!view || this.spamList?._views.has(view)) {
    167      return;
    168    }
    169    this._pendingViews.add(view);
    170    this._maybeAddViews();
    171  }
    172 
    173  /**
    174   * If any downloads have been blocked in the window, add download notification
    175   * listeners for each downloads view that has been registered.
    176   */
    177  _maybeAddViews() {
    178    if (this.spamList) {
    179      for (let view of this._pendingViews) {
    180        if (!this.spamList._views.has(view)) {
    181          this.spamList.addView(view);
    182        }
    183      }
    184      this._pendingViews.clear();
    185    }
    186  }
    187 
    188  /**
    189   * Remove download notification listeners for all views. This is invoked when
    190   * the window is closed.
    191   */
    192  removeAllViews() {
    193    if (this.spamList) {
    194      for (let view of this.spamList._views) {
    195        this.spamList.removeView(view);
    196      }
    197    }
    198    this._pendingViews.clear();
    199  }
    200 }
    201 
    202 /**
    203 * Helper to grant a certain principal permission for automatic downloads
    204 * and to clear its download spam messages from the UI
    205 */
    206 class DownloadSpamEnabler {
    207  /**
    208   * Constructs a DownloadSpamEnabler object
    209   *
    210   * @param {nsIPrincipal} principal
    211   * @param {DownloadSpamProtection} downloadSpamProtection
    212   */
    213  constructor(principal, downloadSpamProtection) {
    214    this.principal = principal;
    215    this.downloadSpamProtection = downloadSpamProtection;
    216  }
    217  /**
    218   * Allows a DownloadSpam item
    219   *
    220   * @param {DownloadSpam} downloadSpam
    221   */
    222  allow(downloadSpam) {
    223    const pm = Services.perms;
    224    pm.addFromPrincipal(
    225      this.principal,
    226      "automatic-download",
    227      pm.ALLOW_ACTION,
    228      pm.EXPIRE_SESSION
    229    );
    230    downloadSpam.hasBlockedData = downloadSpam.hasPartialData = false;
    231    const { url } = downloadSpam.source;
    232    for (let window of lazy.BrowserWindowTracker.orderedWindows) {
    233      this.downloadSpamProtection.removeDownloadSpamForWindow(url, window);
    234    }
    235  }
    236 }
    237 /**
    238 * Responsible for detecting events related to downloads spam and notifying the
    239 * relevant window's WindowSpamProtection object. This is a singleton object,
    240 * constructed by DownloadIntegration.sys.mjs when the first download is blocked.
    241 */
    242 export class DownloadSpamProtection {
    243  /**
    244   * Stores spam protection data per-window.
    245   *
    246   * @type {WeakMap<Window, WindowSpamProtection>}
    247   */
    248  _forWindowMap = new WeakMap();
    249 
    250  /**
    251   * Add download spam data for a given source URL in the window where the
    252   * download was blocked. This is invoked when a download is blocked by
    253   * nsExternalAppHandler::IsDownloadSpam
    254   *
    255   * @param {string} url
    256   * @param {nsILoadInfo} loadInfo
    257   */
    258  update(url, loadInfo) {
    259    loadInfo = loadInfo.QueryInterface(Ci.nsILoadInfo);
    260    const window = loadInfo.browsingContext.topChromeWindow;
    261    if (window == null) {
    262      lazy.DownloadsCommon.log(
    263        "Download spam blocked in a non-chrome window. URL: ",
    264        url
    265      );
    266      return;
    267    }
    268    // Get the spam protection object for a given window or create one if it
    269    // does not already exist. Also attach notification listeners to any pending
    270    // downloads views.
    271    let wsp =
    272      this._forWindowMap.get(window) ?? new WindowSpamProtection(window);
    273    this._forWindowMap.set(window, wsp);
    274    wsp.addDownloadSpam(
    275      url,
    276      new DownloadSpamEnabler(loadInfo.triggeringPrincipal, this)
    277    );
    278  }
    279 
    280  /**
    281   * Get the spam list for a given window (provided it exists).
    282   *
    283   * @param {Window} window
    284   * @returns {DownloadList}
    285   */
    286  getSpamListForWindow(window) {
    287    return this._forWindowMap.get(window)?.spamList;
    288  }
    289 
    290  /**
    291   * Remove the download spam data for a given source URL in the passed window,
    292   * if any exists.
    293   *
    294   * @param {string} url
    295   * @param {Window} window
    296   */
    297  removeDownloadSpamForWindow(url, window) {
    298    let wsp = this._forWindowMap.get(window);
    299    wsp?.removeDownloadSpamForUrl(url);
    300  }
    301 
    302  /**
    303   * Create the spam protection object for a given window (if not already
    304   * created) and prepare to start listening for notifications on the passed
    305   * downloads view. The bulk of resources won't be expended until a download is
    306   * blocked. To add multiple views, call this method multiple times.
    307   *
    308   * @param {object} view An object that implements handlers for download
    309   *                      related notifications, like onDownloadAdded.
    310   * @param {Window} window
    311   */
    312  register(view, window) {
    313    let wsp =
    314      this._forWindowMap.get(window) ?? new WindowSpamProtection(window);
    315    // Try setting up the view now; it will be deferred if there's no spam.
    316    wsp.registerView(view);
    317    this._forWindowMap.set(window, wsp);
    318  }
    319 
    320  /**
    321   * Remove the spam protection object for a window when it is closed.
    322   *
    323   * @param {Window} window
    324   */
    325  unregister(window) {
    326    let wsp = this._forWindowMap.get(window);
    327    if (wsp) {
    328      // Stop listening on the view if it was previously set up.
    329      wsp.removeAllViews();
    330      this._forWindowMap.delete(window);
    331    }
    332  }
    333 }
    334 
    335 /**
    336 * Represents a special Download object for download spam.
    337 *
    338 * @augments Download
    339 */
    340 class DownloadSpam extends Download {
    341  constructor(url, downloadSpamEnabler) {
    342    super();
    343    this._downloadSpamEnabler = downloadSpamEnabler;
    344    this.hasBlockedData = true;
    345    this.stopped = true;
    346    this.error = new DownloadError({
    347      becauseBlockedByReputationCheck: true,
    348      reputationCheckVerdict: lazy.Downloads.Error.BLOCK_VERDICT_DOWNLOAD_SPAM,
    349    });
    350    this.target = { path: "" };
    351    this.source = { url };
    352    this.blockedDownloadsCount = 1;
    353  }
    354 
    355  /**
    356   * Allows the principal which triggered this download to perform automatic downloads
    357   * and clears the UI from messages reporting this download spam
    358   */
    359  allow() {
    360    this._downloadSpamEnabler.allow(this);
    361    this._notifyChange();
    362  }
    363 }