Bug 1254100 - Part 2 - Downloads blocked by Application Reputation should provide information about the verdict. r=mak
MozReview-Commit-ID: FYH5Tdtbzn
--- a/browser/components/downloads/DownloadsCommon.jsm
+++ b/browser/components/downloads/DownloadsCommon.jsm
@@ -136,23 +136,16 @@ PrefObserver.register({
//// DownloadsCommon
/**
* This object is exposed directly to the consumers of this JavaScript module,
* 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
* returning formatted strings.
*/
get strings() {
let strings = {};
let sb = Services.strings.createBundle(kDownloadsStringBundleUrl);
let enumerator = sb.getSimpleEnumeration();
@@ -523,44 +516,46 @@ 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) +
(Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1) +
Ci.nsIPrompt.BUTTON_POS_1_DEFAULT;
let type = "";
let message = s.unblockTip;
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;
}
if (type) {
message = type + "\n\n" + message;
}
Services.ww.registerNotification(function onOpen(subj, topic) {
--- a/browser/components/downloads/content/allDownloadsViewOverlay.js
+++ b/browser/components/downloads/content/allDownloadsViewOverlay.js
@@ -367,18 +367,18 @@ HistoryDownloadElementShell.prototype =
}
if (this._historyDownload) {
let uri = NetUtil.newURI(this.download.source.url);
PlacesUtils.bhistory.removePage(uri);
}
},
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();
}
}).catch(Cu.reportError);
},
// Returns whether or not the download handled by this shell should
// show up in the search results for the given term. Both the display
--- a/browser/components/downloads/content/downloads.js
+++ b/browser/components/downloads/content/downloads.js
@@ -1094,18 +1094,18 @@ DownloadsViewItem.prototype = {
cmd_delete() {
DownloadsCommon.removeAndFinalizeDownload(this.download);
PlacesUtils.bhistory.removePage(
NetUtil.newURI(this.download.source.url));
},
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();
}
}).catch(Cu.reportError);
},
downloadsCmd_open() {
this.download.launch().catch(Cu.reportError);
--- a/browser/components/downloads/test/browser/browser_confirm_unblock_download.js
+++ b/browser/components/downloads/test/browser/browser_confirm_unblock_download.js
@@ -28,19 +28,19 @@ function addDialogOpenObserver(buttonAct
}
});
}
});
}
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.");
});
--- a/toolkit/components/jsdownloads/src/DownloadCore.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadCore.jsm
@@ -1524,30 +1524,41 @@ this.DownloadError = function (aProperti
}
if (aProperties.becauseBlockedByParentalControls) {
this.becauseBlocked = true;
this.becauseBlockedByParentalControls = true;
} else if (aProperties.becauseBlockedByReputationCheck) {
this.becauseBlocked = true;
this.becauseBlockedByReputationCheck = true;
+ this.reputationCheckVerdict = aProperties.reputationCheckVerdict || "";
} else if (aProperties.becauseBlockedByRuntimePermissions) {
this.becauseBlocked = true;
this.becauseBlockedByRuntimePermissions = true;
} else if (aProperties.becauseBlocked) {
this.becauseBlocked = true;
}
if (aProperties.innerException) {
this.innerException = aProperties.innerException;
}
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,
/**
* The result code associated with this error.
*/
result: false,
@@ -1584,16 +1595,25 @@ this.DownloadError.prototype = {
* download files was not granted.
*
* This does not apply to all systems. On Android this flag is set to true if
* a needed runtime permission (storage) has not been granted by the user.
*/
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
* to the store.
*/
innerException: null,
/**
* Returns a static representation of the current object state.
@@ -1606,16 +1626,17 @@ this.DownloadError.prototype = {
result: this.result,
message: this.message,
becauseSourceFailed: this.becauseSourceFailed,
becauseTargetFailed: this.becauseTargetFailed,
becauseBlocked: this.becauseBlocked,
becauseBlockedByParentalControls: this.becauseBlockedByParentalControls,
becauseBlockedByReputationCheck: this.becauseBlockedByReputationCheck,
becauseBlockedByRuntimePermissions: this.becauseBlockedByRuntimePermissions,
+ reputationCheckVerdict: this.reputationCheckVerdict,
};
serializeUnknownProperties(this, serializable);
return serializable;
},
};
/**
@@ -1631,17 +1652,18 @@ this.DownloadError.fromSerializable = fu
deserializeUnknownProperties(e, aSerializable, property =>
property != "result" &&
property != "message" &&
property != "becauseSourceFailed" &&
property != "becauseTargetFailed" &&
property != "becauseBlocked" &&
property != "becauseBlockedByParentalControls" &&
property != "becauseBlockedByReputationCheck" &&
- property != "becauseBlockedByRuntimePermissions");
+ property != "becauseBlockedByRuntimePermissions" &&
+ property != "reputationCheckVerdict");
return e;
};
////////////////////////////////////////////////////////////////////////////////
//// DownloadSaver
/**
@@ -2143,17 +2165,19 @@ this.DownloadCopySaver.prototype = {
* @resolves When the reputation check and cleanup is complete.
* @rejects DownloadError if the download should be blocked.
*/
_checkReputationAndMove: Task.async(function* () {
let download = this.download;
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;
// We will remove the potentially dangerous file if instructed by
// DownloadIntegration. We will always remove the file when the
// download did not use a partial file path, meaning it
// currently has its final filename.
if (!DownloadIntegration.shouldKeepBlockedData() || !partFilePath) {
@@ -2161,17 +2185,20 @@ this.DownloadCopySaver.prototype = {
yield OS.File.remove(partFilePath || targetPath);
} catch (ex) {
Cu.reportError(ex);
}
} else {
download.hasBlockedData = true;
}
- throw new DownloadError({ becauseBlockedByReputationCheck: true });
+ throw new DownloadError({
+ becauseBlockedByReputationCheck: true,
+ reputationCheckVerdict: verdict,
+ });
}
if (partFilePath) {
yield OS.File.move(partFilePath, targetPath);
}
}),
/**
--- a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
@@ -120,16 +120,30 @@ const kObserverTopics = [
"suspend_process_notification",
"wake_notification",
"resume_process_notification",
"network:offline-about-to-go-offline",
"network:offline-status-changed",
"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
/**
* Provides functions to integrate with the host application, handling for
* example the global prompts on shutdown.
*/
this.DownloadIntegration = {
@@ -143,16 +157,17 @@ this.DownloadIntegration = {
dontCheckRuntimePermissions: false,
shouldBlockInTestForRuntimePermissions: false,
#ifdef MOZ_URL_CLASSIFIER
dontCheckApplicationReputation: false,
#else
dontCheckApplicationReputation: true,
#endif
shouldBlockInTestForApplicationReputation: false,
+ verdictInTestForApplicationReputation: "",
shouldKeepBlockedDataInTest: false,
dontOpenFileAndFolder: false,
downloadDoneCalled: false,
_deferTestOpenFile: null,
_deferTestShowDir: null,
_deferTestClearPrivateList: null,
/**
@@ -526,51 +541,69 @@ this.DownloadIntegration = {
/**
* Checks to determine whether to block downloads because they might be
* malware, based on application reputation checks.
*
* aParam aDownload
* 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;
let channelRedirects;
try {
hash = aDownload.saver.getSha256Hash();
sigInfo = aDownload.saver.getSignatureInfo();
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;
if (aDownload.source.referrer) {
aReferrer: NetUtil.newURI(aDownload.source.referrer);
}
gApplicationReputationService.queryReputation({
sourceURI: NetUtil.newURI(aDownload.source.url),
referrerURI: aReferrer,
fileSize: aDownload.currentBytes,
sha256Hash: hash,
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;
},
#ifdef XP_WIN
/**
* Checks whether downloaded files should be marked as coming from
* Internet Zone.
--- a/toolkit/components/jsdownloads/test/unit/common_test_Download.js
+++ b/toolkit/components/jsdownloads/test/unit/common_test_Download.js
@@ -1706,22 +1706,25 @@ add_task(function* test_getSha256Hash()
* }
* @return {Promise}
* @resolves The reputation blocked download.
* @rejects JavaScript exception.
*/
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;
try {
if (keepPartialData) {
download = yield promiseStartDownload_tryToKeepPartialData();
continueResponses();
@@ -1735,17 +1738,21 @@ var promiseBlockedDownload = Task.async(
yield promiseDownloadStopped(download);
do_throw("The download should have blocked.");
} catch (ex) {
if (!(ex instanceof Downloads.Error) || !ex.becauseBlocked) {
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);
do_check_false(download.succeeded);
do_check_false(yield OS.File.exists(download.target.path));
cleanup();
return download;