Bug 1216537 - Check and request storage permission if file download is started. r=nalexander,paolo

This commit is contained in:
Sebastian Kaspari 2016-01-15 19:38:18 +01:00
parent 4d55424f7e
commit 88ba0b58f8
4 changed files with 99 additions and 3 deletions

View File

@ -460,6 +460,12 @@ this.Download.prototype = {
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.
@ -1495,7 +1501,8 @@ this.DownloadError = function (aProperties)
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);
@ -1522,6 +1529,9 @@ this.DownloadError = function (aProperties)
} 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;
}
@ -1569,6 +1579,15 @@ this.DownloadError.prototype = {
*/
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
@ -1591,6 +1610,7 @@ this.DownloadError.prototype = {
becauseBlocked: this.becauseBlocked,
becauseBlockedByParentalControls: this.becauseBlockedByParentalControls,
becauseBlockedByReputationCheck: this.becauseBlockedByReputationCheck,
becauseBlockedByRuntimePermissions: this.becauseBlockedByRuntimePermissions,
};
serializeUnknownProperties(this, serializable);
@ -1615,7 +1635,8 @@ this.DownloadError.fromSerializable = function (aSerializable) {
property != "becauseTargetFailed" &&
property != "becauseBlocked" &&
property != "becauseBlockedByParentalControls" &&
property != "becauseBlockedByReputationCheck");
property != "becauseBlockedByReputationCheck" &&
property != "becauseBlockedByRuntimePermissions");
return e;
};
@ -2316,14 +2337,18 @@ this.DownloadLegacySaver.prototype = {
*/
onProgressBytes: function DLS_onProgressBytes(aCurrentBytes, aTotalBytes)
{
this.progressWasNotified = true;
// Ignore progress notifications until we are ready to process them.
if (!this.setProgressBytesFn) {
// Keep the data from the last progress notification that was received.
this.currentBytes = aCurrentBytes;
this.totalBytes = aTotalBytes;
return;
}
let hasPartFile = !!this.download.target.partFilePath;
this.progressWasNotified = true;
this.setProgressBytesFn(aCurrentBytes, aTotalBytes,
aCurrentBytes > 0 && hasPartFile);
},
@ -2433,6 +2458,9 @@ this.DownloadLegacySaver.prototype = {
}
this.setProgressBytesFn = aSetProgressBytesFn;
if (this.progressWasNotified) {
this.onProgressBytes(this.currentBytes, this.totalBytes);
}
return Task.spawn(function* task_DLS_execute() {
try {

View File

@ -68,6 +68,10 @@ XPCOMUtils.defineLazyServiceGetter(this, "gMIMEService",
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) {
@ -136,6 +140,8 @@ this.DownloadIntegration = {
dontLoadObservers: false,
dontCheckParentalControls: false,
shouldBlockInTest: false,
dontCheckRuntimePermissions: false,
shouldBlockInTestForRuntimePermissions: false,
#ifdef MOZ_URL_CLASSIFIER
dontCheckApplicationReputation: false,
#else
@ -498,6 +504,25 @@ this.DownloadIntegration = {
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.

View File

@ -1640,6 +1640,47 @@ add_task(function* test_blocked_parental_controls_httpstatus450()
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

View File

@ -798,6 +798,8 @@ add_task(function test_common_initialize()
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);