DownloadsCommon.sys.mjs (54827B)
1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ 2 /* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 5 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 /** 8 * Handles the Downloads panel shared methods and data access. 9 * 10 * This file includes the following constructors and global objects: 11 * 12 * DownloadsCommon 13 * This object is exposed directly to the consumers of this JavaScript module, 14 * and provides shared methods for all the instances of the user interface. 15 * 16 * DownloadsData 17 * Retrieves the list of past and completed downloads from the underlying 18 * Downloads API data, and provides asynchronous notifications allowing 19 * to build a consistent view of the available data. 20 * 21 * DownloadsIndicatorData 22 * This object registers itself with DownloadsData as a view, and transforms the 23 * notifications it receives into overall status data, that is then broadcast to 24 * the registered download status indicators. 25 */ 26 27 // Globals 28 29 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 30 31 const lazy = {}; 32 33 ChromeUtils.defineESModuleGetters(lazy, { 34 BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", 35 DownloadHistory: "resource://gre/modules/DownloadHistory.sys.mjs", 36 DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs", 37 Downloads: "resource://gre/modules/Downloads.sys.mjs", 38 NetUtil: "resource://gre/modules/NetUtil.sys.mjs", 39 PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", 40 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 41 }); 42 43 XPCOMUtils.defineLazyServiceGetters(lazy, { 44 gClipboardHelper: [ 45 "@mozilla.org/widget/clipboardhelper;1", 46 Ci.nsIClipboardHelper, 47 ], 48 gMIMEService: ["@mozilla.org/mime;1", Ci.nsIMIMEService], 49 }); 50 51 ChromeUtils.defineLazyGetter(lazy, "DownloadsLogger", () => { 52 let { ConsoleAPI } = ChromeUtils.importESModule( 53 "resource://gre/modules/Console.sys.mjs" 54 ); 55 let consoleOptions = { 56 maxLogLevelPref: "toolkit.download.loglevel", 57 prefix: "Downloads", 58 }; 59 return new ConsoleAPI(consoleOptions); 60 }); 61 62 XPCOMUtils.defineLazyPreferenceGetter( 63 lazy, 64 "gAlwaysOpenPanel", 65 "browser.download.alwaysOpenPanel", 66 true 67 ); 68 69 const kDownloadsStringBundleUrl = 70 "chrome://browser/locale/downloads/downloads.properties"; 71 72 const kDownloadsFluentStrings = new Localization( 73 ["browser/downloads.ftl"], 74 true 75 ); 76 77 const kDownloadsStringsRequiringFormatting = { 78 contentAnalysisNoAgentError: true, 79 contentAnalysisInvalidAgentSignatureError: true, 80 contentAnalysisUnspecifiedError: true, 81 contentAnalysisTimeoutError: true, 82 sizeWithUnits: true, 83 statusSeparator: true, 84 statusSeparatorBeforeNumber: true, 85 }; 86 87 const kMaxHistoryResultsForLimitedView = 42; 88 89 const kPrefBranch = Services.prefs.getBranch("browser.download."); 90 91 const kGenericContentTypes = [ 92 "application/octet-stream", 93 "binary/octet-stream", 94 "application/unknown", 95 ]; 96 97 var PrefObserver = { 98 QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), 99 getPref(name) { 100 try { 101 switch (typeof this.prefs[name]) { 102 case "boolean": 103 return kPrefBranch.getBoolPref(name); 104 } 105 } catch (ex) {} 106 return this.prefs[name]; 107 }, 108 observe(aSubject, aTopic, aData) { 109 if (this.prefs.hasOwnProperty(aData)) { 110 delete this[aData]; 111 this[aData] = this.getPref(aData); 112 } 113 }, 114 register(prefs) { 115 this.prefs = prefs; 116 kPrefBranch.addObserver("", this); 117 for (let key in prefs) { 118 let name = key; 119 ChromeUtils.defineLazyGetter(this, name, function () { 120 return PrefObserver.getPref(name); 121 }); 122 } 123 }, 124 }; 125 126 PrefObserver.register({ 127 // prefName: defaultValue 128 openInSystemViewerContextMenuItem: true, 129 alwaysOpenInSystemViewerContextMenuItem: true, 130 }); 131 132 // DownloadsCommon 133 134 /** 135 * This object is exposed directly to the consumers of this JavaScript module, 136 * and provides shared methods for all the instances of the user interface. 137 */ 138 export var DownloadsCommon = { 139 // The following legacy constants are still returned by stateOfDownload, but 140 // individual properties of the Download object should normally be used. 141 DOWNLOAD_NOTSTARTED: -1, 142 DOWNLOAD_DOWNLOADING: 0, 143 DOWNLOAD_FINISHED: 1, 144 DOWNLOAD_FAILED: 2, 145 DOWNLOAD_CANCELED: 3, 146 DOWNLOAD_PAUSED: 4, 147 DOWNLOAD_BLOCKED_PARENTAL: 6, 148 DOWNLOAD_DIRTY: 8, 149 DOWNLOAD_BLOCKED_POLICY: 9, 150 DOWNLOAD_BLOCKED_CONTENT_ANALYSIS: 10, 151 152 // The following are the possible values of the "attention" property. 153 ATTENTION_NONE: "", 154 ATTENTION_SUCCESS: "success", 155 ATTENTION_INFO: "info", 156 ATTENTION_WARNING: "warning", 157 ATTENTION_SEVERE: "severe", 158 159 // Bit flags for the attentionSuppressed property. 160 SUPPRESS_NONE: 0, 161 SUPPRESS_PANEL_OPEN: 1, 162 SUPPRESS_ALL_DOWNLOADS_OPEN: 2, 163 SUPPRESS_CONTENT_AREA_DOWNLOADS_OPEN: 4, 164 165 /** 166 * Returns an object whose keys are the string names from the downloads string 167 * bundle, and whose values are either the translated strings or functions 168 * returning formatted strings. 169 */ 170 get strings() { 171 let strings = {}; 172 let sb = Services.strings.createBundle(kDownloadsStringBundleUrl); 173 for (let string of sb.getSimpleEnumeration()) { 174 let stringName = string.key; 175 if (stringName in kDownloadsStringsRequiringFormatting) { 176 strings[stringName] = function () { 177 // Convert "arguments" to a real array before calling into XPCOM. 178 return sb.formatStringFromName(stringName, Array.from(arguments)); 179 }; 180 } else { 181 strings[stringName] = string.value; 182 } 183 } 184 delete this.strings; 185 return (this.strings = strings); 186 }, 187 188 /** 189 * Indicates whether or not to show the 'Open in system viewer' context menu item when appropriate 190 */ 191 get openInSystemViewerItemEnabled() { 192 return PrefObserver.openInSystemViewerContextMenuItem; 193 }, 194 195 /** 196 * Indicates whether or not to show the 'Always open...' context menu item when appropriate 197 */ 198 get alwaysOpenInSystemViewerItemEnabled() { 199 return PrefObserver.alwaysOpenInSystemViewerContextMenuItem; 200 }, 201 202 /** 203 * Get access to one of the DownloadsData, PrivateDownloadsData, or 204 * HistoryDownloadsData objects, depending on the privacy status of the 205 * specified window and on whether history downloads should be included. 206 * 207 * @param [optional] window 208 * The browser window which owns the download button. 209 * If not given, the privacy status will be assumed as non-private. 210 * @param [optional] history 211 * True to include history downloads when the window is public. 212 * @param [optional] privateAll 213 * Whether to force the public downloads data to be returned together 214 * with the private downloads data for a private window. 215 * @param [optional] limited 216 * True to limit the amount of downloads returned to 217 * `kMaxHistoryResultsForLimitedView`. 218 */ 219 getData(window, history = false, privateAll = false, limited = false) { 220 let isPrivate = 221 window && lazy.PrivateBrowsingUtils.isContentWindowPrivate(window); 222 if (isPrivate && !privateAll) { 223 return lazy.PrivateDownloadsData; 224 } 225 if (history) { 226 if (isPrivate && privateAll) { 227 return lazy.LimitedPrivateHistoryDownloadData; 228 } 229 return limited 230 ? lazy.LimitedHistoryDownloadsData 231 : lazy.HistoryDownloadsData; 232 } 233 return lazy.DownloadsData; 234 }, 235 236 /** 237 * Initializes the Downloads back-end and starts receiving events for both the 238 * private and non-private downloads data objects. 239 */ 240 initializeAllDataLinks() { 241 lazy.DownloadsData.initializeDataLink(); 242 lazy.PrivateDownloadsData.initializeDataLink(); 243 }, 244 245 /** 246 * Get access to one of the DownloadsIndicatorData or 247 * PrivateDownloadsIndicatorData objects, depending on the privacy status of 248 * the window in question. 249 */ 250 getIndicatorData(aWindow) { 251 if (lazy.PrivateBrowsingUtils.isContentWindowPrivate(aWindow)) { 252 return lazy.PrivateDownloadsIndicatorData; 253 } 254 return lazy.DownloadsIndicatorData; 255 }, 256 257 /** 258 * Returns a reference to the DownloadsSummaryData singleton - creating one 259 * in the process if one hasn't been instantiated yet. 260 * 261 * @param aWindow 262 * The browser window which owns the download button. 263 * @param aNumToExclude 264 * The number of items on the top of the downloads list to exclude 265 * from the summary. 266 */ 267 getSummary(aWindow, aNumToExclude) { 268 if (lazy.PrivateBrowsingUtils.isContentWindowPrivate(aWindow)) { 269 if (this._privateSummary) { 270 return this._privateSummary; 271 } 272 return (this._privateSummary = new DownloadsSummaryData( 273 true, 274 aNumToExclude 275 )); 276 } 277 if (this._summary) { 278 return this._summary; 279 } 280 return (this._summary = new DownloadsSummaryData(false, aNumToExclude)); 281 }, 282 _summary: null, 283 _privateSummary: null, 284 285 /** 286 * Returns the legacy state integer value for the provided Download object. 287 */ 288 stateOfDownload(download) { 289 // Collapse state using the correct priority. 290 if (!download.stopped) { 291 return DownloadsCommon.DOWNLOAD_DOWNLOADING; 292 } 293 if (download.succeeded) { 294 return DownloadsCommon.DOWNLOAD_FINISHED; 295 } 296 if (download.error) { 297 if (download.error.becauseBlockedByParentalControls) { 298 return DownloadsCommon.DOWNLOAD_BLOCKED_PARENTAL; 299 } 300 if (download.error.becauseBlockedByReputationCheck) { 301 return DownloadsCommon.DOWNLOAD_DIRTY; 302 } 303 if (download.error.becauseBlockedByContentAnalysis) { 304 // BLOCK_VERDICT_MALWARE indicates that the download was 305 // blocked by the content analysis service, so return 306 // DOWNLOAD_BLOCKED_CONTENT_ANALYSIS to indicate this. 307 // Otherwise, the content analysis service returned 308 // WARN, so the user has a chance to unblock the download, 309 // which corresponds with DOWNLOAD_DIRTY. 310 return download.error.reputationCheckVerdict === 311 lazy.Downloads.Error.BLOCK_VERDICT_MALWARE 312 ? DownloadsCommon.DOWNLOAD_BLOCKED_CONTENT_ANALYSIS 313 : DownloadsCommon.DOWNLOAD_DIRTY; 314 } 315 return DownloadsCommon.DOWNLOAD_FAILED; 316 } 317 if (download.canceled) { 318 if (download.hasPartialData) { 319 return DownloadsCommon.DOWNLOAD_PAUSED; 320 } 321 return DownloadsCommon.DOWNLOAD_CANCELED; 322 } 323 return DownloadsCommon.DOWNLOAD_NOTSTARTED; 324 }, 325 326 /** 327 * Removes a Download object from both session and history downloads. 328 */ 329 async deleteDownload(download) { 330 // Check hasBlockedData to avoid double counting if you click the X button 331 // in the Library view and then delete the download from the history. 332 if ( 333 download.error?.becauseBlockedByReputationCheck && 334 download.hasBlockedData 335 ) { 336 Glean.downloads.userActionOnBlockedDownload[ 337 download.error.reputationCheckVerdict 338 ].accumulateSingleSample(1); // confirm block 339 } 340 341 // Remove the associated history element first, if any, so that the views 342 // that combine history and session downloads won't resurrect the history 343 // download into the view just before it is deleted permanently. 344 try { 345 await lazy.PlacesUtils.history.remove(download.source.url); 346 } catch (ex) { 347 console.error(ex); 348 } 349 let list = await lazy.Downloads.getList(lazy.Downloads.ALL); 350 await list.remove(download); 351 if (download.error?.becauseBlockedByContentAnalysis) { 352 await download.respondToContentAnalysisWarnWithBlock(); 353 } 354 await download.finalize(true); 355 }, 356 357 /** 358 * Deletes all files associated with a download, with or without removing it 359 * from the session downloads list and/or download history. 360 * 361 * @param download 362 * The download to delete and/or forget. 363 * @param clearHistory 364 * Optional. Removes history from session downloads list or history. 365 * 0 - Don't remove the download from session list or history. 366 * 1 - Remove the download from session list, but not history. 367 * 2 - Remove the download from both session list and history. 368 */ 369 async deleteDownloadFiles(download, clearHistory = 0) { 370 if (clearHistory > 1) { 371 try { 372 await lazy.PlacesUtils.history.remove(download.source.url); 373 } catch (ex) { 374 console.error(ex); 375 } 376 } 377 if (clearHistory > 0) { 378 let list = await lazy.Downloads.getList(lazy.Downloads.ALL); 379 await list.remove(download); 380 } 381 await download.manuallyRemoveData(); 382 if (download.error?.becauseBlockedByContentAnalysis) { 383 await download.respondToContentAnalysisWarnWithBlock(); 384 } 385 if (clearHistory < 2) { 386 lazy.DownloadHistory.updateMetaData(download).catch(console.error); 387 } 388 }, 389 390 /** 391 * Get a nsIMIMEInfo object for a download 392 */ 393 getMimeInfo(download) { 394 if (!download.succeeded) { 395 return null; 396 } 397 let contentType = download.contentType; 398 let url = Cc["@mozilla.org/network/standard-url-mutator;1"] 399 .createInstance(Ci.nsIURIMutator) 400 .setSpec("http://example.com") // construct the URL 401 .setFilePath(download.target.path) 402 .finalize() 403 .QueryInterface(Ci.nsIURL); 404 let fileExtension = url.fileExtension; 405 406 // look at file extension if there's no contentType or it is generic 407 if (!contentType || kGenericContentTypes.includes(contentType)) { 408 try { 409 contentType = lazy.gMIMEService.getTypeFromExtension(fileExtension); 410 } catch (ex) { 411 DownloadsCommon.log( 412 "Cant get mimeType from file extension: ", 413 fileExtension 414 ); 415 } 416 } 417 if (!(contentType || fileExtension)) { 418 return null; 419 } 420 let mimeInfo = null; 421 try { 422 mimeInfo = lazy.gMIMEService.getFromTypeAndExtension( 423 contentType || "", 424 fileExtension || "" 425 ); 426 } catch (ex) { 427 DownloadsCommon.log( 428 "Can't get nsIMIMEInfo for contentType: ", 429 contentType, 430 "and fileExtension:", 431 fileExtension 432 ); 433 } 434 return mimeInfo; 435 }, 436 437 /** 438 * Confirm if the download exists on the filesystem and is a given mime-type 439 */ 440 isFileOfType(download, mimeType) { 441 if (!(download.succeeded && download.target?.exists)) { 442 DownloadsCommon.log( 443 `isFileOfType returning false for mimeType: ${mimeType}, succeeded: ${download.succeeded}, exists: ${download.target?.exists}` 444 ); 445 return false; 446 } 447 let mimeInfo = DownloadsCommon.getMimeInfo(download); 448 return mimeInfo?.type === mimeType.toLowerCase(); 449 }, 450 451 /** 452 * Copies the source URI of the given Download object to the clipboard. 453 */ 454 copyDownloadLink(download) { 455 lazy.gClipboardHelper.copyString( 456 download.source.originalUrl || download.source.url 457 ); 458 }, 459 460 /** 461 * Given an iterable collection of Download objects, generates and returns 462 * statistics about that collection. 463 * 464 * @param downloads An iterable collection of Download objects. 465 * 466 * @return Object whose properties are the generated statistics. Currently, 467 * we return the following properties: 468 * 469 * numActive : The total number of downloads. 470 * numPaused : The total number of paused downloads. 471 * numDownloading : The total number of downloads being downloaded. 472 * totalSize : The total size of all downloads once completed. 473 * totalTransferred: The total amount of transferred data for these 474 * downloads. 475 * slowestSpeed : The slowest download rate. 476 * rawTimeLeft : The estimated time left for the downloads to 477 * complete. 478 * percentComplete : The percentage of bytes successfully downloaded. 479 */ 480 summarizeDownloads(downloads) { 481 let summary = { 482 numActive: 0, 483 numPaused: 0, 484 numDownloading: 0, 485 totalSize: 0, 486 totalTransferred: 0, 487 // slowestSpeed is Infinity so that we can use Math.min to 488 // find the slowest speed. We'll set this to 0 afterwards if 489 // it's still at Infinity by the time we're done iterating all 490 // download. 491 slowestSpeed: Infinity, 492 rawTimeLeft: -1, 493 percentComplete: -1, 494 }; 495 496 for (let download of downloads) { 497 summary.numActive++; 498 499 if (!download.stopped) { 500 summary.numDownloading++; 501 if (download.hasProgress && download.speed > 0) { 502 let sizeLeft = download.totalBytes - download.currentBytes; 503 summary.rawTimeLeft = Math.max( 504 summary.rawTimeLeft, 505 sizeLeft / download.speed 506 ); 507 summary.slowestSpeed = Math.min(summary.slowestSpeed, download.speed); 508 } 509 } else if (download.canceled && download.hasPartialData) { 510 summary.numPaused++; 511 } 512 513 // Only add to total values if we actually know the download size. 514 if (download.succeeded) { 515 summary.totalSize += download.target.size; 516 summary.totalTransferred += download.target.size; 517 } else if (download.hasProgress) { 518 summary.totalSize += download.totalBytes; 519 summary.totalTransferred += download.currentBytes; 520 } 521 } 522 523 if (summary.totalSize != 0) { 524 summary.percentComplete = Math.floor( 525 (summary.totalTransferred / summary.totalSize) * 100 526 ); 527 } 528 529 if (summary.slowestSpeed == Infinity) { 530 summary.slowestSpeed = 0; 531 } 532 533 return summary; 534 }, 535 536 /** 537 * If necessary, smooths the estimated number of seconds remaining for one 538 * or more downloads to complete. 539 * 540 * @param aSeconds 541 * Current raw estimate on number of seconds left for one or more 542 * downloads. This is a floating point value to help get sub-second 543 * accuracy for current and future estimates. 544 */ 545 smoothSeconds(aSeconds, aLastSeconds) { 546 // We apply an algorithm similar to the DownloadUtils.getTimeLeft function, 547 // though tailored to a single time estimation for all downloads. We never 548 // apply something if the new value is less than half the previous value. 549 let shouldApplySmoothing = aLastSeconds >= 0 && aSeconds > aLastSeconds / 2; 550 if (shouldApplySmoothing) { 551 // Apply hysteresis to favor downward over upward swings. Trust only 30% 552 // of the new value if lower, and 10% if higher (exponential smoothing). 553 let diff = aSeconds - aLastSeconds; 554 aSeconds = aLastSeconds + (diff < 0 ? 0.3 : 0.1) * diff; 555 556 // If the new time is similar, reuse something close to the last time 557 // left, but subtract a little to provide forward progress. 558 diff = aSeconds - aLastSeconds; 559 let diffPercent = (diff / aLastSeconds) * 100; 560 if (Math.abs(diff) < 5 || Math.abs(diffPercent) < 5) { 561 aSeconds = aLastSeconds - (diff < 0 ? 0.4 : 0.2); 562 } 563 } 564 565 // In the last few seconds of downloading, we are always subtracting and 566 // never adding to the time left. Ensure that we never fall below one 567 // second left until all downloads are actually finished. 568 return (aLastSeconds = Math.max(aSeconds, 1)); 569 }, 570 571 /** 572 * Opens a downloaded file. 573 * 574 * @param downloadProperties 575 * A Download object or the initial properties of a serialized download 576 * @param options.openWhere 577 * Optional string indicating how to handle opening a download target file URI. 578 * One of "window", "tab", "tabshifted". 579 * @param options.useSystemDefault 580 * Optional value indicating how to handle launching this download, 581 * this call only. Will override the associated mimeInfo.preferredAction 582 * @returns {Promise<void>} 583 * Resolves when the instruction to launch the file has been successfully 584 * given to the operating system or handled internally. 585 * @rejects 586 * With a JavaScript exception if there was an error trying to launch the file. 587 */ 588 async openDownload(download, options) { 589 // some download objects got serialized and need reconstituting 590 if (typeof download.launch !== "function") { 591 download = await lazy.Downloads.createDownload(download); 592 } 593 return download.launch(options).catch(ex => console.error(ex)); 594 }, 595 596 /** 597 * Show a downloaded file in the system file manager. 598 * 599 * @param aFile 600 * a downloaded file. 601 */ 602 showDownloadedFile(aFile) { 603 if (!(aFile instanceof Ci.nsIFile)) { 604 throw new Error("aFile must be a nsIFile object"); 605 } 606 try { 607 // Show the directory containing the file and select the file. 608 aFile.reveal(); 609 } catch (ex) { 610 // If reveal fails for some reason (e.g., it's not implemented on unix 611 // or the file doesn't exist), try using the parent if we have it. 612 let parent = aFile.parent; 613 if (parent) { 614 this.showDirectory(parent); 615 } 616 } 617 }, 618 619 /** 620 * Show the specified folder in the system file manager. 621 * 622 * @param aDirectory 623 * a directory to be opened with system file manager. 624 */ 625 showDirectory(aDirectory) { 626 if (!(aDirectory instanceof Ci.nsIFile)) { 627 throw new Error("aDirectory must be a nsIFile object"); 628 } 629 try { 630 aDirectory.launch(); 631 } catch (ex) { 632 // If launch fails (probably because it's not implemented), let 633 // the OS handler try to open the directory. 634 Cc["@mozilla.org/uriloader/external-protocol-service;1"] 635 .getService(Ci.nsIExternalProtocolService) 636 .loadURI( 637 lazy.NetUtil.newURI(aDirectory), 638 Services.scriptSecurityManager.getSystemPrincipal() 639 ); 640 } 641 }, 642 643 /** 644 * Displays an alert message box which asks the user if they want to 645 * unblock the downloaded file or not. 646 * 647 * @param options 648 * An object with the following properties: 649 * { 650 * verdict: 651 * The detailed reason why the download was blocked, according to 652 * the "Downloads.Error.BLOCK_VERDICT_" constants. If an unknown 653 * reason is specified, "Downloads.Error.BLOCK_VERDICT_MALWARE" is 654 * assumed. 655 * becauseBlockedByReputationCheck: 656 * Whether the the download was blocked by a reputation check. 657 * window: 658 * The window with which this action is associated. 659 * dialogType: 660 * String that determines which actions are available: 661 * - "unblock" to offer just "unblock". 662 * - "chooseUnblock" to offer "unblock" and "confirmBlock". 663 * - "chooseOpen" to offer "open" and "confirmBlock". 664 * } 665 * 666 * @returns {Promise<string>} 667 * Resolves to a string representing the action that should be executed: 668 * - "open" to allow the download and open the file. 669 * - "unblock" to allow the download without opening the file. 670 * - "confirmBlock" to delete the blocked data permanently. 671 * - "cancel" to do nothing and cancel the operation. 672 */ 673 async confirmUnblockDownload({ 674 verdict, 675 becauseBlockedByReputationCheck, 676 window, 677 dialogType, 678 }) { 679 let s = DownloadsCommon.strings; 680 681 // All the dialogs have an action button and a cancel button, while only 682 // some of them have an additonal button to remove the file. The cancel 683 // button must always be the one at BUTTON_POS_1 because this is the value 684 // returned by confirmEx when using ESC or closing the dialog (bug 345067). 685 let title = s.unblockHeaderUnblock; 686 let firstButtonText = s.unblockButtonUnblock; 687 let firstButtonAction = "unblock"; 688 let buttonFlags = 689 Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0 + 690 Ci.nsIPrompt.BUTTON_TITLE_CANCEL * Ci.nsIPrompt.BUTTON_POS_1; 691 692 switch (dialogType) { 693 case "unblock": 694 // Use only the unblock action. The default is to cancel. 695 buttonFlags += Ci.nsIPrompt.BUTTON_POS_1_DEFAULT; 696 break; 697 case "chooseUnblock": 698 // Use the unblock and remove file actions. The default is remove file. 699 buttonFlags += 700 Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2 + 701 Ci.nsIPrompt.BUTTON_POS_2_DEFAULT; 702 break; 703 case "chooseOpen": 704 // Use the unblock and open file actions. The default is open file. 705 title = s.unblockHeaderOpen; 706 firstButtonText = s.unblockButtonOpen; 707 firstButtonAction = "open"; 708 buttonFlags += 709 Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2 + 710 Ci.nsIPrompt.BUTTON_POS_0_DEFAULT; 711 break; 712 default: 713 console.error("Unexpected dialog type: " + dialogType); 714 return "cancel"; 715 } 716 717 let message; 718 let tip = s.unblockTip2; 719 switch (verdict) { 720 case lazy.Downloads.Error.BLOCK_VERDICT_UNCOMMON: 721 message = s.unblockTypeUncommon2; 722 break; 723 case lazy.Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED: 724 if (becauseBlockedByReputationCheck) { 725 message = s.unblockTypePotentiallyUnwanted2; 726 } else { 727 message = s.unblockTypeContentAnalysisWarn; 728 tip = s.unblockContentAnalysisTip; 729 } 730 break; 731 case lazy.Downloads.Error.BLOCK_VERDICT_INSECURE: 732 message = s.unblockInsecure2; 733 break; 734 default: 735 // Assume Downloads.Error.BLOCK_VERDICT_MALWARE 736 message = s.unblockTypeMalware; 737 break; 738 } 739 message += "\n\n" + tip; 740 741 Services.ww.registerNotification(function onOpen(subj, topic) { 742 if (topic == "domwindowopened" && subj instanceof Ci.nsIDOMWindow) { 743 // Make sure to listen for "DOMContentLoaded" because it is fired 744 // before the "load" event. 745 subj.addEventListener( 746 "DOMContentLoaded", 747 function () { 748 if ( 749 subj.document.documentURI == 750 "chrome://global/content/commonDialog.xhtml" 751 ) { 752 Services.ww.unregisterNotification(onOpen); 753 let dialog = subj.document.getElementById("commonDialog"); 754 if (dialog) { 755 // Change the dialog to use a warning icon. 756 dialog.classList.add("alert-dialog"); 757 } 758 } 759 }, 760 { once: true } 761 ); 762 } 763 }); 764 765 let rv = Services.prompt.confirmEx( 766 window, 767 title, 768 message, 769 buttonFlags, 770 firstButtonText, 771 null, 772 s.unblockButtonConfirmBlock, 773 null, 774 {} 775 ); 776 return [firstButtonAction, "cancel", "confirmBlock"][rv]; 777 }, 778 }; 779 780 ChromeUtils.defineLazyGetter(DownloadsCommon, "log", () => { 781 return lazy.DownloadsLogger.log.bind(lazy.DownloadsLogger); 782 }); 783 ChromeUtils.defineLazyGetter(DownloadsCommon, "error", () => { 784 return lazy.DownloadsLogger.error.bind(lazy.DownloadsLogger); 785 }); 786 787 // DownloadsData 788 789 /** 790 * Retrieves the list of past and completed downloads from the underlying 791 * Downloads API data, and provides asynchronous notifications allowing to 792 * build a consistent view of the available data. 793 * 794 * Note that using this object does not automatically initialize the list of 795 * downloads. This is useful to display a neutral progress indicator in 796 * the main browser window until the autostart timeout elapses. 797 * 798 * This powers the DownloadsData, PrivateDownloadsData, and HistoryDownloadsData 799 * singleton objects. 800 */ 801 function DownloadsDataCtor({ isPrivate, isHistory, maxHistoryResults } = {}) { 802 this._isPrivate = !!isPrivate; 803 804 // Contains all the available Download objects and their integer state. 805 this._oldDownloadStates = new WeakMap(); 806 807 // For the history downloads list we don't need to register this as a view, 808 // but we have to ensure that the DownloadsData object is initialized before 809 // we register more views. This ensures that the view methods of DownloadsData 810 // are invoked before those of views registered on HistoryDownloadsData, 811 // allowing the endTime property to be set correctly. 812 if (isHistory) { 813 if (isPrivate) { 814 lazy.PrivateDownloadsData.initializeDataLink(); 815 } 816 lazy.DownloadsData.initializeDataLink(); 817 this._promiseList = lazy.DownloadsData._promiseList.then(() => { 818 // For history downloads in Private Browsing mode, we'll fetch the combined 819 // list of public and private downloads. 820 return lazy.DownloadHistory.getList({ 821 type: isPrivate ? lazy.Downloads.ALL : lazy.Downloads.PUBLIC, 822 maxHistoryResults, 823 }); 824 }); 825 return; 826 } 827 828 // This defines "initializeDataLink" and "_promiseList" synchronously, then 829 // continues execution only when "initializeDataLink" is called, allowing the 830 // underlying data to be loaded only when actually needed. 831 this._promiseList = (async () => { 832 await new Promise(resolve => (this.initializeDataLink = resolve)); 833 let list = await lazy.Downloads.getList( 834 isPrivate ? lazy.Downloads.PRIVATE : lazy.Downloads.PUBLIC 835 ); 836 list.addView(this); 837 return list; 838 })(); 839 } 840 841 DownloadsDataCtor.prototype = { 842 /** 843 * Starts receiving events for current downloads. 844 */ 845 initializeDataLink() {}, 846 847 /** 848 * Promise resolved with the underlying DownloadList object once we started 849 * receiving events for current downloads. 850 */ 851 _promiseList: null, 852 853 /** 854 * Iterator for all the available Download objects. This is empty until the 855 * data has been loaded using the JavaScript API for downloads. 856 */ 857 get _downloads() { 858 return ChromeUtils.nondeterministicGetWeakMapKeys(this._oldDownloadStates); 859 }, 860 861 /** 862 * True if there are finished downloads that can be removed from the list. 863 */ 864 get canRemoveFinished() { 865 for (let download of this._downloads) { 866 // Stopped, paused, and failed downloads with partial data are removed. 867 if (download.stopped && !(download.canceled && download.hasPartialData)) { 868 return true; 869 } 870 } 871 return false; 872 }, 873 874 /** 875 * Asks the back-end to remove finished downloads from the list. This method 876 * is only called after the data link has been initialized. 877 */ 878 removeFinished() { 879 lazy.Downloads.getList( 880 this._isPrivate ? lazy.Downloads.PRIVATE : lazy.Downloads.PUBLIC 881 ) 882 .then(list => list.removeFinished()) 883 .catch(console.error); 884 }, 885 886 // Integration with the asynchronous Downloads back-end 887 888 onDownloadAdded(download) { 889 // Download objects do not store the end time of downloads, as the Downloads 890 // API does not need to persist this information for all platforms. Once a 891 // download terminates on a Desktop browser, it becomes a history download, 892 // for which the end time is stored differently, as a Places annotation. 893 download.endTime = Date.now(); 894 895 this._oldDownloadStates.set( 896 download, 897 DownloadsCommon.stateOfDownload(download) 898 ); 899 if ( 900 download.error?.becauseBlockedByReputationCheck || 901 download.error?.becauseBlockedByContentAnalysis 902 ) { 903 this._notifyDownloadEvent("error"); 904 } 905 }, 906 907 onDownloadChanged(download) { 908 let oldState = this._oldDownloadStates.get(download); 909 let newState = DownloadsCommon.stateOfDownload(download); 910 this._oldDownloadStates.set(download, newState); 911 912 if (oldState != newState) { 913 if ( 914 download.succeeded || 915 (download.canceled && !download.hasPartialData) || 916 download.error 917 ) { 918 // Store the end time that may be displayed by the views. 919 download.endTime = Date.now(); 920 921 // This state transition code should actually be located in a Downloads 922 // API module (bug 941009). 923 lazy.DownloadHistory.updateMetaData(download).catch(console.error); 924 } 925 926 if ( 927 download.succeeded || 928 (download.error && download.error.becauseBlocked) 929 ) { 930 this._notifyDownloadEvent("finish"); 931 } 932 } 933 934 if (!download.newDownloadNotified) { 935 download.newDownloadNotified = true; 936 this._notifyDownloadEvent("start", { 937 openDownloadsListOnStart: download.openDownloadsListOnStart, 938 }); 939 } 940 }, 941 942 onDownloadRemoved(download) { 943 this._oldDownloadStates.delete(download); 944 }, 945 946 // Registration of views 947 948 /** 949 * Adds an object to be notified when the available download data changes. 950 * The specified object is initialized with the currently available downloads. 951 * 952 * @param aView 953 * DownloadsView object to be added. This reference must be passed to 954 * removeView before termination. 955 */ 956 addView(aView) { 957 this._promiseList.then(list => list.addView(aView)).catch(console.error); 958 }, 959 960 /** 961 * Removes an object previously added using addView. 962 * 963 * @param aView 964 * DownloadsView object to be removed. 965 */ 966 removeView(aView) { 967 this._promiseList.then(list => list.removeView(aView)).catch(console.error); 968 }, 969 970 // Notifications sent to the most recent browser window only 971 972 /** 973 * Set to true after the first download causes the downloads panel to be 974 * displayed. 975 */ 976 get panelHasShownBefore() { 977 try { 978 return Services.prefs.getBoolPref("browser.download.panel.shown"); 979 } catch (ex) {} 980 return false; 981 }, 982 983 set panelHasShownBefore(aValue) { 984 Services.prefs.setBoolPref("browser.download.panel.shown", aValue); 985 }, 986 987 /** 988 * Displays a new or finished download notification in the most recent browser 989 * window, if one is currently available with the required privacy type. 990 * 991 * @param {string} aType 992 * Set to "start" for new downloads, "finish" for completed downloads, 993 * "error" for downloads that failed and need attention 994 * @param {boolean} [openDownloadsListOnStart] 995 * (Only relevant when aType = "start") 996 * true (default) - open the downloads panel. 997 * false - only show an indicator notification. 998 */ 999 _notifyDownloadEvent(aType, { openDownloadsListOnStart = true } = {}) { 1000 DownloadsCommon.log( 1001 "Attempting to notify that a new download has started or finished." 1002 ); 1003 1004 // Show the panel in the most recent browser window, if present. 1005 let browserWin = lazy.BrowserWindowTracker.getTopWindow({ 1006 private: this._isPrivate, 1007 allowFromInactiveWorkspace: true, 1008 }); 1009 if (!browserWin) { 1010 return; 1011 } 1012 1013 let shouldOpenDownloadsPanel = 1014 aType == "start" && 1015 DownloadsCommon.summarizeDownloads(this._downloads).numDownloading <= 1 && 1016 lazy.gAlwaysOpenPanel; 1017 1018 // For new downloads after the first one, don't show the panel 1019 // automatically, but provide a visible notification in the topmost browser 1020 // window, if the status indicator is already visible. Also ensure that if 1021 // openDownloadsListOnStart = false is passed, we always skip opening the 1022 // panel. That's because this will only be passed if the download is started 1023 // without user interaction or if a dialog was previously opened in the 1024 // process of the download (e.g. unknown content type dialog). 1025 if ( 1026 aType != "error" && 1027 ((this.panelHasShownBefore && !shouldOpenDownloadsPanel) || 1028 !openDownloadsListOnStart || 1029 browserWin != Services.focus.activeWindow) 1030 ) { 1031 DownloadsCommon.log("Showing new download notification."); 1032 browserWin.DownloadsIndicatorView.showEventNotification(aType); 1033 return; 1034 } 1035 this.panelHasShownBefore = true; 1036 browserWin.DownloadsPanel.showPanel(); 1037 }, 1038 }; 1039 1040 ChromeUtils.defineLazyGetter(lazy, "HistoryDownloadsData", function () { 1041 return new DownloadsDataCtor({ isHistory: true }); 1042 }); 1043 1044 ChromeUtils.defineLazyGetter(lazy, "LimitedHistoryDownloadsData", function () { 1045 return new DownloadsDataCtor({ 1046 isHistory: true, 1047 maxHistoryResults: kMaxHistoryResultsForLimitedView, 1048 }); 1049 }); 1050 1051 ChromeUtils.defineLazyGetter( 1052 lazy, 1053 "LimitedPrivateHistoryDownloadData", 1054 function () { 1055 return new DownloadsDataCtor({ 1056 isPrivate: true, 1057 isHistory: true, 1058 maxHistoryResults: kMaxHistoryResultsForLimitedView, 1059 }); 1060 } 1061 ); 1062 1063 ChromeUtils.defineLazyGetter(lazy, "PrivateDownloadsData", function () { 1064 return new DownloadsDataCtor({ isPrivate: true }); 1065 }); 1066 1067 ChromeUtils.defineLazyGetter(lazy, "DownloadsData", function () { 1068 return new DownloadsDataCtor(); 1069 }); 1070 1071 // DownloadsViewPrototype 1072 1073 /** 1074 * A prototype for an object that registers itself with DownloadsData as soon 1075 * as a view is registered with it. 1076 */ 1077 const DownloadsViewPrototype = { 1078 /** 1079 * Contains all the available Download objects and their current state value. 1080 * 1081 * SUBCLASSES MUST OVERRIDE THIS PROPERTY. 1082 */ 1083 _oldDownloadStates: null, 1084 1085 // Registration of views 1086 1087 /** 1088 * Array of view objects that should be notified when the available status 1089 * data changes. 1090 * 1091 * SUBCLASSES MUST OVERRIDE THIS PROPERTY. 1092 */ 1093 _views: null, 1094 1095 /** 1096 * Determines whether this view object is over the private or non-private 1097 * downloads. 1098 * 1099 * SUBCLASSES MUST OVERRIDE THIS PROPERTY. 1100 */ 1101 _isPrivate: false, 1102 1103 /** 1104 * Adds an object to be notified when the available status data changes. 1105 * The specified object is initialized with the currently available status. 1106 * 1107 * @param aView 1108 * View object to be added. This reference must be 1109 * passed to removeView before termination. 1110 */ 1111 addView(aView) { 1112 // Start receiving events when the first of our views is registered. 1113 if (!this._views.length) { 1114 if (this._isPrivate) { 1115 lazy.PrivateDownloadsData.addView(this); 1116 } else { 1117 lazy.DownloadsData.addView(this); 1118 } 1119 } 1120 1121 this._views.push(aView); 1122 this.refreshView(aView); 1123 }, 1124 1125 /** 1126 * Updates the properties of an object previously added using addView. 1127 * 1128 * @param aView 1129 * View object to be updated. 1130 */ 1131 refreshView(aView) { 1132 // Update immediately even if we are still loading data asynchronously. 1133 // Subclasses must provide these two functions! 1134 this._refreshProperties(); 1135 this._updateView(aView); 1136 }, 1137 1138 /** 1139 * Removes an object previously added using addView. 1140 * 1141 * @param aView 1142 * View object to be removed. 1143 */ 1144 removeView(aView) { 1145 let index = this._views.indexOf(aView); 1146 if (index != -1) { 1147 this._views.splice(index, 1); 1148 } 1149 1150 // Stop receiving events when the last of our views is unregistered. 1151 if (!this._views.length) { 1152 if (this._isPrivate) { 1153 lazy.PrivateDownloadsData.removeView(this); 1154 } else { 1155 lazy.DownloadsData.removeView(this); 1156 } 1157 } 1158 }, 1159 1160 // Callback functions from DownloadList 1161 1162 /** 1163 * Indicates whether we are still loading downloads data asynchronously. 1164 */ 1165 _loading: false, 1166 1167 /** 1168 * Called before multiple downloads are about to be loaded. 1169 */ 1170 onDownloadBatchStarting() { 1171 this._loading = true; 1172 }, 1173 1174 /** 1175 * Called after data loading finished. 1176 */ 1177 onDownloadBatchEnded() { 1178 this._loading = false; 1179 this._updateViews(); 1180 }, 1181 1182 /** 1183 * Called when a new download data item is available, either during the 1184 * asynchronous data load or when a new download is started. 1185 * 1186 * @param download 1187 * Download object that was just added. 1188 * 1189 * Note: Subclasses should override this and still call the base method. 1190 */ 1191 onDownloadAdded(download) { 1192 this._oldDownloadStates.set( 1193 download, 1194 DownloadsCommon.stateOfDownload(download) 1195 ); 1196 }, 1197 1198 /** 1199 * Called when the overall state of a Download has changed. In particular, 1200 * this is called only once when the download succeeds or is blocked 1201 * permanently, and is never called if only the current progress changed. 1202 * 1203 * The onDownloadChanged notification will always be sent afterwards. 1204 * 1205 * Note: Subclasses should override this. 1206 */ 1207 onDownloadStateChanged() { 1208 throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); 1209 }, 1210 1211 /** 1212 * Called every time any state property of a Download may have changed, 1213 * including progress properties. 1214 * 1215 * Note that progress notification changes are throttled at the Downloads.sys.mjs 1216 * API level, and there is no throttling mechanism in the front-end. 1217 * 1218 * Note: Subclasses should override this and still call the base method. 1219 */ 1220 onDownloadChanged(download) { 1221 let oldState = this._oldDownloadStates.get(download); 1222 let newState = DownloadsCommon.stateOfDownload(download); 1223 this._oldDownloadStates.set(download, newState); 1224 1225 if (oldState != newState) { 1226 this.onDownloadStateChanged(download); 1227 } 1228 }, 1229 1230 /** 1231 * Called when a data item is removed, ensures that the widget associated with 1232 * the view item is removed from the user interface. 1233 * 1234 * @param download 1235 * Download object that is being removed. 1236 * 1237 * Note: Subclasses should override this. 1238 */ 1239 onDownloadRemoved(download) { 1240 this._oldDownloadStates.delete(download); 1241 }, 1242 1243 /** 1244 * Private function used to refresh the internal properties being sent to 1245 * each registered view. 1246 * 1247 * Note: Subclasses should override this. 1248 */ 1249 _refreshProperties() { 1250 throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); 1251 }, 1252 1253 /** 1254 * Private function used to refresh an individual view. 1255 * 1256 * Note: Subclasses should override this. 1257 */ 1258 _updateView() { 1259 throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); 1260 }, 1261 1262 /** 1263 * Computes aggregate values and propagates the changes to our views. 1264 */ 1265 _updateViews() { 1266 // Do not update the status indicators during batch loads of download items. 1267 if (this._loading) { 1268 return; 1269 } 1270 1271 this._refreshProperties(); 1272 this._views.forEach(this._updateView, this); 1273 }, 1274 }; 1275 1276 // DownloadsIndicatorData 1277 1278 /** 1279 * This object registers itself with DownloadsData as a view, and transforms the 1280 * notifications it receives into overall status data, that is then broadcast to 1281 * the registered download status indicators. 1282 * 1283 * Note that using this object does not automatically start the Download Manager 1284 * service. Consumers will see an empty list of downloads until the service is 1285 * actually started. This is useful to display a neutral progress indicator in 1286 * the main browser window until the autostart timeout elapses. 1287 */ 1288 function DownloadsIndicatorDataCtor(aPrivate) { 1289 this._oldDownloadStates = new WeakMap(); 1290 this._isPrivate = aPrivate; 1291 this._views = []; 1292 } 1293 DownloadsIndicatorDataCtor.prototype = { 1294 /** 1295 * Map of the relative severities of different attention states. 1296 * Used in sorting the map of active downloads' attention states 1297 * to determine the attention state to be displayed. 1298 */ 1299 _attentionPriority: new Map([ 1300 [DownloadsCommon.ATTENTION_NONE, 0], 1301 [DownloadsCommon.ATTENTION_SUCCESS, 1], 1302 [DownloadsCommon.ATTENTION_INFO, 2], 1303 [DownloadsCommon.ATTENTION_WARNING, 3], 1304 [DownloadsCommon.ATTENTION_SEVERE, 4], 1305 ]), 1306 1307 /** 1308 * Iterator for all the available Download objects. This is empty until the 1309 * data has been loaded using the JavaScript API for downloads. 1310 */ 1311 get _downloads() { 1312 return ChromeUtils.nondeterministicGetWeakMapKeys(this._oldDownloadStates); 1313 }, 1314 1315 /** 1316 * Removes an object previously added using addView. 1317 * 1318 * @param aView 1319 * DownloadsIndicatorView object to be removed. 1320 */ 1321 removeView(aView) { 1322 DownloadsViewPrototype.removeView.call(this, aView); 1323 1324 if (!this._views.length) { 1325 this._itemCount = 0; 1326 } 1327 }, 1328 1329 onDownloadAdded(download) { 1330 DownloadsViewPrototype.onDownloadAdded.call(this, download); 1331 this._itemCount++; 1332 this._updateViews(); 1333 }, 1334 1335 onDownloadStateChanged(download) { 1336 if (this._attentionSuppressed !== DownloadsCommon.SUPPRESS_NONE) { 1337 return; 1338 } 1339 let attention; 1340 if ( 1341 !download.succeeded && 1342 download.error && 1343 download.error.reputationCheckVerdict 1344 ) { 1345 switch (download.error.reputationCheckVerdict) { 1346 case lazy.Downloads.Error.BLOCK_VERDICT_UNCOMMON: 1347 attention = DownloadsCommon.ATTENTION_INFO; 1348 break; 1349 case lazy.Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED: // fall-through 1350 case lazy.Downloads.Error.BLOCK_VERDICT_INSECURE: 1351 case lazy.Downloads.Error.BLOCK_VERDICT_DOWNLOAD_SPAM: 1352 attention = DownloadsCommon.ATTENTION_WARNING; 1353 break; 1354 case lazy.Downloads.Error.BLOCK_VERDICT_MALWARE: 1355 attention = DownloadsCommon.ATTENTION_SEVERE; 1356 break; 1357 default: 1358 attention = DownloadsCommon.ATTENTION_SEVERE; 1359 console.error( 1360 "Unknown reputation verdict: " + 1361 download.error.reputationCheckVerdict 1362 ); 1363 } 1364 } else if (download.succeeded) { 1365 attention = DownloadsCommon.ATTENTION_SUCCESS; 1366 } else if (download.error) { 1367 attention = DownloadsCommon.ATTENTION_WARNING; 1368 } 1369 download.attention = attention; 1370 this.updateAttention(); 1371 }, 1372 1373 onDownloadChanged(download) { 1374 DownloadsViewPrototype.onDownloadChanged.call(this, download); 1375 this._updateViews(); 1376 }, 1377 1378 onDownloadRemoved(download) { 1379 DownloadsViewPrototype.onDownloadRemoved.call(this, download); 1380 this._itemCount--; 1381 this.updateAttention(); 1382 this._updateViews(); 1383 }, 1384 1385 // Propagation of properties to our views 1386 1387 // The following properties are updated by _refreshProperties and are then 1388 // propagated to the views. See _refreshProperties for details. 1389 _hasDownloads: false, 1390 _percentComplete: -1, 1391 1392 /** 1393 * Indicates whether the download indicators should be highlighted. 1394 */ 1395 set attention(aValue) { 1396 this._attention = aValue; 1397 this._updateViews(); 1398 }, 1399 _attention: DownloadsCommon.ATTENTION_NONE, 1400 1401 /** 1402 * Indicates whether the user is interacting with downloads, thus the 1403 * attention indication should not be shown even if requested. 1404 */ 1405 set attentionSuppressed(aFlags) { 1406 this._attentionSuppressed = aFlags; 1407 if (aFlags !== DownloadsCommon.SUPPRESS_NONE) { 1408 for (let download of this._downloads) { 1409 download.attention = DownloadsCommon.ATTENTION_NONE; 1410 } 1411 this.attention = DownloadsCommon.ATTENTION_NONE; 1412 } 1413 }, 1414 get attentionSuppressed() { 1415 return this._attentionSuppressed; 1416 }, 1417 _attentionSuppressed: DownloadsCommon.SUPPRESS_NONE, 1418 1419 /** 1420 * Set the indicator's attention to the most severe attention state among the 1421 * unseen displayed downloads, or DownloadsCommon.ATTENTION_NONE if empty. 1422 */ 1423 updateAttention() { 1424 let currentAttention = DownloadsCommon.ATTENTION_NONE; 1425 let currentPriority = 0; 1426 for (let download of this._downloads) { 1427 let { attention } = download; 1428 let priority = this._attentionPriority.get(attention); 1429 if (priority > currentPriority) { 1430 currentPriority = priority; 1431 currentAttention = attention; 1432 } 1433 } 1434 this.attention = currentAttention; 1435 }, 1436 1437 /** 1438 * Updates the specified view with the current aggregate values. 1439 * 1440 * @param aView 1441 * DownloadsIndicatorView object to be updated. 1442 */ 1443 _updateView(aView) { 1444 aView.hasDownloads = this._hasDownloads; 1445 aView.percentComplete = this._percentComplete; 1446 aView.attention = 1447 this.attentionSuppressed !== DownloadsCommon.SUPPRESS_NONE 1448 ? DownloadsCommon.ATTENTION_NONE 1449 : this._attention; 1450 }, 1451 1452 // Property updating based on current download status 1453 1454 /** 1455 * Number of download items that are available to be displayed. 1456 */ 1457 _itemCount: 0, 1458 1459 /** 1460 * A generator function for the Download objects this summary is currently 1461 * interested in. This generator is passed off to summarizeDownloads in order 1462 * to generate statistics about the downloads we care about - in this case, 1463 * it's all active downloads. 1464 */ 1465 *_activeDownloads() { 1466 let downloads = this._isPrivate 1467 ? lazy.PrivateDownloadsData._downloads 1468 : lazy.DownloadsData._downloads; 1469 for (let download of downloads) { 1470 if ( 1471 download.isInCurrentBatch || 1472 (download.canceled && download.hasPartialData) 1473 ) { 1474 yield download; 1475 } 1476 } 1477 }, 1478 1479 /** 1480 * Computes aggregate values based on the current state of downloads. 1481 */ 1482 _refreshProperties() { 1483 let summary = DownloadsCommon.summarizeDownloads(this._activeDownloads()); 1484 1485 // Determine if the indicator should be shown or get attention. 1486 this._hasDownloads = this._itemCount > 0; 1487 1488 // Always show a progress bar if there are downloads in progress. 1489 if (summary.percentComplete >= 0) { 1490 this._percentComplete = summary.percentComplete; 1491 } else if (summary.numDownloading > 0) { 1492 this._percentComplete = 0; 1493 } else { 1494 this._percentComplete = -1; 1495 } 1496 }, 1497 }; 1498 Object.setPrototypeOf( 1499 DownloadsIndicatorDataCtor.prototype, 1500 DownloadsViewPrototype 1501 ); 1502 1503 ChromeUtils.defineLazyGetter( 1504 lazy, 1505 "PrivateDownloadsIndicatorData", 1506 function () { 1507 return new DownloadsIndicatorDataCtor(true); 1508 } 1509 ); 1510 1511 ChromeUtils.defineLazyGetter(lazy, "DownloadsIndicatorData", function () { 1512 return new DownloadsIndicatorDataCtor(false); 1513 }); 1514 1515 // DownloadsSummaryData 1516 1517 /** 1518 * DownloadsSummaryData is a view for DownloadsData that produces a summary 1519 * of all downloads after a certain exclusion point aNumToExclude. For example, 1520 * if there were 5 downloads in progress, and a DownloadsSummaryData was 1521 * constructed with aNumToExclude equal to 3, then that DownloadsSummaryData 1522 * would produce a summary of the last 2 downloads. 1523 * 1524 * @param aIsPrivate 1525 * True if the browser window which owns the download button is a private 1526 * window. 1527 * @param aNumToExclude 1528 * The number of items to exclude from the summary, starting from the 1529 * top of the list. 1530 */ 1531 function DownloadsSummaryData(aIsPrivate, aNumToExclude) { 1532 this._numToExclude = aNumToExclude; 1533 // Since we can have multiple instances of DownloadsSummaryData, we 1534 // override these values from the prototype so that each instance can be 1535 // completely separated from one another. 1536 this._loading = false; 1537 1538 this._downloads = []; 1539 1540 // Floating point value indicating the last number of seconds estimated until 1541 // the longest download will finish. We need to store this value so that we 1542 // don't continuously apply smoothing if the actual download state has not 1543 // changed. This is set to -1 if the previous value is unknown. 1544 this._lastRawTimeLeft = -1; 1545 1546 // Last number of seconds estimated until all in-progress downloads with a 1547 // known size and speed will finish. This value is stored to allow smoothing 1548 // in case of small variations. This is set to -1 if the previous value is 1549 // unknown. 1550 this._lastTimeLeft = -1; 1551 1552 // The following properties are updated by _refreshProperties and are then 1553 // propagated to the views. 1554 this._showingProgress = false; 1555 this._details = ""; 1556 this._description = ""; 1557 this._numActive = 0; 1558 this._percentComplete = -1; 1559 1560 this._oldDownloadStates = new WeakMap(); 1561 this._isPrivate = aIsPrivate; 1562 this._views = []; 1563 } 1564 1565 DownloadsSummaryData.prototype = { 1566 /** 1567 * Removes an object previously added using addView. 1568 * 1569 * @param aView 1570 * DownloadsSummary view to be removed. 1571 */ 1572 removeView(aView) { 1573 DownloadsViewPrototype.removeView.call(this, aView); 1574 1575 if (!this._views.length) { 1576 // Clear out our collection of Download objects. If we ever have 1577 // another view registered with us, this will get re-populated. 1578 this._downloads = []; 1579 } 1580 }, 1581 1582 onDownloadAdded(download) { 1583 DownloadsViewPrototype.onDownloadAdded.call(this, download); 1584 this._downloads.unshift(download); 1585 this._updateViews(); 1586 }, 1587 1588 onDownloadStateChanged() { 1589 // Since the state of a download changed, reset the estimated time left. 1590 this._lastRawTimeLeft = -1; 1591 this._lastTimeLeft = -1; 1592 }, 1593 1594 onDownloadChanged(download) { 1595 DownloadsViewPrototype.onDownloadChanged.call(this, download); 1596 this._updateViews(); 1597 }, 1598 1599 onDownloadRemoved(download) { 1600 DownloadsViewPrototype.onDownloadRemoved.call(this, download); 1601 let itemIndex = this._downloads.indexOf(download); 1602 this._downloads.splice(itemIndex, 1); 1603 this._updateViews(); 1604 }, 1605 1606 // Propagation of properties to our views 1607 1608 /** 1609 * Updates the specified view with the current aggregate values. 1610 * 1611 * @param aView 1612 * DownloadsIndicatorView object to be updated. 1613 */ 1614 _updateView(aView) { 1615 aView.showingProgress = this._showingProgress; 1616 aView.percentComplete = this._percentComplete; 1617 aView.description = this._description; 1618 aView.details = this._details; 1619 }, 1620 1621 // Property updating based on current download status 1622 1623 /** 1624 * A generator function for the Download objects this summary is currently 1625 * interested in. This generator is passed off to summarizeDownloads in order 1626 * to generate statistics about the downloads we care about - in this case, 1627 * it's the downloads in this._downloads after the first few to exclude, 1628 * which was set when constructing this DownloadsSummaryData instance. 1629 */ 1630 *_downloadsForSummary() { 1631 if (this._downloads.length) { 1632 for (let i = this._numToExclude; i < this._downloads.length; ++i) { 1633 yield this._downloads[i]; 1634 } 1635 } 1636 }, 1637 1638 /** 1639 * Computes aggregate values based on the current state of downloads. 1640 */ 1641 _refreshProperties() { 1642 // Pre-load summary with default values. 1643 let summary = DownloadsCommon.summarizeDownloads( 1644 this._downloadsForSummary() 1645 ); 1646 1647 // Run sync to update view right away and get correct description. 1648 // See refreshView for more details. 1649 this._description = kDownloadsFluentStrings.formatValueSync( 1650 "downloads-more-downloading", 1651 { 1652 count: summary.numDownloading, 1653 } 1654 ); 1655 this._percentComplete = summary.percentComplete; 1656 1657 // Only show the downloading items. 1658 this._showingProgress = summary.numDownloading > 0; 1659 1660 // Display the estimated time left, if present. 1661 if (summary.rawTimeLeft == -1) { 1662 // There are no downloads with a known time left. 1663 this._lastRawTimeLeft = -1; 1664 this._lastTimeLeft = -1; 1665 this._details = ""; 1666 } else { 1667 // Compute the new time left only if state actually changed. 1668 if (this._lastRawTimeLeft != summary.rawTimeLeft) { 1669 this._lastRawTimeLeft = summary.rawTimeLeft; 1670 this._lastTimeLeft = DownloadsCommon.smoothSeconds( 1671 summary.rawTimeLeft, 1672 this._lastTimeLeft 1673 ); 1674 } 1675 [this._details] = lazy.DownloadUtils.getDownloadStatusNoRate( 1676 summary.totalTransferred, 1677 summary.totalSize, 1678 summary.slowestSpeed, 1679 this._lastTimeLeft 1680 ); 1681 } 1682 }, 1683 }; 1684 Object.setPrototypeOf(DownloadsSummaryData.prototype, DownloadsViewPrototype);