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