diff --git a/toolkit/components/jsdownloads/src/DownloadCore.jsm b/toolkit/components/jsdownloads/src/DownloadCore.jsm index 2fb0cd8e074..0d6e7d4763d 100644 --- a/toolkit/components/jsdownloads/src/DownloadCore.jsm +++ b/toolkit/components/jsdownloads/src/DownloadCore.jsm @@ -54,6 +54,8 @@ const Cr = Components.results; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "DownloadIntegration", + "resource://gre/modules/DownloadIntegration.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "OS", @@ -269,6 +271,14 @@ Download.prototype = { yield this._promiseCanceled; } + // Disallow download if parental controls service restricts it. + if (yield DownloadIntegration.shouldBlockForParentalControls(this)) { + let error = new DownloadError(Cr.NS_ERROR_FAILURE, "Download blocked."); + error.becauseBlocked = true; + error.becauseBlockedByParentalControls = true; + throw error; + } + try { // Execute the actual download through the saver object. yield this.saver.execute(DS_setProgressBytes.bind(this)); @@ -541,6 +551,18 @@ DownloadError.prototype = { * Indicates an error occurred while writing to the local target. */ becauseTargetFailed: false, + + /** + * Indicates the download failed because it was blocked. If the reason for + * blocking is known, the corresponding property will be also set. + */ + becauseBlocked: false, + + /** + * Indicates the download was blocked because downloads are globally + * disallowed by the Parental Controls or Family Safety features on Windows. + */ + becauseBlockedByParentalControls: false, }; //////////////////////////////////////////////////////////////////////////////// diff --git a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm index afafe5dc1fe..b6f57eaf0a0 100644 --- a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm +++ b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm @@ -30,7 +30,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "DownloadStore", XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", "resource://gre/modules/FileUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "OS", - "resource://gre/modules/osfile.jsm") + "resource://gre/modules/osfile.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/commonjs/sdk/core/promise.js"); XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Task", @@ -38,6 +40,14 @@ XPCOMUtils.defineLazyModuleGetter(this, "Task", XPCOMUtils.defineLazyServiceGetter(this, "env", "@mozilla.org/process/environment;1", "nsIEnvironment"); +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; +}); + XPCOMUtils.defineLazyGetter(this, "gStringBundle", function() { return Services.strings. createBundle("chrome://mozapps/locale/downloads/downloads.properties"); @@ -54,6 +64,8 @@ this.DownloadIntegration = { // For testing only testMode: false, dontLoad: false, + dontCheckParentalControls: false, + shouldBlockInTest: false, /** * Main DownloadStore object for loading and saving the list of persistent @@ -182,7 +194,6 @@ this.DownloadIntegration = { directory = Services.prefs.getComplexValue("browser.download.dir", Ci.nsIFile); yield OS.File.makeDir(directory.path, { ignoreExisting: true }); - } catch(ex) { // Either the preference isn't set or the directory cannot be created. directory = yield this.getSystemDownloadsDirectory(); @@ -221,6 +232,34 @@ this.DownloadIntegration = { }.bind(this)); }, + /** + * Checks to determine whether to block downloads for parental controls. + * + * aParam aDownload + * The download object. + * + * @return {Promise} + * @resolves The boolean indicates to block downloads or not. + */ + shouldBlockForParentalControls: function DI_shouldBlockForParentalControls(aDownload) { + if (this.dontCheckParentalControls) { + return Promise.resolve(this.shouldBlockInTest); + } + + let isEnabled = gParentalControlsService && + gParentalControlsService.parentalControlsEnabled; + let shouldBlock = isEnabled && + gParentalControlsService.blockFileDownloadsEnabled; + + // Log the event if required by parental controls settings. + if (isEnabled && gParentalControlsService.loggingEnabled) { + gParentalControlsService.log(gParentalControlsService.ePCLog_FileDownload, + shouldBlock, aDownload.source.uri, null); + } + + return Promise.resolve(shouldBlock); + }, + /** * Determines whether it's a Windows Metro app. */ diff --git a/toolkit/components/jsdownloads/src/DownloadLegacy.js b/toolkit/components/jsdownloads/src/DownloadLegacy.js index 9bbcb1f165d..91665855ee4 100644 --- a/toolkit/components/jsdownloads/src/DownloadLegacy.js +++ b/toolkit/components/jsdownloads/src/DownloadLegacy.js @@ -83,6 +83,10 @@ DownloadLegacyTransfer.prototype = { onStateChange: function DLT_onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { + if (!Components.isSuccessCode(aStatus)) { + this._componentFailed = true; + } + // Detect when the last file has been received, or the download failed. if ((aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) && (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK)) { @@ -113,6 +117,8 @@ DownloadLegacyTransfer.prototype = { // change, but if no network request actually started, it is possible that // we only receive a status change with an error status code. if (!Components.isSuccessCode(aStatus)) { + this._componentFailed = true; + // Wait for the associated Download object to be available. this._deferDownload.promise.then(function DLT_OSC_onDownload(aDownload) { aDownload.saver.onTransferFinished(aRequest, aStatus); @@ -160,12 +166,18 @@ DownloadLegacyTransfer.prototype = { saver: { type: "legacy" }, }).then(function DLT_I_onDownload(aDownload) { // Now that the saver is available, hook up the cancellation handler. - aDownload.saver.deferCanceled.promise - .then(function () aCancelable.cancel(Cr.NS_ERROR_ABORT)) - .then(null, Cu.reportError); + aDownload.saver.deferCanceled.promise.then(() => { + // Only cancel if the object executing the download is still running. + if (!this._componentFailed) { + aCancelable.cancel(Cr.NS_ERROR_ABORT); + } + }).then(null, Cu.reportError); // Start the download before allowing it to be controlled. - aDownload.start(); + aDownload.start().then(null, function () { + // In case the operation failed, ensure we stop downloading data. + aDownload.saver.deferCanceled.resolve(); + }); // Start processing all the other events received through nsITransfer. this._deferDownload.resolve(aDownload); @@ -189,6 +201,13 @@ DownloadLegacyTransfer.prototype = { * object associated with this nsITransfer instance, when it is available. */ _deferDownload: null, + + /** + * Indicates that the component that executes the download has notified a + * failure condition. In this case, we should never use the component methods + * that cancel the download. + */ + _componentFailed: false, }; //////////////////////////////////////////////////////////////////////////////// diff --git a/toolkit/components/jsdownloads/test/unit/head.js b/toolkit/components/jsdownloads/test/unit/head.js index 77ae49243be..1cbef8e8acf 100644 --- a/toolkit/components/jsdownloads/test/unit/head.js +++ b/toolkit/components/jsdownloads/test/unit/head.js @@ -451,4 +451,6 @@ add_task(function test_common_initialize() // Disable integration with the host application requiring profile access. DownloadIntegration.dontLoad = true; + // Disable the parental controls checking. + DownloadIntegration.dontCheckParentalControls = true; }); diff --git a/toolkit/components/jsdownloads/test/unit/test_DownloadCore.js b/toolkit/components/jsdownloads/test/unit/test_DownloadCore.js index a8e61470281..aa57bbc729c 100644 --- a/toolkit/components/jsdownloads/test/unit/test_DownloadCore.js +++ b/toolkit/components/jsdownloads/test/unit/test_DownloadCore.js @@ -873,3 +873,25 @@ add_task(function test_download_cancel_midway_restart_with_content_encoding() yield promiseVerifyContents(download.target.file, TEST_DATA_SHORT); }); +/** + * Download with parental controls enabled. + */ +add_task(function test_download_blocked_parental_controls() +{ + function cleanup() { + DownloadIntegration.shouldBlockInTest = false; + } + do_register_cleanup(cleanup); + DownloadIntegration.shouldBlockInTest = true; + + let download = yield promiseSimpleDownload(); + + try { + yield download.start(); + do_throw("The download should have blocked."); + } catch (ex if ex instanceof Downloads.Error && ex.becauseBlocked) { + do_check_true(ex.becauseBlockedByParentalControls); + } + cleanup(); +}); + diff --git a/toolkit/components/jsdownloads/test/unit/test_DownloadIntegration.js b/toolkit/components/jsdownloads/test/unit/test_DownloadIntegration.js index 00cff12e16b..8394e541047 100644 --- a/toolkit/components/jsdownloads/test/unit/test_DownloadIntegration.js +++ b/toolkit/components/jsdownloads/test/unit/test_DownloadIntegration.js @@ -125,7 +125,6 @@ add_task(function test_getUserDownloadsDirectory() cleanup(); }); - /** * Tests that the getTemporaryDownloadsDirectory returns a valid nsFile * download directory object. diff --git a/toolkit/components/jsdownloads/test/unit/test_DownloadLegacy.js b/toolkit/components/jsdownloads/test/unit/test_DownloadLegacy.js index 5dee57ba187..75f2ec75b86 100644 --- a/toolkit/components/jsdownloads/test/unit/test_DownloadLegacy.js +++ b/toolkit/components/jsdownloads/test/unit/test_DownloadLegacy.js @@ -353,3 +353,27 @@ add_task(function test_download_public_and_private() cleanup(); }); +/** + * Download with parental controls enabled. + */ +add_task(function test_download_blocked_parental_controls() +{ + function cleanup() { + DownloadIntegration.shouldBlockInTest = false; + } + do_register_cleanup(cleanup); + DownloadIntegration.shouldBlockInTest = true; + + let download = yield promiseStartLegacyDownload(); + + try { + yield download.start(); + do_throw("The download should have blocked."); + } catch (ex if ex instanceof Downloads.Error && ex.becauseBlocked) { + do_check_true(ex.becauseBlockedByParentalControls); + } + + do_check_false(download.target.file.exists()); + + cleanup(); +});