diff --git a/browser/components/downloads/src/DownloadsCommon.jsm b/browser/components/downloads/src/DownloadsCommon.jsm index e9722019a9ae..98f4d1fa7d10 100644 --- a/browser/components/downloads/src/DownloadsCommon.jsm +++ b/browser/components/downloads/src/DownloadsCommon.jsm @@ -53,6 +53,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Downloads", "resource://gre/modules/Downloads.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "DownloadUIHelper", + "resource://gre/modules/DownloadUIHelper.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils", "resource://gre/modules/DownloadUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "OS", @@ -68,27 +70,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "DownloadsLogger", const nsIDM = Ci.nsIDownloadManager; -const kDownloadsStringBundleUrl = - "chrome://browser/locale/downloads/downloads.properties"; - const kPrefBdmScanWhenDone = "browser.download.manager.scanWhenDone"; const kPrefBdmAlertOnExeOpen = "browser.download.manager.alertOnEXEOpen"; -const kDownloadsStringsRequiringFormatting = { - sizeWithUnits: true, - shortTimeLeftSeconds: true, - shortTimeLeftMinutes: true, - shortTimeLeftHours: true, - shortTimeLeftDays: true, - statusSeparator: true, - statusSeparatorBeforeNumber: true, - fileExecutableSecurityWarning: true -}; - -const kDownloadsStringsRequiringPluralForm = { - otherDownloads2: true -}; - XPCOMUtils.defineLazyGetter(this, "DownloadsLocalFileCtor", function () { return Components.Constructor("@mozilla.org/file/local;1", "nsILocalFile", "initWithPath"); @@ -163,41 +147,6 @@ this.DownloadsCommon = { } this.error.apply(this, aMessageArgs); }, - /** - * 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(); - while (enumerator.hasMoreElements()) { - let string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement); - let stringName = string.key; - if (stringName in kDownloadsStringsRequiringFormatting) { - strings[stringName] = function () { - // Convert "arguments" to a real array before calling into XPCOM. - return sb.formatStringFromName(stringName, - Array.slice(arguments, 0), - arguments.length); - }; - } else if (stringName in kDownloadsStringsRequiringPluralForm) { - strings[stringName] = function (aCount) { - // Convert "arguments" to a real array before calling into XPCOM. - let formattedString = sb.formatStringFromName(stringName, - Array.slice(arguments, 0), - arguments.length); - return PluralForm.get(aCount, formattedString); - }; - } else { - strings[stringName] = string.value; - } - } - delete this.strings; - return this.strings = strings; - }, /** * Generates a very short string representing the given time left. @@ -480,65 +429,44 @@ this.DownloadsCommon = { if (!(aOwnerWindow instanceof Ci.nsIDOMWindow)) throw new Error("aOwnerWindow must be a dom-window object"); - // Confirm opening executable files if required. + let promiseShouldLaunch; if (aFile.isExecutable()) { - let showAlert = true; - try { - showAlert = Services.prefs.getBoolPref(kPrefBdmAlertOnExeOpen); - } catch (ex) { } - - // On Vista and above, we rely on native security prompting for - // downloaded content unless it's disabled. - if (DownloadsCommon.isWinVistaOrHigher) { - try { - if (Services.prefs.getBoolPref(kPrefBdmScanWhenDone)) { - showAlert = false; - } - } catch (ex) { } - } - - if (showAlert) { - let name = aFile.leafName; - let message = - DownloadsCommon.strings.fileExecutableSecurityWarning(name, name); - let title = - DownloadsCommon.strings.fileExecutableSecurityWarningTitle; - let dontAsk = - DownloadsCommon.strings.fileExecutableSecurityWarningDontAsk; - - let checkbox = { value: false }; - let open = Services.prompt.confirmCheck(aOwnerWindow, title, message, - dontAsk, checkbox); - if (!open) { - return; - } - - Services.prefs.setBoolPref(kPrefBdmAlertOnExeOpen, - !checkbox.value); - } + // We get a prompter for the provided window here, even though anchoring + // to the most recently active window should work as well. + promiseShouldLaunch = + DownloadUIHelper.getPrompter(aOwnerWindow) + .confirmLaunchExecutable(aFile.path); + } else { + promiseShouldLaunch = Promise.resolve(true); } - // Actually open the file. - try { - if (aMimeInfo && aMimeInfo.preferredAction == aMimeInfo.useHelperApp) { - aMimeInfo.launchWithFile(aFile); + promiseShouldLaunch.then(shouldLaunch => { + if (!shouldLaunch) { return; } - } - catch(ex) { } - - // If either we don't have the mime info, or the preferred action failed, - // attempt to launch the file directly. - try { - aFile.launch(); - } - catch(ex) { - // If launch fails, try sending it through the system's external "file:" - // URL handler. - Cc["@mozilla.org/uriloader/external-protocol-service;1"] - .getService(Ci.nsIExternalProtocolService) - .loadUrl(NetUtil.newURI(aFile)); - } + + // Actually open the file. + try { + if (aMimeInfo && aMimeInfo.preferredAction == aMimeInfo.useHelperApp) { + aMimeInfo.launchWithFile(aFile); + return; + } + } + catch(ex) { } + + // If either we don't have the mime info, or the preferred action failed, + // attempt to launch the file directly. + try { + aFile.launch(); + } + catch(ex) { + // If launch fails, try sending it through the system's external "file:" + // URL handler. + Cc["@mozilla.org/uriloader/external-protocol-service;1"] + .getService(Ci.nsIExternalProtocolService) + .loadUrl(NetUtil.newURI(aFile)); + } + }).then(null, Cu.reportError); }, /** @@ -574,6 +502,15 @@ this.DownloadsCommon = { } }; +/** + * 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. + */ +XPCOMUtils.defineLazyGetter(DownloadsCommon, "strings", function () { + return DownloadUIHelper.strings; +}); + /** * Returns true if we are executing on Windows Vista or a later version. */ diff --git a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm index abf660de8c3e..9885753883a8 100644 --- a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm +++ b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm @@ -29,6 +29,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "DownloadStore", "resource://gre/modules/DownloadStore.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "DownloadImport", "resource://gre/modules/DownloadImport.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "DownloadUIHelper", + "resource://gre/modules/DownloadUIHelper.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", "resource://gre/modules/FileUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", @@ -61,6 +63,7 @@ XPCOMUtils.defineLazyGetter(this, "gParentalControlsService", function() { return null; }); +// This will be replaced by "DownloadUIHelper.strings" (see bug 905123). XPCOMUtils.defineLazyGetter(this, "gStringBundle", function() { return Services.strings. createBundle("chrome://mozapps/locale/downloads/downloads.properties"); @@ -414,6 +417,24 @@ this.DownloadIntegration = { let deferred = Task.spawn(function DI_launchDownload_task() { let file = new FileUtils.File(aDownload.target.path); + // Ask for confirmation if the file is executable. We do this here, + // instead of letting the caller handle the prompt separately in the user + // interface layer, for two reasons. The first is because of its security + // nature, so that add-ons cannot forget to do this check. The second is + // that the system-level security prompt, if enabled, would be displayed + // at launch time in any case. + if (file.isExecutable() && !this.dontOpenFileAndFolder) { + // We don't anchor the prompt to a specific window intentionally, not + // only because this is the same behavior as the system-level prompt, + // but also because the most recently active window is the right choice + // in basically all cases. + let shouldLaunch = yield DownloadUIHelper.getPrompter() + .confirmLaunchExecutable(file.path); + if (!shouldLaunch) { + return; + } + } + // In case of a double extension, like ".tar.gz", we only // consider the last one, because the MIME service cannot // handle multiple extensions. @@ -560,7 +581,11 @@ this.DownloadIntegration = { */ _createDownloadsDirectory: function DI_createDownloadsDirectory(aName) { let directory = this._getDirectory(aName); - directory.append(gStringBundle.GetStringFromName("downloadsFolder")); + + // We read the name of the directory from the list of translated strings + // that is kept by the UI helper module, even if this string is not strictly + // displayed in the user interface. + directory.append(DownloadUIHelper.strings.downloadsFolder); // Create the Downloads folder and ignore if it already exists. return OS.File.makeDir(directory.path, { ignoreExisting: true }). diff --git a/toolkit/components/jsdownloads/src/DownloadUIHelper.jsm b/toolkit/components/jsdownloads/src/DownloadUIHelper.jsm index 297abe0dcfeb..8013c9d476e7 100644 --- a/toolkit/components/jsdownloads/src/DownloadUIHelper.jsm +++ b/toolkit/components/jsdownloads/src/DownloadUIHelper.jsm @@ -24,6 +24,31 @@ const Cr = Components.results; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "OS", + "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"); + +const kStringBundleUrl = + "chrome://browser/locale/downloads/downloads.properties"; + +const kStringsRequiringFormatting = { + sizeWithUnits: true, + shortTimeLeftSeconds: true, + shortTimeLeftMinutes: true, + shortTimeLeftHours: true, + shortTimeLeftDays: true, + statusSeparator: true, + statusSeparatorBeforeNumber: true, + fileExecutableSecurityWarning: true, +}; + +const kStringsRequiringPluralForm = { + otherDownloads2: true, +}; + //////////////////////////////////////////////////////////////////////////////// //// DownloadUIHelper @@ -31,4 +56,123 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); * Provides functions to handle status and messages in the user interface. */ this.DownloadUIHelper = { + /** + * Returns an object that can be used to display prompts related to downloads. + * + * The prompts may be either anchored to a specified window, or anchored to + * the most recently active window, for example if the prompt is displayed in + * response to global notifications that are not associated with any window. + * + * @param aParent + * If specified, should reference the nsIDOMWindow to which the prompts + * should be attached. If omitted, the prompts will be attached to the + * most recently active window. + * + * @return A DownloadPrompter object. + */ + getPrompter: function (aParent) + { + return new DownloadPrompter(aParent || null); + }, +}; + +/** + * 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. + */ +XPCOMUtils.defineLazyGetter(DownloadUIHelper, "strings", function () { + let strings = {}; + let sb = Services.strings.createBundle(kStringBundleUrl); + let enumerator = sb.getSimpleEnumeration(); + while (enumerator.hasMoreElements()) { + let string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement); + let stringName = string.key; + if (stringName in kStringsRequiringFormatting) { + strings[stringName] = function () { + // Convert "arguments" to a real array before calling into XPCOM. + return sb.formatStringFromName(stringName, + Array.slice(arguments, 0), + arguments.length); + }; + } else if (stringName in kStringsRequiringPluralForm) { + strings[stringName] = function (aCount) { + // Convert "arguments" to a real array before calling into XPCOM. + let formattedString = sb.formatStringFromName(stringName, + Array.slice(arguments, 0), + arguments.length); + return PluralForm.get(aCount, formattedString); + }; + } else { + strings[stringName] = string.value; + } + } + return strings; +}); + +//////////////////////////////////////////////////////////////////////////////// +//// DownloadPrompter + +/** + * Allows displaying prompts related to downloads. + * + * @param aParent + * The nsIDOMWindow to which prompts should be attached, or null to + * attach prompts to the most recently active window. + */ +function DownloadPrompter(aParent) +{ + this._prompter = Services.ww.getNewPrompter(aParent); +} + +DownloadPrompter.prototype = { + /** + * nsIPrompt instance for displaying messages. + */ + _prompter: null, + + /** + * Displays a warning message box that informs that the specified file is + * executable, and asks whether the user wants to launch it. The user is + * given the option of disabling future instances of this warning. + * + * @param aPath + * String containing the full path to the file to be opened. + * + * @return {Promise} + * @resolves Boolean indicating whether the launch operation can continue. + * @rejects JavaScript exception. + */ + confirmLaunchExecutable: function (aPath) + { + const kPrefAlertOnEXEOpen = "browser.download.manager.alertOnEXEOpen"; + + try { + try { + if (!Services.prefs.getBoolPref(kPrefAlertOnEXEOpen)) { + return Promise.resolve(true); + } + } catch (ex) { + // If the preference does not exist, continue with the prompt. + } + + let leafName = OS.Path.basename(aPath); + + let s = DownloadUIHelper.strings; + let checkState = { value: false }; + let shouldLaunch = this._prompter.confirmCheck( + s.fileExecutableSecurityWarningTitle, + s.fileExecutableSecurityWarning(leafName, leafName), + s.fileExecutableSecurityWarningDontAsk, + checkState); + + if (shouldLaunch) { + Services.prefs.setBoolPref(kPrefAlertOnEXEOpen, !checkState.value); + } + + return Promise.resolve(shouldLaunch); + } catch (ex) { + return Promise.reject(ex); + } + }, };