summaryrefslogtreecommitdiffstats
path: root/toolkit/mozapps/downloads/nsHelperAppDlg.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/mozapps/downloads/nsHelperAppDlg.js')
-rw-r--r--toolkit/mozapps/downloads/nsHelperAppDlg.js1147
1 files changed, 1147 insertions, 0 deletions
diff --git a/toolkit/mozapps/downloads/nsHelperAppDlg.js b/toolkit/mozapps/downloads/nsHelperAppDlg.js
new file mode 100644
index 000000000..58697cc77
--- /dev/null
+++ b/toolkit/mozapps/downloads/nsHelperAppDlg.js
@@ -0,0 +1,1147 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const {utils: Cu, interfaces: Ci, classes: Cc, results: Cr} = Components;
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "EnableDelayHelper",
+ "resource://gre/modules/SharedPromptUtils.jsm");
+
+///////////////////////////////////////////////////////////////////////////////
+//// Helper Functions
+
+/**
+ * Determines if a given directory is able to be used to download to.
+ *
+ * @param aDirectory
+ * The directory to check.
+ * @return true if we can use the directory, false otherwise.
+ */
+function isUsableDirectory(aDirectory)
+{
+ return aDirectory.exists() && aDirectory.isDirectory() &&
+ aDirectory.isWritable();
+}
+
+// Web progress listener so we can detect errors while mLauncher is
+// streaming the data to a temporary file.
+function nsUnknownContentTypeDialogProgressListener(aHelperAppDialog) {
+ this.helperAppDlg = aHelperAppDialog;
+}
+
+nsUnknownContentTypeDialogProgressListener.prototype = {
+ // nsIWebProgressListener methods.
+ // Look for error notifications and display alert to user.
+ onStatusChange: function( aWebProgress, aRequest, aStatus, aMessage ) {
+ if ( aStatus != Components.results.NS_OK ) {
+ // Display error alert (using text supplied by back-end).
+ // FIXME this.dialog is undefined?
+ Services.prompt.alert( this.dialog, this.helperAppDlg.mTitle, aMessage );
+ // Close the dialog.
+ this.helperAppDlg.onCancel();
+ if ( this.helperAppDlg.mDialog ) {
+ this.helperAppDlg.mDialog.close();
+ }
+ }
+ },
+
+ // Ignore onProgressChange, onProgressChange64, onStateChange, onLocationChange, onSecurityChange, and onRefreshAttempted notifications.
+ onProgressChange: function( aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress ) {
+ },
+
+ onProgressChange64: function( aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress ) {
+ },
+
+
+
+ onStateChange: function( aWebProgress, aRequest, aStateFlags, aStatus ) {
+ },
+
+ onLocationChange: function( aWebProgress, aRequest, aLocation, aFlags ) {
+ },
+
+ onSecurityChange: function( aWebProgress, aRequest, state ) {
+ },
+
+ onRefreshAttempted: function( aWebProgress, aURI, aDelay, aSameURI ) {
+ return true;
+ }
+};
+
+///////////////////////////////////////////////////////////////////////////////
+//// nsUnknownContentTypeDialog
+
+/* This file implements the nsIHelperAppLauncherDialog interface.
+ *
+ * The implementation consists of a JavaScript "class" named nsUnknownContentTypeDialog,
+ * comprised of:
+ * - a JS constructor function
+ * - a prototype providing all the interface methods and implementation stuff
+ *
+ * In addition, this file implements an nsIModule object that registers the
+ * nsUnknownContentTypeDialog component.
+ */
+
+const PREF_BD_USEDOWNLOADDIR = "browser.download.useDownloadDir";
+const nsITimer = Components.interfaces.nsITimer;
+
+var downloadModule = {};
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/DownloadLastDir.jsm", downloadModule);
+Components.utils.import("resource://gre/modules/DownloadPaths.jsm");
+Components.utils.import("resource://gre/modules/DownloadUtils.jsm");
+Components.utils.import("resource://gre/modules/Downloads.jsm");
+Components.utils.import("resource://gre/modules/FileUtils.jsm");
+Components.utils.import("resource://gre/modules/Task.jsm");
+
+/* ctor
+ */
+function nsUnknownContentTypeDialog() {
+ // Initialize data properties.
+ this.mLauncher = null;
+ this.mContext = null;
+ this.mReason = null;
+ this.chosenApp = null;
+ this.givenDefaultApp = false;
+ this.updateSelf = true;
+ this.mTitle = "";
+}
+
+nsUnknownContentTypeDialog.prototype = {
+ classID: Components.ID("{F68578EB-6EC2-4169-AE19-8C6243F0ABE1}"),
+
+ nsIMIMEInfo : Components.interfaces.nsIMIMEInfo,
+
+ QueryInterface: function (iid) {
+ if (!iid.equals(Components.interfaces.nsIHelperAppLauncherDialog) &&
+ !iid.equals(Components.interfaces.nsITimerCallback) &&
+ !iid.equals(Components.interfaces.nsISupports)) {
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+ return this;
+ },
+
+ // ---------- nsIHelperAppLauncherDialog methods ----------
+
+ // show: Open XUL dialog using window watcher. Since the dialog is not
+ // modal, it needs to be a top level window and the way to open
+ // one of those is via that route).
+ show: function(aLauncher, aContext, aReason) {
+ this.mLauncher = aLauncher;
+ this.mContext = aContext;
+ this.mReason = aReason;
+
+ // Cache some information in case this context goes away:
+ try {
+ let parent = aContext.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
+ this._mDownloadDir = new downloadModule.DownloadLastDir(parent);
+ } catch (ex) {
+ Cu.reportError("Missing window information when showing nsIHelperAppLauncherDialog: " + ex);
+ }
+
+ const nsITimer = Components.interfaces.nsITimer;
+ this._showTimer = Components.classes["@mozilla.org/timer;1"]
+ .createInstance(nsITimer);
+ this._showTimer.initWithCallback(this, 0, nsITimer.TYPE_ONE_SHOT);
+ },
+
+ // When opening from new tab, if tab closes while dialog is opening,
+ // (which is a race condition on the XUL file being cached and the timer
+ // in nsExternalHelperAppService), the dialog gets a blur and doesn't
+ // activate the OK button. So we wait a bit before doing opening it.
+ reallyShow: function() {
+ try {
+ let ir = this.mContext.QueryInterface(Components.interfaces.nsIInterfaceRequestor);
+ let docShell = ir.getInterface(Components.interfaces.nsIDocShell);
+ let rootWin = docShell.QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ let ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
+ .getService(Components.interfaces.nsIWindowWatcher);
+ this.mDialog = ww.openWindow(rootWin,
+ "chrome://mozapps/content/downloads/unknownContentType.xul",
+ null,
+ "chrome,centerscreen,titlebar,dialog=yes,dependent",
+ null);
+ } catch (ex) {
+ // The containing window may have gone away. Break reference
+ // cycles and stop doing the download.
+ this.mLauncher.cancel(Components.results.NS_BINDING_ABORTED);
+ return;
+ }
+
+ // Hook this object to the dialog.
+ this.mDialog.dialog = this;
+
+ // Hook up utility functions.
+ this.getSpecialFolderKey = this.mDialog.getSpecialFolderKey;
+
+ // Watch for error notifications.
+ var progressListener = new nsUnknownContentTypeDialogProgressListener(this);
+ this.mLauncher.setWebProgressListener(progressListener);
+ },
+
+ //
+ // displayBadPermissionAlert()
+ //
+ // Diplay an alert panel about the bad permission of folder/directory.
+ //
+ displayBadPermissionAlert: function () {
+ let bundle =
+ Services.strings.createBundle("chrome://mozapps/locale/downloads/unknownContentType.properties");
+
+ Services.prompt.alert(this.dialog,
+ bundle.GetStringFromName("badPermissions.title"),
+ bundle.GetStringFromName("badPermissions"));
+ },
+
+ promptForSaveToFileAsync: function(aLauncher, aContext, aDefaultFile, aSuggestedFileExtension, aForcePrompt) {
+ var result = null;
+
+ this.mLauncher = aLauncher;
+
+ let prefs = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefBranch);
+ let bundle =
+ Services.strings
+ .createBundle("chrome://mozapps/locale/downloads/unknownContentType.properties");
+
+ let parent;
+ let gDownloadLastDir;
+ try {
+ parent = aContext.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
+ } catch (ex) {}
+
+ if (parent) {
+ gDownloadLastDir = new downloadModule.DownloadLastDir(parent);
+ } else {
+ // Use the cached download info, but pick an arbitrary parent window
+ // because the original one is definitely gone (and nsIFilePicker doesn't like
+ // a null parent):
+ gDownloadLastDir = this._mDownloadDir;
+ let windowsEnum = Services.wm.getEnumerator("");
+ while (windowsEnum.hasMoreElements()) {
+ let someWin = windowsEnum.getNext();
+ // We need to make sure we don't end up with this dialog, because otherwise
+ // that's going to go away when the user clicks "Save", and that breaks the
+ // windows file picker that's supposed to show up if we let the user choose
+ // where to save files...
+ if (someWin != this.mDialog) {
+ parent = someWin;
+ }
+ }
+ if (!parent) {
+ Cu.reportError("No candidate parent windows were found for the save filepicker." +
+ "This should never happen.");
+ }
+ }
+
+ Task.spawn(function() {
+ if (!aForcePrompt) {
+ // Check to see if the user wishes to auto save to the default download
+ // folder without prompting. Note that preference might not be set.
+ let autodownload = false;
+ try {
+ autodownload = prefs.getBoolPref(PREF_BD_USEDOWNLOADDIR);
+ } catch (e) { }
+
+ if (autodownload) {
+ // Retrieve the user's default download directory
+ let preferredDir = yield Downloads.getPreferredDownloadsDirectory();
+ let defaultFolder = new FileUtils.File(preferredDir);
+
+ try {
+ result = this.validateLeafName(defaultFolder, aDefaultFile, aSuggestedFileExtension);
+ }
+ catch (ex) {
+ // When the default download directory is write-protected,
+ // prompt the user for a different target file.
+ }
+
+ // Check to make sure we have a valid directory, otherwise, prompt
+ if (result) {
+ // This path is taken when we have a writable default download directory.
+ aLauncher.saveDestinationAvailable(result);
+ return;
+ }
+ }
+ }
+
+ // Use file picker to show dialog.
+ var nsIFilePicker = Components.interfaces.nsIFilePicker;
+ var picker = Components.classes["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
+ var windowTitle = bundle.GetStringFromName("saveDialogTitle");
+ picker.init(parent, windowTitle, nsIFilePicker.modeSave);
+ picker.defaultString = aDefaultFile;
+
+ if (aSuggestedFileExtension) {
+ // aSuggestedFileExtension includes the period, so strip it
+ picker.defaultExtension = aSuggestedFileExtension.substring(1);
+ }
+ else {
+ try {
+ picker.defaultExtension = this.mLauncher.MIMEInfo.primaryExtension;
+ }
+ catch (ex) { }
+ }
+
+ var wildCardExtension = "*";
+ if (aSuggestedFileExtension) {
+ wildCardExtension += aSuggestedFileExtension;
+ picker.appendFilter(this.mLauncher.MIMEInfo.description, wildCardExtension);
+ }
+
+ picker.appendFilters( nsIFilePicker.filterAll );
+
+ // Default to lastDir if it is valid, otherwise use the user's default
+ // downloads directory. getPreferredDownloadsDirectory should always
+ // return a valid directory path, so we can safely default to it.
+ let preferredDir = yield Downloads.getPreferredDownloadsDirectory();
+ picker.displayDirectory = new FileUtils.File(preferredDir);
+
+ gDownloadLastDir.getFileAsync(aLauncher.source, function LastDirCallback(lastDir) {
+ if (lastDir && isUsableDirectory(lastDir))
+ picker.displayDirectory = lastDir;
+
+ if (picker.show() == nsIFilePicker.returnCancel) {
+ // null result means user cancelled.
+ aLauncher.saveDestinationAvailable(null);
+ return;
+ }
+
+ // Be sure to save the directory the user chose through the Save As...
+ // dialog as the new browser.download.dir since the old one
+ // didn't exist.
+ result = picker.file;
+
+ if (result) {
+ try {
+ // Remove the file so that it's not there when we ensure non-existence later;
+ // this is safe because for the file to exist, the user would have had to
+ // confirm that he wanted the file overwritten.
+ // Only remove file if final name exists
+ if (result.exists() && this.getFinalLeafName(result.leafName) == result.leafName)
+ result.remove(false);
+ }
+ catch (ex) {
+ // As it turns out, the failure to remove the file, for example due to
+ // permission error, will be handled below eventually somehow.
+ }
+
+ var newDir = result.parent.QueryInterface(Components.interfaces.nsILocalFile);
+
+ // Do not store the last save directory as a pref inside the private browsing mode
+ gDownloadLastDir.setFile(aLauncher.source, newDir);
+
+ try {
+ result = this.validateLeafName(newDir, result.leafName, null);
+ }
+ catch (ex) {
+ // When the chosen download directory is write-protected,
+ // display an informative error message.
+ // In all cases, download will be stopped.
+
+ if (ex.result == Components.results.NS_ERROR_FILE_ACCESS_DENIED) {
+ this.displayBadPermissionAlert();
+ aLauncher.saveDestinationAvailable(null);
+ return;
+ }
+
+ }
+ }
+ aLauncher.saveDestinationAvailable(result);
+ }.bind(this));
+ }.bind(this)).then(null, Components.utils.reportError);
+ },
+
+ getFinalLeafName: function (aLeafName, aFileExt)
+ {
+ // Remove any leading periods, since we don't want to save hidden files
+ // automatically.
+ aLeafName = aLeafName.replace(/^\.+/, "");
+
+ if (aLeafName == "")
+ aLeafName = "unnamed" + (aFileExt ? "." + aFileExt : "");
+
+ return aLeafName;
+ },
+
+ /**
+ * Ensures that a local folder/file combination does not already exist in
+ * the file system (or finds such a combination with a reasonably similar
+ * leaf name), creates the corresponding file, and returns it.
+ *
+ * @param aLocalFolder
+ * the folder where the file resides
+ * @param aLeafName
+ * the string name of the file (may be empty if no name is known,
+ * in which case a name will be chosen)
+ * @param aFileExt
+ * the extension of the file, if one is known; this will be ignored
+ * if aLeafName is non-empty
+ * @return nsILocalFile
+ * the created file
+ * @throw an error such as permission doesn't allow creation of
+ * file, etc.
+ */
+ validateLeafName: function (aLocalFolder, aLeafName, aFileExt)
+ {
+ if (!(aLocalFolder && isUsableDirectory(aLocalFolder))) {
+ throw new Components.Exception("Destination directory non-existing or permission error",
+ Components.results.NS_ERROR_FILE_ACCESS_DENIED);
+ }
+
+ aLeafName = this.getFinalLeafName(aLeafName, aFileExt);
+ aLocalFolder.append(aLeafName);
+
+ // The following assignment can throw an exception, but
+ // is now caught properly in the caller of validateLeafName.
+ var createdFile = DownloadPaths.createNiceUniqueFile(aLocalFolder);
+
+ if (AppConstants.platform == "win") {
+ let ext;
+ try {
+ // We can fail here if there's no primary extension set
+ ext = "." + this.mLauncher.MIMEInfo.primaryExtension;
+ } catch (e) { }
+
+ // Append a file extension if it's an executable that doesn't have one
+ // but make sure we actually have an extension to add
+ let leaf = createdFile.leafName;
+ if (ext && leaf.slice(-ext.length) != ext && createdFile.isExecutable()) {
+ createdFile.remove(false);
+ aLocalFolder.leafName = leaf + ext;
+ createdFile = DownloadPaths.createNiceUniqueFile(aLocalFolder);
+ }
+ }
+
+ return createdFile;
+ },
+
+ // ---------- implementation methods ----------
+
+ // initDialog: Fill various dialog fields with initial content.
+ initDialog : function() {
+ // Put file name in window title.
+ var suggestedFileName = this.mLauncher.suggestedFileName;
+
+ // Some URIs do not implement nsIURL, so we can't just QI.
+ var url = this.mLauncher.source;
+ if (url instanceof Components.interfaces.nsINestedURI)
+ url = url.innermostURI;
+
+ var fname = "";
+ var iconPath = "goat";
+ this.mSourcePath = url.prePath;
+ if (url instanceof Components.interfaces.nsIURL) {
+ // A url, use file name from it.
+ fname = iconPath = url.fileName;
+ this.mSourcePath += url.directory;
+ } else {
+ // A generic uri, use path.
+ fname = url.path;
+ this.mSourcePath += url.path;
+ }
+
+ if (suggestedFileName)
+ fname = iconPath = suggestedFileName;
+
+ var displayName = fname.replace(/ +/g, " ");
+
+ this.mTitle = this.dialogElement("strings").getFormattedString("title", [displayName]);
+ this.mDialog.document.title = this.mTitle;
+
+ // Put content type, filename and location into intro.
+ this.initIntro(url, fname, displayName);
+
+ var iconString = "moz-icon://" + iconPath + "?size=16&contentType=" + this.mLauncher.MIMEInfo.MIMEType;
+ this.dialogElement("contentTypeImage").setAttribute("src", iconString);
+
+ // if always-save and is-executable and no-handler
+ // then set up simple ui
+ var mimeType = this.mLauncher.MIMEInfo.MIMEType;
+ var shouldntRememberChoice = (mimeType == "application/octet-stream" ||
+ mimeType == "application/x-msdownload" ||
+ this.mLauncher.targetFileIsExecutable);
+ if ((shouldntRememberChoice && !this.openWithDefaultOK()) ||
+ Services.prefs.getBoolPref("browser.download.forbid_open_with")) {
+ // hide featured choice
+ this.dialogElement("normalBox").collapsed = true;
+ // show basic choice
+ this.dialogElement("basicBox").collapsed = false;
+ // change button labels and icons; use "save" icon for the accept
+ // button since it's the only action possible
+ let acceptButton = this.mDialog.document.documentElement
+ .getButton("accept");
+ acceptButton.label = this.dialogElement("strings")
+ .getString("unknownAccept.label");
+ acceptButton.setAttribute("icon", "save");
+ this.mDialog.document.documentElement.getButton("cancel").label = this.dialogElement("strings").getString("unknownCancel.label");
+ // hide other handler
+ this.dialogElement("openHandler").collapsed = true;
+ // set save as the selected option
+ this.dialogElement("mode").selectedItem = this.dialogElement("save");
+ }
+ else {
+ this.initAppAndSaveToDiskValues();
+
+ // Initialize "always ask me" box. This should always be disabled
+ // and set to true for the ambiguous type application/octet-stream.
+ // We don't also check for application/x-msdownload here since we
+ // want users to be able to autodownload .exe files.
+ var rememberChoice = this.dialogElement("rememberChoice");
+
+ // Just because we have a content-type of application/octet-stream
+ // here doesn't actually mean that the content is of that type. Many
+ // servers default to sending text/plain for file types they don't know
+ // about. To account for this, the uriloader does some checking to see
+ // if a file sent as text/plain contains binary characters, and if so (*)
+ // it morphs the content-type into application/octet-stream so that
+ // the file can be properly handled. Since this is not generic binary
+ // data, rather, a data format that the system probably knows about,
+ // we don't want to use the content-type provided by this dialog's
+ // opener, as that's the generic application/octet-stream that the
+ // uriloader has passed, rather we want to ask the MIME Service.
+ // This is so we don't needlessly disable the "autohandle" checkbox.
+
+ // commented out to close the opening brace in the if statement.
+ // var mimeService = Components.classes["@mozilla.org/mime;1"].getService(Components.interfaces.nsIMIMEService);
+ // var type = mimeService.getTypeFromURI(this.mLauncher.source);
+ // this.realMIMEInfo = mimeService.getFromTypeAndExtension(type, "");
+
+ // if (type == "application/octet-stream") {
+ if (shouldntRememberChoice) {
+ rememberChoice.checked = false;
+ rememberChoice.disabled = true;
+ }
+ else {
+ rememberChoice.checked = !this.mLauncher.MIMEInfo.alwaysAskBeforeHandling &&
+ this.mLauncher.MIMEInfo.preferredAction != this.nsIMIMEInfo.handleInternally;
+ }
+ this.toggleRememberChoice(rememberChoice);
+
+ // XXXben - menulist won't init properly, hack.
+ var openHandler = this.dialogElement("openHandler");
+ openHandler.parentNode.removeChild(openHandler);
+ var openHandlerBox = this.dialogElement("openHandlerBox");
+ openHandlerBox.appendChild(openHandler);
+ }
+
+ this.mDialog.setTimeout("dialog.postShowCallback()", 0);
+
+ this.delayHelper = new EnableDelayHelper({
+ disableDialog: () => {
+ this.mDialog.document.documentElement.getButton("accept").disabled = true;
+ },
+ enableDialog: () => {
+ this.mDialog.document.documentElement.getButton("accept").disabled = false;
+ },
+ focusTarget: this.mDialog
+ });
+ },
+
+ notify: function (aTimer) {
+ if (aTimer == this._showTimer) {
+ if (!this.mDialog) {
+ this.reallyShow();
+ }
+ // The timer won't release us, so we have to release it.
+ this._showTimer = null;
+ }
+ else if (aTimer == this._saveToDiskTimer) {
+ // Since saveToDisk may open a file picker and therefore block this routine,
+ // we should only call it once the dialog is closed.
+ this.mLauncher.saveToDisk(null, false);
+ this._saveToDiskTimer = null;
+ }
+ },
+
+ postShowCallback: function () {
+ this.mDialog.sizeToContent();
+
+ // Set initial focus
+ this.dialogElement("mode").focus();
+ },
+
+ // initIntro:
+ initIntro: function(url, filename, displayname) {
+ this.dialogElement( "location" ).value = displayname;
+ this.dialogElement( "location" ).setAttribute("realname", filename);
+ this.dialogElement( "location" ).setAttribute("tooltiptext", displayname);
+
+ // if mSourcePath is a local file, then let's use the pretty path name
+ // instead of an ugly url...
+ var pathString;
+ if (url instanceof Components.interfaces.nsIFileURL) {
+ try {
+ // Getting .file might throw, or .parent could be null
+ pathString = url.file.parent.path;
+ } catch (ex) {}
+ }
+
+ if (!pathString) {
+ // wasn't a fileURL
+ var tmpurl = url.clone(); // don't want to change the real url
+ try {
+ tmpurl.userPass = "";
+ } catch (ex) {}
+ pathString = tmpurl.prePath;
+ }
+
+ // Set the location text, which is separate from the intro text so it can be cropped
+ var location = this.dialogElement( "source" );
+ location.value = pathString;
+ location.setAttribute("tooltiptext", this.mSourcePath);
+
+ // Show the type of file.
+ var type = this.dialogElement("type");
+ var mimeInfo = this.mLauncher.MIMEInfo;
+
+ // 1. Try to use the pretty description of the type, if one is available.
+ var typeString = mimeInfo.description;
+
+ if (typeString == "") {
+ // 2. If there is none, use the extension to identify the file, e.g. "ZIP file"
+ var primaryExtension = "";
+ try {
+ primaryExtension = mimeInfo.primaryExtension;
+ }
+ catch (ex) {
+ }
+ if (primaryExtension != "")
+ typeString = this.dialogElement("strings").getFormattedString("fileType", [primaryExtension.toUpperCase()]);
+ // 3. If we can't even do that, just give up and show the MIME type.
+ else
+ typeString = mimeInfo.MIMEType;
+ }
+ // When the length is unknown, contentLength would be -1
+ if (this.mLauncher.contentLength >= 0) {
+ let [size, unit] = DownloadUtils.
+ convertByteUnits(this.mLauncher.contentLength);
+ type.value = this.dialogElement("strings")
+ .getFormattedString("orderedFileSizeWithType",
+ [typeString, size, unit]);
+ }
+ else {
+ type.value = typeString;
+ }
+ },
+
+ // Returns true if opening the default application makes sense.
+ openWithDefaultOK: function() {
+ // The checking is different on Windows...
+ if (AppConstants.platform == "win") {
+ // Windows presents some special cases.
+ // We need to prevent use of "system default" when the file is
+ // executable (so the user doesn't launch nasty programs downloaded
+ // from the web), and, enable use of "system default" if it isn't
+ // executable (because we will prompt the user for the default app
+ // in that case).
+
+ // Default is Ok if the file isn't executable (and vice-versa).
+ return !this.mLauncher.targetFileIsExecutable;
+ }
+ // On other platforms, default is Ok if there is a default app.
+ // Note that nsIMIMEInfo providers need to ensure that this holds true
+ // on each platform.
+ return this.mLauncher.MIMEInfo.hasDefaultHandler;
+ },
+
+ // Set "default" application description field.
+ initDefaultApp: function() {
+ // Use description, if we can get one.
+ var desc = this.mLauncher.MIMEInfo.defaultDescription;
+ if (desc) {
+ var defaultApp = this.dialogElement("strings").getFormattedString("defaultApp", [desc]);
+ this.dialogElement("defaultHandler").label = defaultApp;
+ }
+ else {
+ this.dialogElement("modeDeck").setAttribute("selectedIndex", "1");
+ // Hide the default handler item too, in case the user picks a
+ // custom handler at a later date which triggers the menulist to show.
+ this.dialogElement("defaultHandler").hidden = true;
+ }
+ },
+
+ // getPath:
+ getPath: function (aFile) {
+ if (AppConstants.platform == "macosx") {
+ return aFile.leafName || aFile.path;
+ }
+ return aFile.path;
+ },
+
+ // initAppAndSaveToDiskValues:
+ initAppAndSaveToDiskValues: function() {
+ var modeGroup = this.dialogElement("mode");
+
+ // We don't let users open .exe files or random binary data directly
+ // from the browser at the moment because of security concerns.
+ var openWithDefaultOK = this.openWithDefaultOK();
+ var mimeType = this.mLauncher.MIMEInfo.MIMEType;
+ if (this.mLauncher.targetFileIsExecutable || (
+ (mimeType == "application/octet-stream" ||
+ mimeType == "application/x-msdownload") &&
+ !openWithDefaultOK)) {
+ this.dialogElement("open").disabled = true;
+ var openHandler = this.dialogElement("openHandler");
+ openHandler.disabled = true;
+ openHandler.selectedItem = null;
+ modeGroup.selectedItem = this.dialogElement("save");
+ return;
+ }
+
+ // Fill in helper app info, if there is any.
+ try {
+ this.chosenApp =
+ this.mLauncher.MIMEInfo.preferredApplicationHandler
+ .QueryInterface(Components.interfaces.nsILocalHandlerApp);
+ } catch (e) {
+ this.chosenApp = null;
+ }
+ // Initialize "default application" field.
+ this.initDefaultApp();
+
+ var otherHandler = this.dialogElement("otherHandler");
+
+ // Fill application name textbox.
+ if (this.chosenApp && this.chosenApp.executable &&
+ this.chosenApp.executable.path) {
+ otherHandler.setAttribute("path",
+ this.getPath(this.chosenApp.executable));
+
+ otherHandler.label = this.getFileDisplayName(this.chosenApp.executable);
+ otherHandler.hidden = false;
+ }
+
+ var openHandler = this.dialogElement("openHandler");
+ openHandler.selectedIndex = 0;
+ var defaultOpenHandler = this.dialogElement("defaultHandler");
+
+ if (this.mLauncher.MIMEInfo.preferredAction == this.nsIMIMEInfo.useSystemDefault) {
+ // Open (using system default).
+ modeGroup.selectedItem = this.dialogElement("open");
+ } else if (this.mLauncher.MIMEInfo.preferredAction == this.nsIMIMEInfo.useHelperApp) {
+ // Open with given helper app.
+ modeGroup.selectedItem = this.dialogElement("open");
+ openHandler.selectedItem = (otherHandler && !otherHandler.hidden) ?
+ otherHandler : defaultOpenHandler;
+ } else {
+ // Save to disk.
+ modeGroup.selectedItem = this.dialogElement("save");
+ }
+
+ // If we don't have a "default app" then disable that choice.
+ if (!openWithDefaultOK) {
+ var isSelected = defaultOpenHandler.selected;
+
+ // Disable that choice.
+ defaultOpenHandler.hidden = true;
+ // If that's the default, then switch to "save to disk."
+ if (isSelected) {
+ openHandler.selectedIndex = 1;
+ modeGroup.selectedItem = this.dialogElement("save");
+ }
+ }
+
+ otherHandler.nextSibling.hidden = otherHandler.nextSibling.nextSibling.hidden = false;
+ this.updateOKButton();
+ },
+
+ // Returns the user-selected application
+ helperAppChoice: function() {
+ return this.chosenApp;
+ },
+
+ get saveToDisk() {
+ return this.dialogElement("save").selected;
+ },
+
+ get useOtherHandler() {
+ return this.dialogElement("open").selected && this.dialogElement("openHandler").selectedIndex == 1;
+ },
+
+ get useSystemDefault() {
+ return this.dialogElement("open").selected && this.dialogElement("openHandler").selectedIndex == 0;
+ },
+
+ toggleRememberChoice: function (aCheckbox) {
+ this.dialogElement("settingsChange").hidden = !aCheckbox.checked;
+ this.mDialog.sizeToContent();
+ },
+
+ openHandlerCommand: function () {
+ var openHandler = this.dialogElement("openHandler");
+ if (openHandler.selectedItem.id == "choose")
+ this.chooseApp();
+ else
+ openHandler.setAttribute("lastSelectedItemID", openHandler.selectedItem.id);
+ },
+
+ updateOKButton: function() {
+ var ok = false;
+ if (this.dialogElement("save").selected) {
+ // This is always OK.
+ ok = true;
+ }
+ else if (this.dialogElement("open").selected) {
+ switch (this.dialogElement("openHandler").selectedIndex) {
+ case 0:
+ // No app need be specified in this case.
+ ok = true;
+ break;
+ case 1:
+ // only enable the OK button if we have a default app to use or if
+ // the user chose an app....
+ ok = this.chosenApp || /\S/.test(this.dialogElement("otherHandler").getAttribute("path"));
+ break;
+ }
+ }
+
+ // Enable Ok button if ok to press.
+ this.mDialog.document.documentElement.getButton("accept").disabled = !ok;
+ },
+
+ // Returns true iff the user-specified helper app has been modified.
+ appChanged: function() {
+ return this.helperAppChoice() != this.mLauncher.MIMEInfo.preferredApplicationHandler;
+ },
+
+ updateMIMEInfo: function() {
+ // Don't update mime type preferences when the preferred action is set to
+ // the internal handler -- this dialog is the result of the handler fallback
+ // (e.g. Content-Disposition was set as attachment)
+ var discardUpdate = this.mLauncher.MIMEInfo.preferredAction == this.nsIMIMEInfo.handleInternally &&
+ !this.dialogElement("rememberChoice").checked;
+
+ var needUpdate = false;
+ // If current selection differs from what's in the mime info object,
+ // then we need to update.
+ if (this.saveToDisk) {
+ needUpdate = this.mLauncher.MIMEInfo.preferredAction != this.nsIMIMEInfo.saveToDisk;
+ if (needUpdate)
+ this.mLauncher.MIMEInfo.preferredAction = this.nsIMIMEInfo.saveToDisk;
+ }
+ else if (this.useSystemDefault) {
+ needUpdate = this.mLauncher.MIMEInfo.preferredAction != this.nsIMIMEInfo.useSystemDefault;
+ if (needUpdate)
+ this.mLauncher.MIMEInfo.preferredAction = this.nsIMIMEInfo.useSystemDefault;
+ }
+ else {
+ // For "open with", we need to check both preferred action and whether the user chose
+ // a new app.
+ needUpdate = this.mLauncher.MIMEInfo.preferredAction != this.nsIMIMEInfo.useHelperApp || this.appChanged();
+ if (needUpdate) {
+ this.mLauncher.MIMEInfo.preferredAction = this.nsIMIMEInfo.useHelperApp;
+ // App may have changed - Update application
+ var app = this.helperAppChoice();
+ this.mLauncher.MIMEInfo.preferredApplicationHandler = app;
+ }
+ }
+ // We will also need to update if the "always ask" flag has changed.
+ needUpdate = needUpdate || this.mLauncher.MIMEInfo.alwaysAskBeforeHandling != (!this.dialogElement("rememberChoice").checked);
+
+ // One last special case: If the input "always ask" flag was false, then we always
+ // update. In that case we are displaying the helper app dialog for the first
+ // time for this mime type and we need to store the user's action in the mimeTypes.rdf
+ // data source (whether that action has changed or not; if it didn't change, then we need
+ // to store the "always ask" flag so the helper app dialog will or won't display
+ // next time, per the user's selection).
+ needUpdate = needUpdate || !this.mLauncher.MIMEInfo.alwaysAskBeforeHandling;
+
+ // Make sure mime info has updated setting for the "always ask" flag.
+ this.mLauncher.MIMEInfo.alwaysAskBeforeHandling = !this.dialogElement("rememberChoice").checked;
+
+ return needUpdate && !discardUpdate;
+ },
+
+ // See if the user changed things, and if so, update the
+ // mimeTypes.rdf entry for this mime type.
+ updateHelperAppPref: function() {
+ var handlerInfo = this.mLauncher.MIMEInfo;
+ var hs = Cc["@mozilla.org/uriloader/handler-service;1"].getService(Ci.nsIHandlerService);
+ hs.store(handlerInfo);
+ },
+
+ // onOK:
+ onOK: function() {
+ // Verify typed app path, if necessary.
+ if (this.useOtherHandler) {
+ var helperApp = this.helperAppChoice();
+ if (!helperApp || !helperApp.executable ||
+ !helperApp.executable.exists()) {
+ // Show alert and try again.
+ var bundle = this.dialogElement("strings");
+ var msg = bundle.getFormattedString("badApp", [this.dialogElement("otherHandler").getAttribute("path")]);
+ Services.prompt.alert(this.mDialog, bundle.getString("badApp.title"), msg);
+
+ // Disable the OK button.
+ this.mDialog.document.documentElement.getButton("accept").disabled = true;
+ this.dialogElement("mode").focus();
+
+ // Clear chosen application.
+ this.chosenApp = null;
+
+ // Leave dialog up.
+ return false;
+ }
+ }
+
+ // Remove our web progress listener (a progress dialog will be
+ // taking over).
+ this.mLauncher.setWebProgressListener(null);
+
+ // saveToDisk and launchWithApplication can return errors in
+ // certain circumstances (e.g. The user clicks cancel in the
+ // "Save to Disk" dialog. In those cases, we don't want to
+ // update the helper application preferences in the RDF file.
+ try {
+ var needUpdate = this.updateMIMEInfo();
+
+ if (this.dialogElement("save").selected) {
+ // If we're using a default download location, create a path
+ // for the file to be saved to to pass to |saveToDisk| - otherwise
+ // we must ask the user to pick a save name.
+
+ /*
+ var prefs = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch);
+ var targetFile = null;
+ try {
+ targetFile = prefs.getComplexValue("browser.download.defaultFolder",
+ Components.interfaces.nsILocalFile);
+ var leafName = this.dialogElement("location").getAttribute("realname");
+ // Ensure that we don't overwrite any existing files here.
+ targetFile = this.validateLeafName(targetFile, leafName, null);
+ }
+ catch(e) { }
+
+ this.mLauncher.saveToDisk(targetFile, false);
+ */
+
+ // see @notify
+ // we cannot use opener's setTimeout, see bug 420405
+ this._saveToDiskTimer = Components.classes["@mozilla.org/timer;1"]
+ .createInstance(nsITimer);
+ this._saveToDiskTimer.initWithCallback(this, 0,
+ nsITimer.TYPE_ONE_SHOT);
+ }
+ else
+ this.mLauncher.launchWithApplication(null, false);
+
+ // Update user pref for this mime type (if necessary). We do not
+ // store anything in the mime type preferences for the ambiguous
+ // type application/octet-stream. We do NOT do this for
+ // application/x-msdownload since we want users to be able to
+ // autodownload these to disk.
+ if (needUpdate && this.mLauncher.MIMEInfo.MIMEType != "application/octet-stream")
+ this.updateHelperAppPref();
+ } catch(e) { }
+
+ // Unhook dialog from this object.
+ this.mDialog.dialog = null;
+
+ // Close up dialog by returning true.
+ return true;
+ },
+
+ // onCancel:
+ onCancel: function() {
+ // Remove our web progress listener.
+ this.mLauncher.setWebProgressListener(null);
+
+ // Cancel app launcher.
+ try {
+ this.mLauncher.cancel(Components.results.NS_BINDING_ABORTED);
+ } catch(exception) {
+ }
+
+ // Unhook dialog from this object.
+ this.mDialog.dialog = null;
+
+ // Close up dialog by returning true.
+ return true;
+ },
+
+ // dialogElement: Convenience.
+ dialogElement: function(id) {
+ return this.mDialog.document.getElementById(id);
+ },
+
+ // Retrieve the pretty description from the file
+ getFileDisplayName: function getFileDisplayName(file)
+ {
+ if (AppConstants.platform == "win") {
+ if (file instanceof Components.interfaces.nsILocalFileWin) {
+ try {
+ return file.getVersionInfoField("FileDescription");
+ } catch (e) {}
+ }
+ } else if (AppConstants.platform == "macosx") {
+ if (file instanceof Components.interfaces.nsILocalFileMac) {
+ try {
+ return file.bundleDisplayName;
+ } catch (e) {}
+ }
+ }
+ return file.leafName;
+ },
+
+ finishChooseApp: function() {
+ if (this.chosenApp) {
+ // Show the "handler" menulist since we have a (user-specified)
+ // application now.
+ this.dialogElement("modeDeck").setAttribute("selectedIndex", "0");
+
+ // Update dialog.
+ var otherHandler = this.dialogElement("otherHandler");
+ otherHandler.removeAttribute("hidden");
+ otherHandler.setAttribute("path", this.getPath(this.chosenApp.executable));
+ if (AppConstants.platform == "win")
+ otherHandler.label = this.getFileDisplayName(this.chosenApp.executable);
+ else
+ otherHandler.label = this.chosenApp.name;
+ this.dialogElement("openHandler").selectedIndex = 1;
+ this.dialogElement("openHandler").setAttribute("lastSelectedItemID", "otherHandler");
+
+ this.dialogElement("mode").selectedItem = this.dialogElement("open");
+ }
+ else {
+ var openHandler = this.dialogElement("openHandler");
+ var lastSelectedID = openHandler.getAttribute("lastSelectedItemID");
+ if (!lastSelectedID)
+ lastSelectedID = "defaultHandler";
+ openHandler.selectedItem = this.dialogElement(lastSelectedID);
+ }
+ },
+ // chooseApp: Open file picker and prompt user for application.
+ chooseApp: function() {
+ if (AppConstants.platform == "win") {
+ // Protect against the lack of an extension
+ var fileExtension = "";
+ try {
+ fileExtension = this.mLauncher.MIMEInfo.primaryExtension;
+ } catch(ex) {
+ }
+
+ // Try to use the pretty description of the type, if one is available.
+ var typeString = this.mLauncher.MIMEInfo.description;
+
+ if (!typeString) {
+ // If there is none, use the extension to
+ // identify the file, e.g. "ZIP file"
+ if (fileExtension) {
+ typeString =
+ this.dialogElement("strings").
+ getFormattedString("fileType", [fileExtension.toUpperCase()]);
+ } else {
+ // If we can't even do that, just give up and show the MIME type.
+ typeString = this.mLauncher.MIMEInfo.MIMEType;
+ }
+ }
+
+ var params = {};
+ params.title =
+ this.dialogElement("strings").getString("chooseAppFilePickerTitle");
+ params.description = typeString;
+ params.filename = this.mLauncher.suggestedFileName;
+ params.mimeInfo = this.mLauncher.MIMEInfo;
+ params.handlerApp = null;
+
+ this.mDialog.openDialog("chrome://global/content/appPicker.xul", null,
+ "chrome,modal,centerscreen,titlebar,dialog=yes",
+ params);
+
+ if (params.handlerApp &&
+ params.handlerApp.executable &&
+ params.handlerApp.executable.isFile()) {
+ // Remember the file they chose to run.
+ this.chosenApp = params.handlerApp;
+ }
+ }
+ else {
+#if MOZ_WIDGET_GTK == 3
+ var nsIApplicationChooser = Components.interfaces.nsIApplicationChooser;
+ var appChooser = Components.classes["@mozilla.org/applicationchooser;1"]
+ .createInstance(nsIApplicationChooser);
+ appChooser.init(this.mDialog, this.dialogElement("strings").getString("chooseAppFilePickerTitle"));
+ var contentTypeDialogObj = this;
+ let appChooserCallback = function appChooserCallback_done(aResult) {
+ if (aResult) {
+ contentTypeDialogObj.chosenApp = aResult.QueryInterface(Components.interfaces.nsILocalHandlerApp);
+ }
+ contentTypeDialogObj.finishChooseApp();
+ };
+ appChooser.open(this.mLauncher.MIMEInfo.MIMEType, appChooserCallback);
+ // The finishChooseApp is called from appChooserCallback
+ return;
+#else
+ var nsIFilePicker = Components.interfaces.nsIFilePicker;
+ var fp = Components.classes["@mozilla.org/filepicker;1"]
+ .createInstance(nsIFilePicker);
+ fp.init(this.mDialog,
+ this.dialogElement("strings").getString("chooseAppFilePickerTitle"),
+ nsIFilePicker.modeOpen);
+
+ fp.appendFilters(nsIFilePicker.filterApps);
+
+ if (fp.show() == nsIFilePicker.returnOK && fp.file) {
+ // Remember the file they chose to run.
+ var localHandlerApp =
+ Components.classes["@mozilla.org/uriloader/local-handler-app;1"].
+ createInstance(Components.interfaces.nsILocalHandlerApp);
+ localHandlerApp.executable = fp.file;
+ this.chosenApp = localHandlerApp;
+ }
+#endif // MOZ_WIDGET_GTK == 3
+ }
+ this.finishChooseApp();
+ },
+
+ // Turn this on to get debugging messages.
+ debug: false,
+
+ // Dump text (if debug is on).
+ dump: function( text ) {
+ if ( this.debug ) {
+ dump( text );
+ }
+ },
+
+ // dumpObj:
+ dumpObj: function( spec ) {
+ var val = "<undefined>";
+ try {
+ val = eval( "this."+spec ).toString();
+ } catch( exception ) {
+ }
+ this.dump( spec + "=" + val + "\n" );
+ },
+
+ // dumpObjectProperties
+ dumpObjectProperties: function( desc, obj ) {
+ for( prop in obj ) {
+ this.dump( desc + "." + prop + "=" );
+ var val = "<undefined>";
+ try {
+ val = obj[ prop ];
+ } catch ( exception ) {
+ }
+ this.dump( val + "\n" );
+ }
+ }
+}
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([nsUnknownContentTypeDialog]);