295 lines
9.3 KiB
JavaScript
295 lines
9.3 KiB
JavaScript
/* 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";
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
|
|
DownloadList: "resource://gre/modules/DownloadList.sys.mjs",
|
|
Downloads: "resource://gre/modules/Downloads.sys.mjs",
|
|
DownloadsCommon: "resource:///modules/DownloadsCommon.sys.mjs",
|
|
});
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|