Bug 1254100 - Part 2 - Downloads blocked by Application Reputation should provide information about the verdict. r=mak draft
authorPaolo Amadini <paolo.mozmail@amadzone.org>
Wed, 16 Mar 2016 14:29:23 +0000
changeset 342694 e4439dc7335ba0ecb40300d394f57424f6ac8a28
parent 342693 0dc22ed16dd77c2ebd51ae7419febef3113e01ce
child 342695 bb50bbb3f426c35235893dbce5fefe1688130e08
push id13439
push userpaolo.mozmail@amadzone.org
push dateSun, 20 Mar 2016 14:07:39 +0000
reviewersmak
bugs1254100
milestone48.0a1
Bug 1254100 - Part 2 - Downloads blocked by Application Reputation should provide information about the verdict. r=mak MozReview-Commit-ID: FYH5Tdtbzn
browser/components/downloads/DownloadsCommon.jsm
browser/components/downloads/content/allDownloadsViewOverlay.js
browser/components/downloads/content/downloads.js
browser/components/downloads/test/browser/browser_confirm_unblock_download.js
toolkit/components/jsdownloads/src/DownloadCore.jsm
toolkit/components/jsdownloads/src/DownloadIntegration.jsm
toolkit/components/jsdownloads/test/unit/common_test_Download.js
--- 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;