Bug 1869092 - Fix timeouts in browser_PanelMultiView.js. r=twisniewski,test-only
[gecko.git] / browser / components / downloads / DownloadSpamProtection.sys.mjs
bloba05c508e628fd8202b260d3a7c1a853f31bfccc2
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/. */
5 /**
6  * Provides functions to prevent multiple automatic downloads.
7  */
9 import {
10   Download,
11   DownloadError,
12 } from "resource://gre/modules/DownloadCore.sys.mjs";
14 const lazy = {};
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",
21 });
23 /**
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.
27  */
28 class WindowSpamProtection {
29   constructor(window) {
30     this._window = window;
31   }
33   /**
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>}
38    */
39   _downloadSpamForUrl = new Map();
41   /**
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).
45    * @type {Set<Object>}
46    */
47   _pendingViews = new Set();
49   /**
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.
54    * @type {Boolean}
55    */
56   _blocking = false;
58   /**
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}
64    */
65   get spamList() {
66     if (!this._blocking) {
67       return undefined;
68     }
69     if (!this._spamList) {
70       this._spamList = new lazy.DownloadList();
71     }
72     return this._spamList;
73   }
75   /**
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}
80    */
81   get indicator() {
82     if (!this._indicator) {
83       this._indicator = lazy.DownloadsCommon.getIndicatorData(this._window);
84     }
85     return this._indicator;
86   }
88   /**
89    * Add a blocked download to the spamList or increment the count of an
90    * existing blocked download, then notify listeners about this.
91    * @param {String} url
92    */
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);
103       return;
104     }
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);
111   }
113   /**
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
117    */
118   _notifyDownloadSpamAdded(downloadSpam) {
119     let hasActiveDownloads = lazy.DownloadsCommon.summarizeDownloads(
120       this.indicator._activeDownloads()
121     ).numDownloading;
122     if (
123       !hasActiveDownloads &&
124       this._window === lazy.BrowserWindowTracker.getTopWindow()
125     ) {
126       // If there are no active downloads, open the downloads panel.
127       this._window.DownloadsPanel.showPanel();
128     } else {
129       // Otherwise, flash a taskbar/dock icon notification if available.
130       this._window.getAttention();
131     }
132     this.indicator.onDownloadAdded(downloadSpam);
133   }
135   /**
136    * Remove the download spam data for a given source URL.
137    * @param {String} url
138    */
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);
145     }
146   }
148   /**
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.
153    */
154   registerView(view) {
155     if (!view || this.spamList?._views.has(view)) {
156       return;
157     }
158     this._pendingViews.add(view);
159     this._maybeAddViews();
160   }
162   /**
163    * If any downloads have been blocked in the window, add download notification
164    * listeners for each downloads view that has been registered.
165    */
166   _maybeAddViews() {
167     if (this.spamList) {
168       for (let view of this._pendingViews) {
169         if (!this.spamList._views.has(view)) {
170           this.spamList.addView(view);
171         }
172       }
173       this._pendingViews.clear();
174     }
175   }
177   /**
178    * Remove download notification listeners for all views. This is invoked when
179    * the window is closed.
180    */
181   removeAllViews() {
182     if (this.spamList) {
183       for (let view of this.spamList._views) {
184         this.spamList.removeView(view);
185       }
186     }
187     this._pendingViews.clear();
188   }
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.
195  */
196 export class DownloadSpamProtection {
197   /**
198    * Stores spam protection data per-window.
199    * @type {WeakMap<Window, WindowSpamProtection>}
200    */
201   _forWindowMap = new WeakMap();
203   /**
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
209    */
210   update(url, window) {
211     if (window == null) {
212       lazy.DownloadsCommon.log(
213         "Download spam blocked in a non-chrome window. URL: ",
214         url
215       );
216       return;
217     }
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
220     // downloads views.
221     let wsp =
222       this._forWindowMap.get(window) ?? new WindowSpamProtection(window);
223     this._forWindowMap.set(window, wsp);
224     wsp.addDownloadSpam(url);
225   }
227   /**
228    * Get the spam list for a given window (provided it exists).
229    * @param {Window} window
230    * @returns {DownloadList}
231    */
232   getSpamListForWindow(window) {
233     return this._forWindowMap.get(window)?.spamList;
234   }
236   /**
237    * Remove the download spam data for a given source URL in the passed window,
238    * if any exists.
239    * @param {String} url
240    * @param {Window} window
241    */
242   removeDownloadSpamForWindow(url, window) {
243     let wsp = this._forWindowMap.get(window);
244     wsp?.removeDownloadSpamForUrl(url);
245   }
247   /**
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
255    */
256   register(view, window) {
257     let wsp =
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);
262   }
264   /**
265    * Remove the spam protection object for a window when it is closed.
266    * @param {Window} window
267    */
268   unregister(window) {
269     let wsp = this._forWindowMap.get(window);
270     if (wsp) {
271       // Stop listening on the view if it was previously set up.
272       wsp.removeAllViews();
273       this._forWindowMap.delete(window);
274     }
275   }
279  * Represents a special Download object for download spam.
280  * @extends Download
281  */
282 class DownloadSpam extends Download {
283   constructor(url) {
284     super();
285     this.hasBlockedData = true;
286     this.stopped = true;
287     this.error = new DownloadError({
288       becauseBlockedByReputationCheck: true,
289       reputationCheckVerdict: lazy.Downloads.Error.BLOCK_VERDICT_DOWNLOAD_SPAM,
290     });
291     this.target = { path: "" };
292     this.source = { url };
293     this.blockedDownloadsCount = 1;
294   }