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/. */
6 * Provides functions to prevent multiple automatic downloads.
12 } from "resource://gre/modules/DownloadCore.sys.mjs";
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: "resource:///modules/DownloadsCommon.sys.mjs",
24 * Each window tracks download spam independently, so one of these objects is
25 * constructed for each window. This is responsible for tracking the spam and
26 * updating the window's downloads UI accordingly.
28 class WindowSpamProtection {
30 this._window = window;
34 * This map stores blocked spam downloads for the window, keyed by the
35 * download's source URL. This is done so we can track the number of times a
36 * given download has been blocked.
37 * @type {Map<String, DownloadSpam>}
39 _downloadSpamForUrl = new Map();
42 * This set stores views that are waiting to have download notification
43 * listeners attached. They will be attached when the spamList is created
44 * (i.e. when the first spam download is blocked).
47 _pendingViews = new Set();
50 * Set to true when we first start _blocking downloads in the window. This is
51 * used to lazily load the spamList. Spam downloads are rare enough that many
52 * sessions will have no blocked downloads. So we don't want to create a
53 * DownloadList unless we actually need it.
59 * A per-window DownloadList for blocked spam downloads. Registered views will
60 * be sent notifications about downloads in this list, so that blocked spam
61 * downloads can be represented in the UI. If spam downloads haven't been
62 * blocked in the window, this will be undefined. See DownloadList.sys.mjs.
63 * @type {DownloadList | undefined}
66 if (!this._blocking) {
69 if (!this._spamList) {
70 this._spamList = new lazy.DownloadList();
72 return this._spamList;
76 * A per-window downloads indicator whose state depends on notifications from
77 * DownloadLists registered in the window (for example, the visual state of
78 * the downloads toolbar button). See DownloadsCommon.sys.mjs for more details.
79 * @type {DownloadsIndicatorData}
82 if (!this._indicator) {
83 this._indicator = lazy.DownloadsCommon.getIndicatorData(this._window);
85 return this._indicator;
89 * Add a blocked download to the spamList or increment the count of an
90 * existing blocked download, then notify listeners about this.
93 addDownloadSpam(url) {
94 this._blocking = true;
95 // Start listening on registered downloads views, if any exist.
96 this._maybeAddViews();
97 // If this URL is already paired with a DownloadSpam object, increment its
98 // blocked downloads count by 1 and don't open the downloads panel.
99 if (this._downloadSpamForUrl.has(url)) {
100 let downloadSpam = this._downloadSpamForUrl.get(url);
101 downloadSpam.blockedDownloadsCount += 1;
102 this.indicator.onDownloadStateChanged(downloadSpam);
105 // Otherwise, create a new DownloadSpam object for the URL, add it to the
106 // spamList, and open the downloads panel.
107 let downloadSpam = new DownloadSpam(url);
108 this.spamList.add(downloadSpam);
109 this._downloadSpamForUrl.set(url, downloadSpam);
110 this._notifyDownloadSpamAdded(downloadSpam);
114 * Notify the downloads panel that a new download has been added to the
115 * spamList. This is invoked when a new DownloadSpam object is created.
116 * @param {DownloadSpam} downloadSpam
118 _notifyDownloadSpamAdded(downloadSpam) {
119 let hasActiveDownloads = lazy.DownloadsCommon.summarizeDownloads(
120 this.indicator._activeDownloads()
123 !hasActiveDownloads &&
124 this._window === lazy.BrowserWindowTracker.getTopWindow()
126 // If there are no active downloads, open the downloads panel.
127 this._window.DownloadsPanel.showPanel();
129 // Otherwise, flash a taskbar/dock icon notification if available.
130 this._window.getAttention();
132 this.indicator.onDownloadAdded(downloadSpam);
136 * Remove the download spam data for a given source URL.
137 * @param {String} url
139 removeDownloadSpamForUrl(url) {
140 if (this._downloadSpamForUrl.has(url)) {
141 let downloadSpam = this._downloadSpamForUrl.get(url);
142 this.spamList.remove(downloadSpam);
143 this.indicator.onDownloadRemoved(downloadSpam);
144 this._downloadSpamForUrl.delete(url);
149 * Set up a downloads view (e.g. the downloads panel) to receive notifications
150 * about downloads in the spamList.
151 * @param {Object} view An object that implements handlers for download
152 * related notifications, like onDownloadAdded.
155 if (!view || this.spamList?._views.has(view)) {
158 this._pendingViews.add(view);
159 this._maybeAddViews();
163 * If any downloads have been blocked in the window, add download notification
164 * listeners for each downloads view that has been registered.
168 for (let view of this._pendingViews) {
169 if (!this.spamList._views.has(view)) {
170 this.spamList.addView(view);
173 this._pendingViews.clear();
178 * Remove download notification listeners for all views. This is invoked when
179 * the window is closed.
183 for (let view of this.spamList._views) {
184 this.spamList.removeView(view);
187 this._pendingViews.clear();
192 * Responsible for detecting events related to downloads spam and notifying the
193 * relevant window's WindowSpamProtection object. This is a singleton object,
194 * constructed by DownloadIntegration.sys.mjs when the first download is blocked.
196 export class DownloadSpamProtection {
198 * Stores spam protection data per-window.
199 * @type {WeakMap<Window, WindowSpamProtection>}
201 _forWindowMap = new WeakMap();
204 * Add download spam data for a given source URL in the window where the
205 * download was blocked. This is invoked when a download is blocked by
206 * nsExternalAppHandler::IsDownloadSpam
207 * @param {String} url
208 * @param {Window} window
210 update(url, window) {
211 if (window == null) {
212 lazy.DownloadsCommon.log(
213 "Download spam blocked in a non-chrome window. URL: ",
218 // Get the spam protection object for a given window or create one if it
219 // does not already exist. Also attach notification listeners to any pending
222 this._forWindowMap.get(window) ?? new WindowSpamProtection(window);
223 this._forWindowMap.set(window, wsp);
224 wsp.addDownloadSpam(url);
228 * Get the spam list for a given window (provided it exists).
229 * @param {Window} window
230 * @returns {DownloadList}
232 getSpamListForWindow(window) {
233 return this._forWindowMap.get(window)?.spamList;
237 * Remove the download spam data for a given source URL in the passed window,
239 * @param {String} url
240 * @param {Window} window
242 removeDownloadSpamForWindow(url, window) {
243 let wsp = this._forWindowMap.get(window);
244 wsp?.removeDownloadSpamForUrl(url);
248 * Create the spam protection object for a given window (if not already
249 * created) and prepare to start listening for notifications on the passed
250 * downloads view. The bulk of resources won't be expended until a download is
251 * blocked. To add multiple views, call this method multiple times.
252 * @param {Object} view An object that implements handlers for download
253 * related notifications, like onDownloadAdded.
254 * @param {Window} window
256 register(view, window) {
258 this._forWindowMap.get(window) ?? new WindowSpamProtection(window);
259 // Try setting up the view now; it will be deferred if there's no spam.
260 wsp.registerView(view);
261 this._forWindowMap.set(window, wsp);
265 * Remove the spam protection object for a window when it is closed.
266 * @param {Window} window
269 let wsp = this._forWindowMap.get(window);
271 // Stop listening on the view if it was previously set up.
272 wsp.removeAllViews();
273 this._forWindowMap.delete(window);
279 * Represents a special Download object for download spam.
282 class DownloadSpam extends Download {
285 this.hasBlockedData = true;
287 this.error = new DownloadError({
288 becauseBlockedByReputationCheck: true,
289 reputationCheckVerdict: lazy.Downloads.Error.BLOCK_VERDICT_DOWNLOAD_SPAM,
291 this.target = { path: "" };
292 this.source = { url };
293 this.blockedDownloadsCount = 1;