Bug 1216537 - Check and request storage permission if file download is started. r?nalexander,paolo draft
authorSebastian Kaspari <s.kaspari@gmail.com>
Mon, 11 Jan 2016 13:17:31 +0100
changeset 321990 acce22cda89f10d1e10cfea356a29048b1016d38
parent 321989 fd9205be2ccc04bae43ff2a14c7c100ccd8e1ff6
child 322060 468d079c41bbf2a12e0f5fb52f9cb6a28d0ab365
push id9497
push users.kaspari@gmail.com
push dateFri, 15 Jan 2016 12:27:24 +0000
reviewersnalexander, paolo
bugs1216537
milestone46.0a1
Bug 1216537 - Check and request storage permission if file download is started. r?nalexander,paolo
toolkit/components/jsdownloads/src/DownloadCore.jsm
toolkit/components/jsdownloads/src/DownloadIntegration.jsm
toolkit/components/jsdownloads/test/unit/common_test_Download.js
toolkit/components/jsdownloads/test/unit/head.js
--- a/toolkit/components/jsdownloads/src/DownloadCore.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadCore.jsm
@@ -455,16 +455,21 @@ this.Download.prototype = {
       }
 
       try {
         // Disallow download if parental controls service restricts it.
         if (yield DownloadIntegration.shouldBlockForParentalControls(this)) {
           throw new DownloadError({ becauseBlockedByParentalControls: true });
         }
 
+        // Disallow download if needed runtime permissions have not been granted by user
+        if (yield DownloadIntegration.shouldBlockForRuntimePermissions()) {
+          throw new DownloadError({ becauseBlockedByRuntimePermissions: true });
+        }
+
         // We should check if we have been canceled in the meantime, after all
         // the previous asynchronous operations have been executed and just
         // before we call the "execute" method of the saver.
         if (this._promiseCanceled) {
           // The exception will become a cancellation in the "catch" block.
           throw undefined;
         }
 
@@ -1490,17 +1495,18 @@ this.DownloadError = function (aProperti
 
   // Set the error name used by the Error object prototype first.
   this.name = "DownloadError";
   this.result = aProperties.result || Cr.NS_ERROR_FAILURE;
   if (aProperties.message) {
     this.message = aProperties.message;
   } else if (aProperties.becauseBlocked ||
              aProperties.becauseBlockedByParentalControls ||
-             aProperties.becauseBlockedByReputationCheck) {
+             aProperties.becauseBlockedByReputationCheck ||
+             aProperties.becauseBlockedByRuntimePermissions) {
     this.message = "Download blocked.";
   } else {
     let exception = new Components.Exception("", this.result);
     this.message = exception.toString();
   }
   if (aProperties.inferCause) {
     let module = ((this.result & 0x7FFF0000) >> 16) -
                  NS_ERROR_MODULE_BASE_OFFSET;
@@ -1517,16 +1523,19 @@ this.DownloadError = function (aProperti
   }
 
   if (aProperties.becauseBlockedByParentalControls) {
     this.becauseBlocked = true;
     this.becauseBlockedByParentalControls = true;
   } else if (aProperties.becauseBlockedByReputationCheck) {
     this.becauseBlocked = true;
     this.becauseBlockedByReputationCheck = true;
+  } else if (aProperties.becauseBlockedByRuntimePermissions) {
+    this.becauseBlocked = true;
+    this.becauseBlockedByRuntimePermissions = true;
   } else if (aProperties.becauseBlocked) {
     this.becauseBlocked = true;
   }
 
   if (aProperties.innerException) {
     this.innerException = aProperties.innerException;
   }
 
@@ -1565,16 +1574,24 @@ this.DownloadError.prototype = {
 
   /**
    * Indicates the download was blocked because it failed the reputation check
    * and may be malware.
    */
   becauseBlockedByReputationCheck: false,
 
   /**
+   * Indicates the download was blocked because a runtime permission required to
+   * 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 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.
@@ -1586,16 +1603,17 @@ this.DownloadError.prototype = {
     let serializable = {
       result: this.result,
       message: this.message,
       becauseSourceFailed: this.becauseSourceFailed,
       becauseTargetFailed: this.becauseTargetFailed,
       becauseBlocked: this.becauseBlocked,
       becauseBlockedByParentalControls: this.becauseBlockedByParentalControls,
       becauseBlockedByReputationCheck: this.becauseBlockedByReputationCheck,
+      becauseBlockedByRuntimePermissions: this.becauseBlockedByRuntimePermissions
     };
 
     serializeUnknownProperties(this, serializable);
     return serializable;
   },
 };
 
 /**
@@ -1610,17 +1628,18 @@ this.DownloadError.fromSerializable = fu
   let e = new DownloadError(aSerializable);
   deserializeUnknownProperties(e, aSerializable, property =>
     property != "result" &&
     property != "message" &&
     property != "becauseSourceFailed" &&
     property != "becauseTargetFailed" &&
     property != "becauseBlocked" &&
     property != "becauseBlockedByParentalControls" &&
-    property != "becauseBlockedByReputationCheck");
+    property != "becauseBlockedByReputationCheck" &&
+    property != "becauseBlockedByRuntimePermissions");
 
   return e;
 };
 
 ////////////////////////////////////////////////////////////////////////////////
 //// DownloadSaver
 
 /**
--- a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
@@ -63,16 +63,20 @@ XPCOMUtils.defineLazyServiceGetter(this,
                                    "@mozilla.org/process/environment;1",
                                    "nsIEnvironment");
 XPCOMUtils.defineLazyServiceGetter(this, "gMIMEService",
                                    "@mozilla.org/mime;1",
                                    "nsIMIMEService");
 XPCOMUtils.defineLazyServiceGetter(this, "gExternalProtocolService",
                                    "@mozilla.org/uriloader/external-protocol-service;1",
                                    "nsIExternalProtocolService");
+#ifdef MOZ_WIDGET_ANDROID
+XPCOMUtils.defineLazyModuleGetter(this, "RuntimePermissions",
+                                  "resource://gre/modules/RuntimePermissions.jsm");
+#endif
 
 XPCOMUtils.defineLazyGetter(this, "gParentalControlsService", function() {
   if ("@mozilla.org/parental-controls-service;1" in Cc) {
     return Cc["@mozilla.org/parental-controls-service;1"]
       .createInstance(Ci.nsIParentalControlsService);
   }
   return null;
 });
@@ -131,16 +135,18 @@ const kObserverTopics = [
 this.DownloadIntegration = {
   // For testing only
   _testMode: false,
   testPromptDownloads: 0,
   dontLoadList: false,
   dontLoadObservers: false,
   dontCheckParentalControls: false,
   shouldBlockInTest: false,
+  dontCheckRuntimePermissions: false,
+  shouldBlockInTestForRuntimePermissions: false,
 #ifdef MOZ_URL_CLASSIFIER
   dontCheckApplicationReputation: false,
 #else
   dontCheckApplicationReputation: true,
 #endif
   shouldBlockInTestForApplicationReputation: false,
   shouldKeepBlockedDataInTest: false,
   dontOpenFileAndFolder: false,
@@ -494,16 +500,35 @@ this.DownloadIntegration = {
                                    shouldBlock,
                                    NetUtil.newURI(aDownload.source.url), null);
     }
 
     return Promise.resolve(shouldBlock);
   },
 
   /**
+   * Checks to determine whether to block downloads for not granted runtime permissions.
+   *
+   * @return {Promise}
+   * @resolves The boolean indicates to block downloads or not.
+   */
+  shouldBlockForRuntimePermissions: function DI_shouldBlockForRuntimePermissions() {
+    if (this.dontCheckRuntimePermissions) {
+        return Promise.resolve(this.shouldBlockInTestForRuntimePermissions);
+    }
+
+#ifdef MOZ_WIDGET_ANDROID
+    return RuntimePermissions.waitForPermissions(RuntimePermissions.WRITE_EXTERNAL_STORAGE)
+                             .then(permissionGranted => !permissionGranted);
+#else
+    return Promise.resolve(false);
+#endif
+  },
+
+  /**
    * 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.
--- a/toolkit/components/jsdownloads/test/unit/common_test_Download.js
+++ b/toolkit/components/jsdownloads/test/unit/common_test_Download.js
@@ -1636,16 +1636,57 @@ add_task(function* test_blocked_parental
     do_check_true(download.error.becauseBlockedByParentalControls);
     do_check_true(download.stopped);
   }
 
   do_check_false(yield OS.File.exists(download.target.path));
 });
 
 /**
+ * Download with runtime permissions
+ */
+add_task(function* test_blocked_runtime_permissions()
+{
+  function cleanup() {
+    DownloadIntegration.shouldBlockInTestForRuntimePermissions = false;
+  }
+  do_register_cleanup(cleanup);
+  DownloadIntegration.shouldBlockInTestForRuntimePermissions = true;
+
+  let download;
+  try {
+    if (!gUseLegacySaver) {
+      // When testing DownloadCopySaver, we want to check that the promise
+      // returned by the "start" method is rejected.
+      download = yield promiseNewDownload();
+      yield download.start();
+    } else {
+      // When testing DownloadLegacySaver, we cannot be sure whether we are
+      // testing the promise returned by the "start" method or we are testing
+      // the "error" property checked by promiseDownloadStopped.  This happens
+      // because we don't have control over when the download is started.
+      download = yield promiseStartLegacyDownload();
+      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.becauseBlockedByRuntimePermissions);
+    do_check_true(download.error.becauseBlockedByRuntimePermissions);
+  }
+
+  // Now that the download stopped, the target file should not exist.
+  do_check_false(yield OS.File.exists(download.target.path));
+
+  cleanup();
+});
+
+/**
  * Check that DownloadCopySaver can always retrieve the hash.
  * DownloadLegacySaver can only retrieve the hash when
  * nsIExternalHelperAppService is invoked.
  */
 add_task(function* test_getSha256Hash()
 {
   if (!gUseLegacySaver) {
     let download = yield promiseStartDownload(httpUrl("source.txt"));
--- a/toolkit/components/jsdownloads/test/unit/head.js
+++ b/toolkit/components/jsdownloads/test/unit/head.js
@@ -793,16 +793,18 @@ add_task(function test_common_initialize
   // Disable the parental controls checking.
   DownloadIntegration.dontCheckParentalControls = true;
   // Disable application reputation checks.
   DownloadIntegration.dontCheckApplicationReputation = true;
   // Disable the calls to the OS to launch files and open containing folders
   DownloadIntegration.dontOpenFileAndFolder = true;
   DownloadIntegration._deferTestOpenFile = Promise.defer();
   DownloadIntegration._deferTestShowDir = Promise.defer();
+  // Disable checking runtime permissions
+  DownloadIntegration.dontCheckRuntimePermissions = true;
 
   // Avoid leaking uncaught promise errors
   DownloadIntegration._deferTestOpenFile.promise.then(null, () => undefined);
   DownloadIntegration._deferTestShowDir.promise.then(null, () => undefined);
 
   // Get a reference to nsIComponentRegistrar, and ensure that is is freed
   // before the XPCOM shutdown.
   let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);