/* 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/. */ import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; import { BrowserUtils } from "resource://gre/modules/BrowserUtils.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { EnableDelayHelper: "resource://gre/modules/PromptUtils.sys.mjs", }); XPCOMUtils.defineLazyServiceGetter( lazy, "gReputationService", "@mozilla.org/reputationservice/application-reputation-service;1", Ci.nsIApplicationReputationService ); XPCOMUtils.defineLazyServiceGetter( lazy, "gMIMEService", "@mozilla.org/mime;1", Ci.nsIMIMEService ); import { Integration } from "resource://gre/modules/Integration.sys.mjs"; Integration.downloads.defineESModuleGetter( lazy, "DownloadIntegration", "resource://gre/modules/DownloadIntegration.sys.mjs" ); // ///////////////////////////////////////////////////////////////////////////// // // 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(aWebProgress, aRequest, aStatus, aMessage) { if (aStatus != Cr.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, onContentBlockingEvent and onRefreshAttempted notifications. onProgressChange( aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress ) {}, onProgressChange64( aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress ) {}, onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {}, onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {}, onSecurityChange(aWebProgress, aRequest, aState) {}, onContentBlockingEvent(aWebProgress, aRequest, aEvent) {}, onRefreshAttempted(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 */ const PREF_BD_USEDOWNLOADDIR = "browser.download.useDownloadDir"; const nsITimer = Ci.nsITimer; import * as downloadModule from "resource://gre/modules/DownloadLastDir.sys.mjs"; import { DownloadPaths } from "resource://gre/modules/DownloadPaths.sys.mjs"; import { DownloadUtils } from "resource://gre/modules/DownloadUtils.sys.mjs"; import { Downloads } from "resource://gre/modules/Downloads.sys.mjs"; import { FileUtils } from "resource://gre/modules/FileUtils.sys.mjs"; /* ctor */ export 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: Ci.nsIMIMEInfo, QueryInterface: ChromeUtils.generateQI([ "nsIHelperAppLauncherDialog", "nsITimerCallback", ]), // ---------- 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(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.getInterface(Ci.nsIDOMWindow); this._mDownloadDir = new downloadModule.DownloadLastDir(parent); } catch (ex) { console.error( "Missing window information when showing nsIHelperAppLauncherDialog: " + ex ); } const nsITimer = Ci.nsITimer; this._showTimer = Cc["@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() { try { let docShell = this.mContext.getInterface(Ci.nsIDocShell); let rootWin = docShell.browsingContext.topChromeWindow; this.mDialog = Services.ww.openWindow( rootWin, "chrome://mozapps/content/downloads/unknownContentType.xhtml", 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(Cr.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() { let bundle = Services.strings.createBundle( "chrome://mozapps/locale/downloads/unknownContentType.properties" ); Services.prompt.alert( this.dialog, bundle.GetStringFromName("badPermissions.title"), bundle.GetStringFromName("badPermissions") ); }, promptForSaveToFileAsync( aLauncher, aContext, aDefaultFileName, aSuggestedFileExtension, aForcePrompt ) { var result = null; this.mLauncher = aLauncher; let bundle = Services.strings.createBundle( "chrome://mozapps/locale/downloads/unknownContentType.properties" ); let parent; let gDownloadLastDir; try { parent = aContext.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; for (let someWin of Services.wm.getEnumerator("")) { // 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) { console.error( "No candidate parent windows were found for the save filepicker." + "This should never happen." ); } } (async () => { 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 = Services.prefs.getBoolPref( PREF_BD_USEDOWNLOADDIR, false ); if (autodownload) { // Retrieve the user's default download directory let preferredDir = await Downloads.getPreferredDownloadsDirectory(); let defaultFolder = new FileUtils.File(preferredDir); try { if (aDefaultFileName) { result = this.validateLeafName( defaultFolder, aDefaultFileName, 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 = Ci.nsIFilePicker; var picker = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker); var windowTitle = bundle.GetStringFromName("saveDialogTitle"); picker.init(parent, windowTitle, nsIFilePicker.modeSave); if (aDefaultFileName) { picker.defaultString = this.getFinalLeafName(aDefaultFileName); } 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 = await Downloads.getPreferredDownloadsDirectory(); picker.displayDirectory = new FileUtils.File(preferredDir); gDownloadLastDir.getFileAsync(aLauncher.source).then(lastDir => { if (lastDir && isUsableDirectory(lastDir)) { picker.displayDirectory = lastDir; } picker.open(returnValue => { if (returnValue == 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) { let allowOverwrite = false; try { // If we're overwriting, avoid renaming our file, and assume // overwriting it does the right thing. if ( result.exists() && this.getFinalLeafName(result.leafName, "", true) == result.leafName ) { allowOverwrite = true; } } 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(Ci.nsIFile); // 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, allowOverwrite, true ); } 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 == Cr.NS_ERROR_FILE_ACCESS_DENIED) { this.displayBadPermissionAlert(); aLauncher.saveDestinationAvailable(null); return; } } } // Don't pop up the downloads panel redundantly. aLauncher.saveDestinationAvailable(result, true); }); }); })().catch(console.error); }, getFinalLeafName(aLeafName, aFileExt, aAfterFilePicker) { return ( DownloadPaths.sanitize(aLeafName, { compressWhitespaces: !aAfterFilePicker, allowInvalidFilenames: aAfterFilePicker, }) || "unnamed" + (aFileExt ? "." + aFileExt : "") ); }, /** * 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 * @param aAllowExisting * if set to true, avoid creating a unique file. * @param aAfterFilePicker * if set to true, this was a file entered by the user from a file picker. * @return nsIFile * the created file * @throw an error such as permission doesn't allow creation of * file, etc. */ validateLeafName( aLocalFolder, aLeafName, aFileExt, aAllowExisting = false, aAfterFilePicker = false ) { if (!(aLocalFolder && isUsableDirectory(aLocalFolder))) { throw new Components.Exception( "Destination directory non-existing or permission error", Cr.NS_ERROR_FILE_ACCESS_DENIED ); } aLeafName = this.getFinalLeafName(aLeafName, aFileExt, aAfterFilePicker); aLocalFolder.append(aLeafName); if (!aAllowExisting) { // The following assignment can throw an exception, but // is now caught properly in the caller of validateLeafName. var validatedFile = DownloadPaths.createNiceUniqueFile(aLocalFolder); } else { validatedFile = aLocalFolder; } return validatedFile; }, // ---------- implementation methods ---------- // initDialog: Fill various dialog fields with initial content. initDialog() { // Put file name in window title. var suggestedFileName = this.mLauncher.suggestedFileName; this.mDialog.document.addEventListener("dialogaccept", this); this.mDialog.document.addEventListener("dialogcancel", this); let url = this.mLauncher.source; if (url instanceof Ci.nsINestedURI) { url = url.innermostURI; } let iconPath = "goat"; let fname = ""; if (suggestedFileName) { fname = iconPath = suggestedFileName; } else if (url instanceof Ci.nsIURL) { // A url, use file name from it. fname = iconPath = url.fileName; } else if (["data", "blob"].includes(url.scheme)) { // The path is useless for these, so use a reasonable default. let { MIMEType } = this.mLauncher.MIMEInfo; fname = lazy.gMIMEService.getValidFileName(null, MIMEType, url, 0); } else { fname = url.pathQueryRef; } this.mSourcePath = url.prePath; // Some URIs do not implement nsIURL, so we can't just QI. if (url instanceof Ci.nsIURL) { this.mSourcePath += url.directory; } else { // Don't make the url excessively long (e.g. for data URIs) // (this doesn't use a temp var to avoid copying a potentially // several mb-long string) this.mSourcePath += url.pathQueryRef.length > 500 ? url.pathQueryRef.substring(0, 500) + "\u2026" : url.pathQueryRef; } 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, displayName); var iconString = "moz-icon://" + iconPath + "?size=16&contentType=" + this.mLauncher.MIMEInfo.MIMEType; this.dialogElement("contentTypeImage").setAttribute("src", iconString); let dialog = this.mDialog.document.getElementById("unknownContentType"); // if always-save and is-executable and no-handler // then set up simple ui var mimeType = this.mLauncher.MIMEInfo.MIMEType; let isPlain = mimeType == "text/plain"; this.isExemptExecutableExtension = Services.policies.isExemptExecutableExtension( url.spec, fname?.split(".").at(-1) ); var shouldntRememberChoice = mimeType == "application/octet-stream" || mimeType == "application/x-msdownload" || (this.mLauncher.targetFileIsExecutable && !this.isExemptExecutableExtension) || // Do not offer to remember text/plain mimetype choices if the file // isn't actually a 'plain' text file. (isPlain && lazy.gReputationService.isBinary(suggestedFileName)); 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 = dialog.getButton("accept"); acceptButton.label = this.dialogElement("strings").getString( "unknownAccept.label" ); acceptButton.setAttribute("icon", "save"); dialog.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.initInteractiveControls(); // 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. if (shouldntRememberChoice) { rememberChoice.checked = false; rememberChoice.hidden = true; } else { rememberChoice.checked = !this.mLauncher.MIMEInfo.alwaysAskBeforeHandling && this.mLauncher.MIMEInfo.preferredAction != this.nsIMIMEInfo.handleInternally; } this.toggleRememberChoice(rememberChoice); } this.mDialog.setTimeout(function () { this.dialog.postShowCallback(); }, 0); this.delayHelper = new lazy.EnableDelayHelper({ disableDialog: () => { dialog.getButton("accept").disabled = true; }, enableDialog: () => { dialog.getButton("accept").disabled = false; }, focusTarget: this.mDialog, }); }, notify(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.promptForSaveDestination(); this._saveToDiskTimer = null; } }, postShowCallback() { this.mDialog.sizeToContent(); // Set initial focus this.dialogElement("mode").focus(); }, initIntro(url, displayName) { this.dialogElement("location").value = displayName; 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... let pathString; if (url instanceof Ci.nsIFileURL) { try { // Getting .file might throw, or .parent could be null pathString = url.file.parent.path; } catch (ex) {} } if (!pathString) { pathString = BrowserUtils.formatURIForDisplay(url, { showInsecureHTTP: true, }); } // 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() { // 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 || this.isExemptExecutableExtension ); } // 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() { // 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(aFile) { if (AppConstants.platform == "macosx") { return aFile.leafName || aFile.path; } return aFile.path; }, initInteractiveControls() { 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; var openHandler = this.dialogElement("openHandler"); if ( (this.mLauncher.targetFileIsExecutable && !this.isExemptExecutableExtension) || ((mimeType == "application/octet-stream" || mimeType == "application/x-msdos-program" || mimeType == "application/x-msdownload") && !openWithDefaultOK) ) { this.dialogElement("open").disabled = true; 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( Ci.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; } openHandler.selectedIndex = 0; var defaultOpenHandler = this.dialogElement("defaultHandler"); if (this.shouldShowInternalHandlerOption()) { this.dialogElement("handleInternally").hidden = false; } 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 if ( !this.dialogElement("handleInternally").hidden && this.mLauncher.MIMEInfo.preferredAction == this.nsIMIMEInfo.handleInternally ) { // Handle internally modeGroup.selectedItem = this.dialogElement("handleInternally"); } 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; if (this.dialogElement("open").selected) { modeGroup.selectedItem = this.dialogElement("save"); } } } otherHandler.nextSibling.hidden = otherHandler.nextSibling.nextSibling.hidden = false; this.updateOKButton(); }, // Returns the user-selected application helperAppChoice() { 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 ); }, get handleInternally() { return this.dialogElement("handleInternally").selected; }, toggleRememberChoice(aCheckbox) { this.dialogElement("settingsChange").hidden = !aCheckbox.checked; this.mDialog.sizeToContent(); }, openHandlerCommand() { var openHandler = this.dialogElement("openHandler"); if (openHandler.selectedItem.id == "choose") { this.chooseApp(); } else { openHandler.setAttribute( "lastSelectedItemID", openHandler.selectedItem.id ); } }, updateOKButton() { 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. let dialog = this.mDialog.document.getElementById("unknownContentType"); dialog.getButton("accept").disabled = !ok; }, // Returns true iff the user-specified helper app has been modified. appChanged() { return ( this.helperAppChoice() != this.mLauncher.MIMEInfo.preferredApplicationHandler ); }, updateMIMEInfo() { let { MIMEInfo } = this.mLauncher; // Don't erase the preferred choice being internal handler // -- this dialog is often the result of the handler fallback // (e.g. Content-Disposition was set as attachment) and we don't // want to inadvertently cause that to always show the dialog if // users don't want that behaviour. // Note: this is the same condition as the one in initDialog // which avoids ticking the checkbox. The user can still change // the action by ticking the checkbox, or by using the prefs to // manually select always ask (at which point `areAlwaysOpeningInternally` // will be false, which means `discardUpdate` will be false, which means // we'll store the last-selected option even if the filetype's pref is // set to always ask). let areAlwaysOpeningInternally = MIMEInfo.preferredAction == Ci.nsIMIMEInfo.handleInternally && !MIMEInfo.alwaysAskBeforeHandling; let discardUpdate = areAlwaysOpeningInternally && !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 if (this.useOtherHandler) { // 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; } } else if (this.handleInternally) { needUpdate = this.mLauncher.MIMEInfo.preferredAction != this.nsIMIMEInfo.handleInternally; if (needUpdate) { this.mLauncher.MIMEInfo.preferredAction = this.nsIMIMEInfo.handleInternally; } } // 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 handler service // (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, store this mime type in the // handler service. updateHelperAppPref() { var handlerInfo = this.mLauncher.MIMEInfo; var hs = Cc["@mozilla.org/uriloader/handler-service;1"].getService( Ci.nsIHandlerService ); hs.store(handlerInfo); }, onOK(aEvent) { // 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. let dialog = this.mDialog.document.getElementById("unknownContentType"); dialog.getButton("accept").disabled = true; this.dialogElement("mode").focus(); // Clear chosen application. this.chosenApp = null; // Leave dialog up. aEvent.preventDefault(); } } // Remove our web progress listener (a progress dialog will be // taking over). this.mLauncher.setWebProgressListener(null); // saveToDisk and setDownloadToLaunch 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) { // see @notify // we cannot use opener's setTimeout, see bug 420405 this._saveToDiskTimer = Cc["@mozilla.org/timer;1"].createInstance(nsITimer); this._saveToDiskTimer.initWithCallback(this, 0, nsITimer.TYPE_ONE_SHOT); } else { let uri = this.mLauncher.source; // Launch local files immediately without downloading them: if (uri instanceof Ci.nsIFileURL) { this.mLauncher.launchLocalFile(); } else { this.mLauncher.setDownloadToLaunch(this.handleInternally, null); } } // 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) { console.error(e); } this.onUnload(); }, onCancel() { // Remove our web progress listener. this.mLauncher.setWebProgressListener(null); // Cancel app launcher. try { this.mLauncher.cancel(Cr.NS_BINDING_ABORTED); } catch (e) { console.error(e); } this.onUnload(); }, onUnload() { this.mDialog.document.removeEventListener("dialogaccept", this); this.mDialog.document.removeEventListener("dialogcancel", this); // Unhook dialog from this object. this.mDialog.dialog = null; }, handleEvent(aEvent) { switch (aEvent.type) { case "dialogaccept": this.onOK(aEvent); break; case "dialogcancel": this.onCancel(); break; } }, dialogElement(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 Ci.nsILocalFileWin) { try { return file.getVersionInfoField("FileDescription"); } catch (e) {} } } else if (AppConstants.platform == "macosx") { if (file instanceof Ci.nsILocalFileMac) { try { return file.bundleDisplayName; } catch (e) {} } } return file.leafName; }, finishChooseApp() { 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() { 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.xhtml", 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 ("@mozilla.org/applicationchooser;1" in Cc) { var nsIApplicationChooser = Ci.nsIApplicationChooser; var appChooser = Cc["@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( Ci.nsILocalHandlerApp ); } contentTypeDialogObj.finishChooseApp(); }; appChooser.open(this.mLauncher.MIMEInfo.MIMEType, appChooserCallback); // The finishChooseApp is called from appChooserCallback return; } else { var nsIFilePicker = Ci.nsIFilePicker; var fp = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker); fp.init( this.mDialog, this.dialogElement("strings").getString("chooseAppFilePickerTitle"), nsIFilePicker.modeOpen ); fp.appendFilters(nsIFilePicker.filterApps); fp.open(aResult => { if (aResult == nsIFilePicker.returnOK && fp.file) { // Remember the file they chose to run. var localHandlerApp = Cc[ "@mozilla.org/uriloader/local-handler-app;1" ].createInstance(Ci.nsILocalHandlerApp); localHandlerApp.executable = fp.file; this.chosenApp = localHandlerApp; } this.finishChooseApp(); }); // The finishChooseApp is called from fp.open() callback return; } this.finishChooseApp(); }, shouldShowInternalHandlerOption() { let browsingContext = this.mDialog.BrowsingContext.get( this.mLauncher.browsingContextId ); let primaryExtension = ""; try { // The primaryExtension getter may throw if there are no // known extensions for this mimetype. primaryExtension = this.mLauncher.MIMEInfo.primaryExtension; } catch (e) {} // Only available for PDF files when pdf.js is enabled. // Skip if the current window uses the resource scheme, to avoid // showing the option when using the Download button in pdf.js. if (primaryExtension == "pdf") { return ( !( this.mLauncher.source.schemeIs("blob") || this.mLauncher.source.equalsExceptRef( browsingContext.currentWindowGlobal.documentURI ) ) && !Services.prefs.getBoolPref("pdfjs.disabled", true) && Services.prefs.getBoolPref( "browser.helperApps.showOpenOptionForPdfJS", false ) ); } return ( Services.prefs.getBoolPref( "browser.helperApps.showOpenOptionForViewableInternally", false ) && lazy.DownloadIntegration.shouldViewDownloadInternally( this.mLauncher.MIMEInfo.MIMEType, primaryExtension ) ); }, // Turn this on to get debugging messages. debug: false, // Dump text (if debug is on). dump(text) { if (this.debug) { dump(text); } }, };