summaryrefslogtreecommitdiffstats
path: root/browser/components/downloads/DownloadSpamProtection.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/downloads/DownloadSpamProtection.sys.mjs')
-rw-r--r--browser/components/downloads/DownloadSpamProtection.sys.mjs300
1 files changed, 300 insertions, 0 deletions
diff --git a/browser/components/downloads/DownloadSpamProtection.sys.mjs b/browser/components/downloads/DownloadSpamProtection.sys.mjs
new file mode 100644
index 0000000000..fa0cb97476
--- /dev/null
+++ b/browser/components/downloads/DownloadSpamProtection.sys.mjs
@@ -0,0 +1,300 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Provides functions to prevent multiple automatic downloads.
+ */
+
+import {
+ Download,
+ DownloadError,
+} from "resource://gre/modules/DownloadCore.sys.mjs";
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ DownloadList: "resource://gre/modules/DownloadList.sys.mjs",
+ Downloads: "resource://gre/modules/Downloads.sys.mjs",
+ DownloadsCommon: "resource:///modules/DownloadsCommon.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
+});
+
+/**
+ * Each window tracks download spam independently, so one of these objects is
+ * constructed for each window. This is responsible for tracking the spam and
+ * updating the window's downloads UI accordingly.
+ */
+class WindowSpamProtection {
+ constructor(window) {
+ this._window = window;
+ }
+
+ /**
+ * This map stores blocked spam downloads for the window, keyed by the
+ * download's source URL. This is done so we can track the number of times a
+ * given download has been blocked.
+ * @type {Map<String, DownloadSpam>}
+ */
+ _downloadSpamForUrl = new Map();
+
+ /**
+ * This set stores views that are waiting to have download notification
+ * listeners attached. They will be attached when the spamList is created
+ * (i.e. when the first spam download is blocked).
+ * @type {Set<Object>}
+ */
+ _pendingViews = new Set();
+
+ /**
+ * Set to true when we first start _blocking downloads in the window. This is
+ * used to lazily load the spamList. Spam downloads are rare enough that many
+ * sessions will have no blocked downloads. So we don't want to create a
+ * DownloadList unless we actually need it.
+ * @type {Boolean}
+ */
+ _blocking = false;
+
+ /**
+ * A per-window DownloadList for blocked spam downloads. Registered views will
+ * be sent notifications about downloads in this list, so that blocked spam
+ * downloads can be represented in the UI. If spam downloads haven't been
+ * blocked in the window, this will be undefined. See DownloadList.sys.mjs.
+ * @type {DownloadList | undefined}
+ */
+ get spamList() {
+ if (!this._blocking) {
+ return undefined;
+ }
+ if (!this._spamList) {
+ this._spamList = new lazy.DownloadList();
+ }
+ return this._spamList;
+ }
+
+ /**
+ * A per-window downloads indicator whose state depends on notifications from
+ * DownloadLists registered in the window (for example, the visual state of
+ * the downloads toolbar button). See DownloadsCommon.sys.mjs for more details.
+ * @type {DownloadsIndicatorData}
+ */
+ get indicator() {
+ if (!this._indicator) {
+ this._indicator = lazy.DownloadsCommon.getIndicatorData(this._window);
+ }
+ return this._indicator;
+ }
+
+ /**
+ * Add a blocked download to the spamList or increment the count of an
+ * existing blocked download, then notify listeners about this.
+ * @param {String} url
+ */
+ addDownloadSpam(url) {
+ this._blocking = true;
+ // Start listening on registered downloads views, if any exist.
+ this._maybeAddViews();
+ // If this URL is already paired with a DownloadSpam object, increment its
+ // blocked downloads count by 1 and don't open the downloads panel.
+ if (this._downloadSpamForUrl.has(url)) {
+ let downloadSpam = this._downloadSpamForUrl.get(url);
+ downloadSpam.blockedDownloadsCount += 1;
+ this.indicator.onDownloadStateChanged(downloadSpam);
+ return;
+ }
+ // Otherwise, create a new DownloadSpam object for the URL, add it to the
+ // spamList, and open the downloads panel.
+ let downloadSpam = new DownloadSpam(url);
+ this.spamList.add(downloadSpam);
+ this._downloadSpamForUrl.set(url, downloadSpam);
+ this._notifyDownloadSpamAdded(downloadSpam);
+ }
+
+ /**
+ * Notify the downloads panel that a new download has been added to the
+ * spamList. This is invoked when a new DownloadSpam object is created.
+ * @param {DownloadSpam} downloadSpam
+ */
+ _notifyDownloadSpamAdded(downloadSpam) {
+ let hasActiveDownloads = lazy.DownloadsCommon.summarizeDownloads(
+ this.indicator._activeDownloads()
+ ).numDownloading;
+ if (
+ !hasActiveDownloads &&
+ this._window === lazy.BrowserWindowTracker.getTopWindow()
+ ) {
+ // If there are no active downloads, open the downloads panel.
+ this._window.DownloadsPanel.showPanel();
+ } else {
+ // Otherwise, flash a taskbar/dock icon notification if available.
+ this._window.getAttention();
+ }
+ this.indicator.onDownloadAdded(downloadSpam);
+ }
+
+ /**
+ * Remove the download spam data for a given source URL.
+ * @param {String} url
+ */
+ removeDownloadSpamForUrl(url) {
+ if (this._downloadSpamForUrl.has(url)) {
+ let downloadSpam = this._downloadSpamForUrl.get(url);
+ this.spamList.remove(downloadSpam);
+ this.indicator.onDownloadRemoved(downloadSpam);
+ this._downloadSpamForUrl.delete(url);
+ }
+ }
+
+ /**
+ * Set up a downloads view (e.g. the downloads panel) to receive notifications
+ * about downloads in the spamList.
+ * @param {Object} view An object that implements handlers for download
+ * related notifications, like onDownloadAdded.
+ */
+ registerView(view) {
+ if (!view || this.spamList?._views.has(view)) {
+ return;
+ }
+ this._pendingViews.add(view);
+ this._maybeAddViews();
+ }
+
+ /**
+ * If any downloads have been blocked in the window, add download notification
+ * listeners for each downloads view that has been registered.
+ */
+ _maybeAddViews() {
+ if (this.spamList) {
+ for (let view of this._pendingViews) {
+ if (!this.spamList._views.has(view)) {
+ this.spamList.addView(view);
+ }
+ }
+ this._pendingViews.clear();
+ }
+ }
+
+ /**
+ * Remove download notification listeners for all views. This is invoked when
+ * the window is closed.
+ */
+ removeAllViews() {
+ if (this.spamList) {
+ for (let view of this.spamList._views) {
+ this.spamList.removeView(view);
+ }
+ }
+ this._pendingViews.clear();
+ }
+}
+
+/**
+ * Responsible for detecting events related to downloads spam and notifying the
+ * relevant window's WindowSpamProtection object. This is a singleton object,
+ * constructed by DownloadIntegration.sys.mjs when the first download is blocked.
+ */
+export class DownloadSpamProtection {
+ /**
+ * Stores spam protection data per-window.
+ * @type {WeakMap<Window, WindowSpamProtection>}
+ */
+ _forWindowMap = new WeakMap();
+
+ /**
+ * Add download spam data for a given source URL in the window where the
+ * download was blocked. This is invoked when a download is blocked by
+ * nsExternalAppHandler::IsDownloadSpam
+ * @param {String} url
+ * @param {Window} window
+ */
+ update(url, window) {
+ if (window == null) {
+ lazy.DownloadsCommon.log(
+ "Download spam blocked in a non-chrome window. URL: ",
+ url
+ );
+ return;
+ }
+ // Get the spam protection object for a given window or create one if it
+ // does not already exist. Also attach notification listeners to any pending
+ // downloads views.
+ let wsp =
+ this._forWindowMap.get(window) ?? new WindowSpamProtection(window);
+ this._forWindowMap.set(window, wsp);
+ wsp.addDownloadSpam(url);
+ }
+
+ /**
+ * Get the spam list for a given window (provided it exists).
+ * @param {Window} window
+ * @returns {DownloadList}
+ */
+ getSpamListForWindow(window) {
+ return this._forWindowMap.get(window)?.spamList;
+ }
+
+ /**
+ * Remove the download spam data for a given source URL in the passed window,
+ * if any exists.
+ * @param {String} url
+ * @param {Window} window
+ */
+ removeDownloadSpamForWindow(url, window) {
+ let wsp = this._forWindowMap.get(window);
+ wsp?.removeDownloadSpamForUrl(url);
+ }
+
+ /**
+ * Create the spam protection object for a given window (if not already
+ * created) and prepare to start listening for notifications on the passed
+ * downloads view. The bulk of resources won't be expended until a download is
+ * blocked. To add multiple views, call this method multiple times.
+ * @param {Object} view An object that implements handlers for download
+ * related notifications, like onDownloadAdded.
+ * @param {Window} window
+ */
+ register(view, window) {
+ let wsp =
+ this._forWindowMap.get(window) ?? new WindowSpamProtection(window);
+ // Try setting up the view now; it will be deferred if there's no spam.
+ wsp.registerView(view);
+ this._forWindowMap.set(window, wsp);
+ }
+
+ /**
+ * Remove the spam protection object for a window when it is closed.
+ * @param {Window} window
+ */
+ unregister(window) {
+ let wsp = this._forWindowMap.get(window);
+ if (wsp) {
+ // Stop listening on the view if it was previously set up.
+ wsp.removeAllViews();
+ this._forWindowMap.delete(window);
+ }
+ }
+}
+
+/**
+ * Represents a special Download object for download spam.
+ * @extends Download
+ */
+class DownloadSpam extends Download {
+ constructor(url) {
+ super();
+ this.hasBlockedData = true;
+ this.stopped = true;
+ this.error = new DownloadError({
+ becauseBlockedByReputationCheck: true,
+ reputationCheckVerdict: lazy.Downloads.Error.BLOCK_VERDICT_DOWNLOAD_SPAM,
+ });
+ this.target = { path: "" };
+ this.source = { url };
+ this.blockedDownloadsCount = 1;
+ }
+}