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 }