diff --git a/browser/components/downloads/DownloadsCommon.jsm b/browser/components/downloads/DownloadsCommon.jsm index fcd8bcc25f37..27a15c8a8919 100644 --- a/browser/components/downloads/DownloadsCommon.jsm +++ b/browser/components/downloads/DownloadsCommon.jsm @@ -140,13 +140,6 @@ PrefObserver.register({ * and provides shared methods for all the instances of the user interface. */ this.DownloadsCommon = { - /** - * Constants with the different types of unblock messages. - */ - BLOCK_VERDICT_MALWARE: "Malware", - BLOCK_VERDICT_POTENTIALLY_UNWANTED: "PotentiallyUnwanted", - BLOCK_VERDICT_UNCOMMON: "Uncommon", - /** * Returns an object whose keys are the string names from the downloads string * bundle, and whose values are either the translated strings or functions @@ -528,15 +521,17 @@ this.DownloadsCommon = { * Displays an alert message box which asks the user if they want to * unblock the downloaded file or not. * - * @param aType - * The type of malware the downloaded file contains. + * @param aVerdict + * The detailed reason why the download was blocked, according to the + * "Downloads.Error.BLOCK_VERDICT_" constants. If an unknown reason is + * specified, "Downloads.Error.BLOCK_VERDICT_MALWARE" is assumed. * @param aOwnerWindow * The window with which this action is associated. * * @return True to unblock the file, false to keep the user safe and * cancel the operation. */ - confirmUnblockDownload: Task.async(function* (aType, aOwnerWindow) { + confirmUnblockDownload: Task.async(function* (aVerdict, aOwnerWindow) { let s = DownloadsCommon.strings; let title = s.unblockHeader; let buttonFlags = (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) + @@ -547,15 +542,15 @@ this.DownloadsCommon = { let okButton = s.unblockButtonContinue; let cancelButton = s.unblockButtonCancel; - switch (aType) { - case this.BLOCK_VERDICT_MALWARE: - type = s.unblockTypeMalware; + switch (aVerdict) { + case Downloads.Error.BLOCK_VERDICT_UNCOMMON: + type = s.unblockTypeUncommon; break; - case this.BLOCK_VERDICT_POTENTIALLY_UNWANTED: + case Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED: type = s.unblockTypePotentiallyUnwanted; break; - case this.BLOCK_VERDICT_UNCOMMON: - type = s.unblockTypeUncommon; + default: // Assume Downloads.Error.BLOCK_VERDICT_MALWARE + type = s.unblockTypeMalware; break; } diff --git a/browser/components/downloads/content/allDownloadsViewOverlay.js b/browser/components/downloads/content/allDownloadsViewOverlay.js index f8c0ee63b1d7..9435c8be0ab8 100644 --- a/browser/components/downloads/content/allDownloadsViewOverlay.js +++ b/browser/components/downloads/content/allDownloadsViewOverlay.js @@ -372,8 +372,8 @@ HistoryDownloadElementShell.prototype = { }, downloadsCmd_unblock() { - DownloadsCommon.confirmUnblockDownload(DownloadsCommon.BLOCK_VERDICT_MALWARE, - window).then((confirmed) => { + let verdict = this.download.error.reputationCheckVerdict; + DownloadsCommon.confirmUnblockDownload(verdict, window).then(confirmed => { if (confirmed) { return this.download.unblock(); } diff --git a/browser/components/downloads/content/downloads.js b/browser/components/downloads/content/downloads.js index 632c886555a0..a50c118f70e5 100644 --- a/browser/components/downloads/content/downloads.js +++ b/browser/components/downloads/content/downloads.js @@ -1099,8 +1099,8 @@ DownloadsViewItem.prototype = { downloadsCmd_unblock() { DownloadsPanel.hidePanel(); - DownloadsCommon.confirmUnblockDownload(DownloadsCommon.BLOCK_VERDICT_MALWARE, - window).then((confirmed) => { + let verdict = this.download.error.reputationCheckVerdict; + DownloadsCommon.confirmUnblockDownload(verdict, window).then(confirmed => { if (confirmed) { return this.download.unblock(); } diff --git a/browser/components/downloads/test/browser/browser_confirm_unblock_download.js b/browser/components/downloads/test/browser/browser_confirm_unblock_download.js index b5ae3ec19240..a40d3f6fda8a 100644 --- a/browser/components/downloads/test/browser/browser_confirm_unblock_download.js +++ b/browser/components/downloads/test/browser/browser_confirm_unblock_download.js @@ -33,14 +33,14 @@ function addDialogOpenObserver(buttonAction) { add_task(function* test_confirm_unblock_dialog_unblock() { addDialogOpenObserver("accept"); - let result = yield DownloadsCommon.confirmUnblockDownload(DownloadsCommon.BLOCK_VERDICT_MALWARE, + let result = yield DownloadsCommon.confirmUnblockDownload(Downloads.Error.BLOCK_VERDICT_MALWARE, window); ok(result, "Should return true when the user clicks on `Unblock` button."); }); add_task(function* test_confirm_unblock_dialog_keep_safe() { addDialogOpenObserver("cancel"); - let result = yield DownloadsCommon.confirmUnblockDownload(DownloadsCommon.BLOCK_VERDICT_MALWARE, + let result = yield DownloadsCommon.confirmUnblockDownload(Downloads.Error.BLOCK_VERDICT_MALWARE, window); ok(!result, "Should return false when the user clicks on `Keep me safe` button."); }); diff --git a/toolkit/components/jsdownloads/src/DownloadCore.jsm b/toolkit/components/jsdownloads/src/DownloadCore.jsm index 7b2a508165aa..cd8cc5d94e81 100644 --- a/toolkit/components/jsdownloads/src/DownloadCore.jsm +++ b/toolkit/components/jsdownloads/src/DownloadCore.jsm @@ -1529,6 +1529,7 @@ this.DownloadError = function (aProperties) } else if (aProperties.becauseBlockedByReputationCheck) { this.becauseBlocked = true; this.becauseBlockedByReputationCheck = true; + this.reputationCheckVerdict = aProperties.reputationCheckVerdict || ""; } else if (aProperties.becauseBlockedByRuntimePermissions) { this.becauseBlocked = true; this.becauseBlockedByRuntimePermissions = true; @@ -1543,6 +1544,16 @@ this.DownloadError = function (aProperties) this.stack = new Error().stack; } +/** + * These constants are used by the reputationCheckVerdict property and indicate + * the detailed reason why a download is blocked. + * + * @note These values should not be changed because they can be serialized. + */ +this.DownloadError.BLOCK_VERDICT_MALWARE = "Malware"; +this.DownloadError.BLOCK_VERDICT_POTENTIALLY_UNWANTED = "PotentiallyUnwanted"; +this.DownloadError.BLOCK_VERDICT_UNCOMMON = "Uncommon"; + this.DownloadError.prototype = { __proto__: Error.prototype, @@ -1588,6 +1599,15 @@ this.DownloadError.prototype = { */ becauseBlockedByRuntimePermissions: false, + /** + * If becauseBlockedByReputationCheck is true, indicates the detailed reason + * why the download was blocked, according to the "BLOCK_VERDICT_" constants. + * + * If the download was not blocked or the reason for the block is unknown, + * this will be an empty string. + */ + reputationCheckVerdict: "", + /** * If this DownloadError was caused by an exception this property will * contain the original exception. This will not be serialized when saving @@ -1611,6 +1631,7 @@ this.DownloadError.prototype = { becauseBlockedByParentalControls: this.becauseBlockedByParentalControls, becauseBlockedByReputationCheck: this.becauseBlockedByReputationCheck, becauseBlockedByRuntimePermissions: this.becauseBlockedByRuntimePermissions, + reputationCheckVerdict: this.reputationCheckVerdict, }; serializeUnknownProperties(this, serializable); @@ -1636,7 +1657,8 @@ this.DownloadError.fromSerializable = function (aSerializable) { property != "becauseBlocked" && property != "becauseBlockedByParentalControls" && property != "becauseBlockedByReputationCheck" && - property != "becauseBlockedByRuntimePermissions"); + property != "becauseBlockedByRuntimePermissions" && + property != "reputationCheckVerdict"); return e; }; @@ -2148,7 +2170,9 @@ this.DownloadCopySaver.prototype = { let targetPath = this.download.target.path; let partFilePath = this.download.target.partFilePath; - if (yield DownloadIntegration.shouldBlockForReputationCheck(download)) { + let { shouldBlock, verdict } = + yield DownloadIntegration.shouldBlockForReputationCheck(download); + if (shouldBlock) { download.progress = 100; download.hasPartialData = false; @@ -2166,7 +2190,10 @@ this.DownloadCopySaver.prototype = { download.hasBlockedData = true; } - throw new DownloadError({ becauseBlockedByReputationCheck: true }); + throw new DownloadError({ + becauseBlockedByReputationCheck: true, + reputationCheckVerdict: verdict, + }); } if (partFilePath) { diff --git a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm index 839035b9d448..006cdbfd1798 100644 --- a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm +++ b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm @@ -125,6 +125,20 @@ const kObserverTopics = [ "xpcom-will-shutdown", ]; +/** + * Maps nsIApplicationReputationService verdicts with the DownloadError ones. + */ +const kVerdictMap = { + [Ci.nsIApplicationReputationService.VERDICT_DANGEROUS]: + Downloads.Error.BLOCK_VERDICT_MALWARE, + [Ci.nsIApplicationReputationService.VERDICT_UNCOMMON]: + Downloads.Error.BLOCK_VERDICT_UNCOMMON, + [Ci.nsIApplicationReputationService.VERDICT_POTENTIALLY_UNWANTED]: + Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED, + [Ci.nsIApplicationReputationService.VERDICT_DANGEROUS_HOST]: + Downloads.Error.BLOCK_VERDICT_MALWARE, +}; + //////////////////////////////////////////////////////////////////////////////// //// DownloadIntegration @@ -148,6 +162,7 @@ this.DownloadIntegration = { dontCheckApplicationReputation: true, #endif shouldBlockInTestForApplicationReputation: false, + verdictInTestForApplicationReputation: "", shouldKeepBlockedDataInTest: false, dontOpenFileAndFolder: false, downloadDoneCalled: false, @@ -531,11 +546,20 @@ this.DownloadIntegration = { * The download object. * * @return {Promise} - * @resolves The boolean indicates to block downloads or not. + * @resolves Object with the following properties: + * { + * shouldBlock: Whether the download should be blocked. + * verdict: Detailed reason for the block, according to the + * "Downloads.Error.BLOCK_VERDICT_" constants, or empty + * string if the reason is unknown. + * } */ shouldBlockForReputationCheck: function (aDownload) { if (this.dontCheckApplicationReputation) { - return Promise.resolve(this.shouldBlockInTestForApplicationReputation); + return Promise.resolve({ + shouldBlock: this.shouldBlockInTestForApplicationReputation, + verdict: this.verdictInTestForApplicationReputation, + }); } let hash; let sigInfo; @@ -546,10 +570,16 @@ this.DownloadIntegration = { channelRedirects = aDownload.saver.getRedirects(); } catch (ex) { // Bail if DownloadSaver doesn't have a hash or signature info. - return Promise.resolve(false); + return Promise.resolve({ + shouldBlock: false, + verdict: "", + }); } if (!hash || !sigInfo) { - return Promise.resolve(false); + return Promise.resolve({ + shouldBlock: false, + verdict: "", + }); } let deferred = Promise.defer(); let aReferrer = null; @@ -564,8 +594,11 @@ this.DownloadIntegration = { suggestedFileName: OS.Path.basename(aDownload.target.path), signatureInfo: sigInfo, redirects: channelRedirects }, - function onComplete(aShouldBlock, aRv) { - deferred.resolve(aShouldBlock); + function onComplete(aShouldBlock, aRv, aVerdict) { + deferred.resolve({ + shouldBlock: aShouldBlock, + verdict: (aShouldBlock && kVerdictMap[aVerdict]) || "", + }); }); return deferred.promise; }, diff --git a/toolkit/components/jsdownloads/test/unit/common_test_Download.js b/toolkit/components/jsdownloads/test/unit/common_test_Download.js index 8bedc32a627c..2bce0b36ea86 100644 --- a/toolkit/components/jsdownloads/test/unit/common_test_Download.js +++ b/toolkit/components/jsdownloads/test/unit/common_test_Download.js @@ -1711,12 +1711,15 @@ add_task(function* test_getSha256Hash() var promiseBlockedDownload = Task.async(function* (options) { function cleanup() { DownloadIntegration.shouldBlockInTestForApplicationReputation = false; + DownloadIntegration.verdictInTestForApplicationReputation = ""; DownloadIntegration.shouldKeepBlockedDataInTest = false; } do_register_cleanup(cleanup); let {keepPartialData, keepBlockedData} = options; DownloadIntegration.shouldBlockInTestForApplicationReputation = true; + DownloadIntegration.verdictInTestForApplicationReputation = + Downloads.Error.BLOCK_VERDICT_UNCOMMON; DownloadIntegration.shouldKeepBlockedDataInTest = keepBlockedData; let download; @@ -1740,7 +1743,11 @@ var promiseBlockedDownload = Task.async(function* (options) { throw ex; } do_check_true(ex.becauseBlockedByReputationCheck); + do_check_eq(ex.reputationCheckVerdict, + Downloads.Error.BLOCK_VERDICT_UNCOMMON); do_check_true(download.error.becauseBlockedByReputationCheck); + do_check_eq(download.error.reputationCheckVerdict, + Downloads.Error.BLOCK_VERDICT_UNCOMMON); } do_check_true(download.stopped);